@@ -0,0 +1,290 @@
import { Component , OnInit , ChangeDetectionStrategy , ChangeDetectorRef } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { FormsModule } from '@angular/forms' ;
import {
PretextVirtualScrollDirective ,
type PretextScrollEvent ,
type PretextColumnDef ,
} from 'ngx-pretext-table' ;
interface DemoRow {
id : number ;
name : string ;
description : string ;
tags : string [ ] ;
status : string ;
}
@Component ( {
selector : 'app-demo' ,
standalone : true ,
changeDetection : ChangeDetectionStrategy.OnPush ,
imports : [ CommonModule , FormsModule , PretextVirtualScrollDirective ] ,
template : `
<div class="demo-header">
<h2>Pretext + Virtual Scroll: Mixed Content Demo</h2>
<p>{{ totalRows | number }} rows — text measured by <strong>pretext</strong>,
inputs/tags use height registry, DOM corrections via ResizeObserver.</p>
<div class="stats">
<span class="stat">Visible: {{ rangeStart }}– {{ rangeEnd }}</span>
<span class="stat">Rendered: {{ visibleData.length }} rows</span>
<span class="stat">Total: {{ totalRows | number }}</span>
</div>
</div>
<div pretextVirtualScroll
[data]="allData"
[columns]="pretextColumns"
[font]="font"
[lineHeight]="lineHeight"
[rowPadding]="rowPadding"
[minRowHeight]="44"
[scrollHeight]="'560px'"
[bufferRows]="5"
[measureRows]="true"
(visibleRangeChange)="onVisibleRangeChange( $ event)"
class="scroll-container">
<table class="pretext-table">
<thead>
<tr>
<th class="col-id">ID</th>
<th class="col-name">Name</th>
<th class="col-desc">Description</th>
<th class="col-tags">Tags</th>
<th class="col-status">Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of visibleData; let i = index"
[attr.data-vs-row]="rangeStart + i"
[style.height.px]="visibleRowHeights[i]">
<td class="col-id">{{ row.id }}</td>
<td class="col-name">{{ row.name }}</td>
<td class="col-desc wrap-cell">{{ row.description }}</td>
<td class="col-tags">
<span class="tag" *ngFor="let tag of row.tags">{{ tag }}</span>
</td>
<td class="col-status">
<select [ngModel]="row.status" class="status-select">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot text-dot"></span> Description: pretext text layout
</div>
<div class="legend-item">
<span class="legend-dot fixed-dot"></span> Status: fixed height (36px)
</div>
<div class="legend-item">
<span class="legend-dot compute-dot"></span> Tags: computed from tag count
</div>
<div class="legend-item">
<span class="legend-dot measure-dot"></span> All rows: ResizeObserver correction
</div>
</div>
` ,
styles : [ `
:host {
display: block;
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.demo-header { margin-bottom: 16px; }
.demo-header h2 { margin: 0 0 8px; font-size: 20px; }
.demo-header p { margin: 0 0 12px; color: #555; font-size: 14px; }
.stats { display: flex; gap: 12px; flex-wrap: wrap; }
.stat {
padding: 4px 10px;
background: #f0f0f0;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
}
.scroll-container {
border: 1px solid #dee2e6;
border-radius: 6px;
background: #fff;
}
.pretext-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 14px;
}
.pretext-table thead {
position: sticky;
top: 0;
z-index: 1;
background: #f8f9fa;
}
.pretext-table th {
padding: 10px 8px;
text-align: left;
font-weight: 600;
font-size: 13px;
color: #333;
border-bottom: 2px solid #dee2e6;
}
.pretext-table td {
padding: 8px;
vertical-align: top;
border-bottom: 1px solid #eee;
color: #444;
}
.pretext-table tr:hover td { background: #f8f9fa; }
.col-id { width: 50px; }
.col-name { width: 130px; }
.col-desc { width: 350px; }
.col-tags { width: 250px; }
.col-status { width: 120px; }
.wrap-cell {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
.tag {
display: inline-block;
padding: 2px 8px;
margin: 2px 4px 2px 0;
background: #e8f0fe;
color: #1a73e8;
border-radius: 12px;
font-size: 12px;
white-space: nowrap;
}
.status-select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
background: #fff;
}
.legend {
margin-top: 12px;
display: flex;
gap: 20px;
flex-wrap: wrap;
font-size: 12px;
color: #666;
}
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
display: inline-block;
}
.text-dot { background: #4caf50; }
.fixed-dot { background: #ff9800; }
.compute-dot { background: #2196f3; }
.measure-dot { background: #9c27b0; }
` ] ,
} )
export class DemoComponent implements OnInit {
readonly totalRows = 10 _000 ;
readonly font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' ;
readonly lineHeight = 20 ;
readonly rowPadding = 16 ;
allData : DemoRow [ ] = [ ] ;
visibleData : DemoRow [ ] = [ ] ;
visibleRowHeights : number [ ] = [ ] ;
rangeStart = 0 ;
rangeEnd = 0 ;
pretextColumns : PretextColumnDef [ ] = [
{ field : 'id' , width : 34 } ,
{ field : 'name' , width : 114 } ,
{ field : 'description' , width : 334 } ,
{
field : 'tags' ,
width : 234 ,
heightMode : {
kind : 'compute' ,
fn : ( tags : string [ ] ) = > {
if ( ! tags || tags . length === 0 ) return 20 ;
const tagsPerRow = Math . max ( 1 , Math . floor ( 234 / 74 ) ) ;
const rows = Math . ceil ( tags . length / tagsPerRow ) ;
return rows * 24 + ( rows - 1 ) * 4 ;
} ,
} ,
} ,
{
field : 'status' ,
width : 104 ,
heightMode : { kind : 'fixed' , height : 36 } ,
} ,
] ;
constructor ( private cdr : ChangeDetectorRef ) { }
ngOnInit ( ) : void {
this . allData = this . generateData ( this . totalRows ) ;
}
onVisibleRangeChange ( event : PretextScrollEvent ) : void {
this . visibleData = this . allData . slice ( event . start , event . end ) ;
this . visibleRowHeights = event . rowHeights ;
this . rangeStart = event . start ;
this . rangeEnd = event . end ;
this . cdr . markForCheck ( ) ;
}
private generateData ( count : number ) : DemoRow [ ] {
const lorem = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' ,
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' ,
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.' ,
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.' ,
'Excepteur sint occaecat cupidatat non proident, sunt in culpa.' ,
'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit.' ,
'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet.' ,
'At vero eos et accusamus et iusto odio dignissimos ducimus.' ,
] ;
const names = [
'Alice Johnson' , 'Bob Smith' , 'Charlie Brown' , 'Diana Prince' ,
'Eve Wilson' , 'Frank Castle' , 'Grace Hopper' , 'Henry Ford' ,
'Iris Chang' , 'Jack Ryan' , 'Kate Moss' , 'Leo Messi' ,
] ;
const tagPool = [
'Angular' , 'React' , 'Vue' , 'Svelte' , 'TypeScript' ,
'JavaScript' , 'CSS' , 'HTML' , 'Node.js' , 'Python' ,
'Docker' , 'K8s' , 'AWS' , 'Azure' , 'GCP' ,
] ;
const statuses = [ 'active' , 'inactive' , 'pending' ] ;
return Array . from ( { length : count } , ( _ , i ) = > ( {
id : i + 1 ,
name : names [ i % names . length ] ,
description : Array.from ( { length : 1 + ( i % 5 ) } , ( _ , j ) = >
lorem [ ( i + j ) % lorem . length ] ) . join ( ' ' ) ,
tags : Array.from ( { length : i % 7 } , ( _ , j ) = >
tagPool [ ( i + j * 3 ) % tagPool . length ] ) ,
status : statuses [ i % statuses . length ] ,
} ) ) ;
}
}