@rs-x/cli 2.0.0-next.10 → 2.0.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rsx.cjs +186 -4
- package/package.json +1 -1
- package/{rs-x-vscode-extension-2.0.0-next.10.vsix → rs-x-vscode-extension-2.0.0-next.12.vsix} +0 -0
- package/templates/react-demo/README.md +113 -0
- package/templates/react-demo/index.html +12 -0
- package/templates/react-demo/src/app/app.tsx +87 -0
- package/templates/react-demo/src/app/hooks/use-virtual-table-controller.ts +24 -0
- package/templates/react-demo/src/app/hooks/use-virtual-table-viewport.ts +39 -0
- package/templates/react-demo/src/app/virtual-table/row-data.ts +35 -0
- package/templates/react-demo/src/app/virtual-table/row-model.ts +45 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-controller.ts +247 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-data.service.ts +126 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-row.tsx +38 -0
- package/templates/react-demo/src/app/virtual-table/virtual-table-shell.tsx +83 -0
- package/templates/react-demo/src/main.tsx +23 -0
- package/templates/react-demo/src/rsx-bootstrap.ts +18 -0
- package/templates/react-demo/src/styles.css +422 -0
- package/templates/react-demo/tsconfig.json +17 -0
- package/templates/react-demo/vite.config.ts +6 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { type RowData, type SortDirection, type SortKey } from './row-data';
|
|
2
|
+
import { createRowModel, updateRowModel, type RowModel } from './row-model';
|
|
3
|
+
import { VirtualTableDataService } from './virtual-table-data.service';
|
|
4
|
+
|
|
5
|
+
export type RowView = {
|
|
6
|
+
index: number;
|
|
7
|
+
top: number;
|
|
8
|
+
row: RowModel;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type VirtualTableSnapshot = {
|
|
12
|
+
visibleRows: RowView[];
|
|
13
|
+
totalRows: number;
|
|
14
|
+
poolSize: number;
|
|
15
|
+
rowsInView: number;
|
|
16
|
+
loadedPageCount: number;
|
|
17
|
+
spacerHeight: number;
|
|
18
|
+
sortKey: SortKey;
|
|
19
|
+
sortDirection: SortDirection;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ROW_HEIGHT = 36;
|
|
23
|
+
const PAGE_SIZE = 50;
|
|
24
|
+
const POOL_PAGES = 4;
|
|
25
|
+
const CACHE_PADDING_PAGES = 2;
|
|
26
|
+
const RETAIN_PADDING_PAGES = 4;
|
|
27
|
+
|
|
28
|
+
export class VirtualTableController {
|
|
29
|
+
public rowHeight = ROW_HEIGHT;
|
|
30
|
+
public readonly pageSize = PAGE_SIZE;
|
|
31
|
+
public readonly poolSize = PAGE_SIZE * POOL_PAGES;
|
|
32
|
+
public readonly totalRows: number;
|
|
33
|
+
|
|
34
|
+
private scrollTop = 0;
|
|
35
|
+
private viewportHeight = 520;
|
|
36
|
+
private sortKey: SortKey = 'id';
|
|
37
|
+
private sortDirection: SortDirection = 'asc';
|
|
38
|
+
private spacerHeight: number;
|
|
39
|
+
private rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
|
|
40
|
+
private visibleRows: RowView[] = [];
|
|
41
|
+
private snapshot: VirtualTableSnapshot;
|
|
42
|
+
|
|
43
|
+
private readonly listeners = new Set<() => void>();
|
|
44
|
+
private readonly pool = Array.from({ length: PAGE_SIZE * POOL_PAGES }, () =>
|
|
45
|
+
createRowModel(),
|
|
46
|
+
);
|
|
47
|
+
private readonly dataByIndex = new Map<number, RowData>();
|
|
48
|
+
private readonly loadedPages = new Set<number>();
|
|
49
|
+
private readonly pageLoading = new Map<number, Promise<void>>();
|
|
50
|
+
private readonly dataService = new VirtualTableDataService();
|
|
51
|
+
|
|
52
|
+
public constructor() {
|
|
53
|
+
this.totalRows = this.dataService.totalRows;
|
|
54
|
+
this.spacerHeight = this.totalRows * this.rowHeight;
|
|
55
|
+
this.snapshot = {
|
|
56
|
+
visibleRows: this.visibleRows,
|
|
57
|
+
totalRows: this.totalRows,
|
|
58
|
+
poolSize: this.poolSize,
|
|
59
|
+
rowsInView: this.rowsInView,
|
|
60
|
+
loadedPageCount: this.loadedPages.size,
|
|
61
|
+
spacerHeight: this.spacerHeight,
|
|
62
|
+
sortKey: this.sortKey,
|
|
63
|
+
sortDirection: this.sortDirection,
|
|
64
|
+
};
|
|
65
|
+
this.refresh();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public subscribe = (listener: () => void): (() => void) => {
|
|
69
|
+
this.listeners.add(listener);
|
|
70
|
+
return () => {
|
|
71
|
+
this.listeners.delete(listener);
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
public getSnapshot = (): VirtualTableSnapshot => {
|
|
76
|
+
return this.snapshot;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
public setViewportHeight(height: number): void {
|
|
80
|
+
this.viewportHeight = height;
|
|
81
|
+
this.rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
|
|
82
|
+
this.refresh();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public setRowHeight(height: number): void {
|
|
86
|
+
if (this.rowHeight === height) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.rowHeight = height;
|
|
91
|
+
this.spacerHeight = this.totalRows * this.rowHeight;
|
|
92
|
+
this.rowsInView = Math.max(1, Math.ceil(this.viewportHeight / this.rowHeight));
|
|
93
|
+
this.refresh();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public setScrollTop(value: number): void {
|
|
97
|
+
this.scrollTop = value;
|
|
98
|
+
this.refresh();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public toggleSort(nextKey: SortKey): void {
|
|
102
|
+
if (this.sortKey === nextKey) {
|
|
103
|
+
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
104
|
+
} else {
|
|
105
|
+
this.sortKey = nextKey;
|
|
106
|
+
this.sortDirection = 'asc';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.resetLoadedData();
|
|
110
|
+
this.refresh();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private refresh(): void {
|
|
114
|
+
const scrollIndex = Math.floor(this.scrollTop / this.rowHeight);
|
|
115
|
+
const bufferTop = Math.max(0, Math.floor((this.poolSize - this.rowsInView) / 2));
|
|
116
|
+
const maxStart = Math.max(0, this.totalRows - this.poolSize);
|
|
117
|
+
const startIndex = Math.min(Math.max(scrollIndex - bufferTop, 0), maxStart);
|
|
118
|
+
const endIndex = Math.min(startIndex + this.poolSize, this.totalRows);
|
|
119
|
+
const startPage = Math.max(
|
|
120
|
+
0,
|
|
121
|
+
Math.floor(startIndex / this.pageSize) - CACHE_PADDING_PAGES,
|
|
122
|
+
);
|
|
123
|
+
const endPage = Math.min(
|
|
124
|
+
Math.floor((endIndex - 1) / this.pageSize) + CACHE_PADDING_PAGES,
|
|
125
|
+
Math.floor((this.totalRows - 1) / this.pageSize),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
this.ensurePages(startPage, endPage);
|
|
129
|
+
this.pruneCachedPages(startPage, endPage);
|
|
130
|
+
|
|
131
|
+
const nextRows: RowView[] = [];
|
|
132
|
+
const length = endIndex - startIndex;
|
|
133
|
+
|
|
134
|
+
for (let offset = 0; offset < length; offset += 1) {
|
|
135
|
+
const index = startIndex + offset;
|
|
136
|
+
const target = this.pool[offset];
|
|
137
|
+
updateRowModel(target, this.getRowData(index));
|
|
138
|
+
|
|
139
|
+
nextRows.push({
|
|
140
|
+
index,
|
|
141
|
+
top: index * this.rowHeight,
|
|
142
|
+
row: target,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.visibleRows = nextRows;
|
|
147
|
+
this.snapshot = {
|
|
148
|
+
visibleRows: this.visibleRows,
|
|
149
|
+
totalRows: this.totalRows,
|
|
150
|
+
poolSize: this.poolSize,
|
|
151
|
+
rowsInView: this.rowsInView,
|
|
152
|
+
loadedPageCount: this.loadedPages.size,
|
|
153
|
+
spacerHeight: this.spacerHeight,
|
|
154
|
+
sortKey: this.sortKey,
|
|
155
|
+
sortDirection: this.sortDirection,
|
|
156
|
+
};
|
|
157
|
+
this.emit();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private emit(): void {
|
|
161
|
+
for (const listener of this.listeners) {
|
|
162
|
+
listener();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private ensurePages(startPage: number, endPage: number): void {
|
|
167
|
+
for (let pageIndex = startPage; pageIndex <= endPage; pageIndex += 1) {
|
|
168
|
+
this.ensurePageLoaded(pageIndex);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private ensurePageLoaded(pageIndex: number): void {
|
|
173
|
+
if (this.loadedPages.has(pageIndex) || this.pageLoading.has(pageIndex)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const task = this.loadPageAsync(pageIndex).finally(() => {
|
|
178
|
+
this.pageLoading.delete(pageIndex);
|
|
179
|
+
this.loadedPages.add(pageIndex);
|
|
180
|
+
this.refresh();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.pageLoading.set(pageIndex, task);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async loadPageAsync(pageIndex: number): Promise<void> {
|
|
187
|
+
const page = await this.dataService.fetchPage(
|
|
188
|
+
pageIndex,
|
|
189
|
+
this.pageSize,
|
|
190
|
+
this.sortKey,
|
|
191
|
+
this.sortDirection,
|
|
192
|
+
);
|
|
193
|
+
const startIndex = pageIndex * this.pageSize;
|
|
194
|
+
|
|
195
|
+
for (let offset = 0; offset < page.items.length; offset += 1) {
|
|
196
|
+
const item = page.items[offset];
|
|
197
|
+
if (!item) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.dataByIndex.set(startIndex + offset, item);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private getRowData(index: number): RowData {
|
|
206
|
+
const cached = this.dataByIndex.get(index);
|
|
207
|
+
if (cached) {
|
|
208
|
+
return cached;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
id: index + 1,
|
|
213
|
+
name: 'Loading...',
|
|
214
|
+
price: 0,
|
|
215
|
+
quantity: 0,
|
|
216
|
+
category: 'Pending',
|
|
217
|
+
updatedAt: '--',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private resetLoadedData(): void {
|
|
222
|
+
this.dataByIndex.clear();
|
|
223
|
+
this.loadedPages.clear();
|
|
224
|
+
this.pageLoading.clear();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private pruneCachedPages(startPage: number, endPage: number): void {
|
|
228
|
+
const minPage = Math.max(0, startPage - RETAIN_PADDING_PAGES);
|
|
229
|
+
const maxPage = Math.min(
|
|
230
|
+
Math.floor((this.totalRows - 1) / this.pageSize),
|
|
231
|
+
endPage + RETAIN_PADDING_PAGES,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
for (const pageIndex of Array.from(this.loadedPages)) {
|
|
235
|
+
if (pageIndex >= minPage && pageIndex <= maxPage) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.loadedPages.delete(pageIndex);
|
|
240
|
+
const pageStart = pageIndex * this.pageSize;
|
|
241
|
+
const pageEnd = Math.min(pageStart + this.pageSize, this.totalRows);
|
|
242
|
+
for (let index = pageStart; index < pageEnd; index += 1) {
|
|
243
|
+
this.dataByIndex.delete(index);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRowData,
|
|
3
|
+
type RowData,
|
|
4
|
+
type SortDirection,
|
|
5
|
+
type SortKey,
|
|
6
|
+
} from './row-data';
|
|
7
|
+
|
|
8
|
+
export type VirtualTablePage = {
|
|
9
|
+
total: number;
|
|
10
|
+
items: RowData[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TOTAL_ROWS = 1_000_000;
|
|
14
|
+
const REQUEST_DELAY_MS = 120;
|
|
15
|
+
const CATEGORY_COUNT = 4;
|
|
16
|
+
const PRICE_BUCKET_COUNT = 1_000;
|
|
17
|
+
const QUANTITY_BUCKET_COUNT = 100;
|
|
18
|
+
const MAX_CACHED_PAGES = 24;
|
|
19
|
+
|
|
20
|
+
export class VirtualTableDataService {
|
|
21
|
+
private readonly pageCache = new Map<string, VirtualTablePage>();
|
|
22
|
+
|
|
23
|
+
public get totalRows(): number {
|
|
24
|
+
return TOTAL_ROWS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async fetchPage(
|
|
28
|
+
pageIndex: number,
|
|
29
|
+
pageSize: number,
|
|
30
|
+
sortKey: SortKey,
|
|
31
|
+
sortDirection: SortDirection,
|
|
32
|
+
): Promise<VirtualTablePage> {
|
|
33
|
+
const cacheKey = `${sortKey}:${sortDirection}:${pageIndex}:${pageSize}`;
|
|
34
|
+
const cached = this.pageCache.get(cacheKey);
|
|
35
|
+
if (cached) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await this.delay(REQUEST_DELAY_MS + (pageIndex % 5) * 35);
|
|
40
|
+
|
|
41
|
+
const startIndex = pageIndex * pageSize;
|
|
42
|
+
const items: RowData[] = [];
|
|
43
|
+
const endIndex = Math.min(startIndex + pageSize, TOTAL_ROWS);
|
|
44
|
+
|
|
45
|
+
for (let visualIndex = startIndex; visualIndex < endIndex; visualIndex += 1) {
|
|
46
|
+
const id = this.getIdAtVisualIndex(visualIndex, sortKey, sortDirection);
|
|
47
|
+
items.push(createRowData(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const page = { total: TOTAL_ROWS, items };
|
|
51
|
+
this.pageCache.set(cacheKey, page);
|
|
52
|
+
this.trimCache();
|
|
53
|
+
return page;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getIdAtVisualIndex(
|
|
57
|
+
visualIndex: number,
|
|
58
|
+
sortKey: SortKey,
|
|
59
|
+
sortDirection: SortDirection,
|
|
60
|
+
): number {
|
|
61
|
+
const normalizedIndex =
|
|
62
|
+
sortDirection === 'asc' ? visualIndex : TOTAL_ROWS - 1 - visualIndex;
|
|
63
|
+
|
|
64
|
+
if (sortKey === 'price') {
|
|
65
|
+
return this.getPriceSortedId(normalizedIndex);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sortKey === 'quantity') {
|
|
69
|
+
return this.getQuantitySortedId(normalizedIndex);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (sortKey === 'category') {
|
|
73
|
+
return this.getCategorySortedId(normalizedIndex);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return normalizedIndex + 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getPriceSortedId(visualIndex: number): number {
|
|
80
|
+
const groupSize = TOTAL_ROWS / PRICE_BUCKET_COUNT;
|
|
81
|
+
const priceBucket = Math.floor(visualIndex / groupSize);
|
|
82
|
+
const offsetInBucket = visualIndex % groupSize;
|
|
83
|
+
|
|
84
|
+
return priceBucket + offsetInBucket * PRICE_BUCKET_COUNT + 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private getQuantitySortedId(visualIndex: number): number {
|
|
88
|
+
const groupSize = TOTAL_ROWS / QUANTITY_BUCKET_COUNT;
|
|
89
|
+
const quantityBucket = Math.floor(visualIndex / groupSize);
|
|
90
|
+
const offsetInBucket = visualIndex % groupSize;
|
|
91
|
+
const quantityStride = PRICE_BUCKET_COUNT * QUANTITY_BUCKET_COUNT;
|
|
92
|
+
const quantityBlock = Math.floor(offsetInBucket / PRICE_BUCKET_COUNT);
|
|
93
|
+
const priceBucket = offsetInBucket % PRICE_BUCKET_COUNT;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
priceBucket +
|
|
97
|
+
quantityBucket * PRICE_BUCKET_COUNT +
|
|
98
|
+
quantityBlock * quantityStride +
|
|
99
|
+
1
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getCategorySortedId(visualIndex: number): number {
|
|
104
|
+
const groupSize = TOTAL_ROWS / CATEGORY_COUNT;
|
|
105
|
+
const categoryBucket = Math.floor(visualIndex / groupSize);
|
|
106
|
+
const offsetInBucket = visualIndex % groupSize;
|
|
107
|
+
|
|
108
|
+
return categoryBucket + offsetInBucket * CATEGORY_COUNT + 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private delay(durationMs: number): Promise<void> {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
window.setTimeout(resolve, durationMs);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private trimCache(): void {
|
|
118
|
+
while (this.pageCache.size > MAX_CACHED_PAGES) {
|
|
119
|
+
const oldestKey = this.pageCache.keys().next().value as string | undefined;
|
|
120
|
+
if (!oldestKey) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.pageCache.delete(oldestKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FC, memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useRsxExpression } from '@rs-x/react';
|
|
4
|
+
|
|
5
|
+
import type { RowView } from './virtual-table-controller';
|
|
6
|
+
|
|
7
|
+
type VirtualTableRowProps = {
|
|
8
|
+
item: RowView;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const VirtualTableRowComponent: FC<VirtualTableRowProps> = ({ item }) => {
|
|
12
|
+
const id = useRsxExpression(item.row.idExpr);
|
|
13
|
+
const name = useRsxExpression(item.row.nameExpr);
|
|
14
|
+
const category = useRsxExpression(item.row.categoryExpr);
|
|
15
|
+
const price = useRsxExpression(item.row.priceExpr);
|
|
16
|
+
const quantity = useRsxExpression(item.row.quantityExpr);
|
|
17
|
+
const total = useRsxExpression(item.row.totalExpr);
|
|
18
|
+
const updatedAt = useRsxExpression(item.row.updatedAtExpr);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className="table-row"
|
|
23
|
+
style={{ transform: `translateY(${item.top}px)` }}
|
|
24
|
+
>
|
|
25
|
+
<span data-label="ID">#{id ?? 0}</span>
|
|
26
|
+
<span data-label="Name">{name ?? ''}</span>
|
|
27
|
+
<span data-label="Category">{category ?? ''}</span>
|
|
28
|
+
<span data-label="Price">€{price ?? 0}</span>
|
|
29
|
+
<span data-label="Qty">{quantity ?? 0}</span>
|
|
30
|
+
<span data-label="Total" className="total">
|
|
31
|
+
€{total ?? 0}
|
|
32
|
+
</span>
|
|
33
|
+
<span data-label="Updated">{updatedAt ?? '--'}</span>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const VirtualTableRow = memo(VirtualTableRowComponent);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useVirtualTableController } from '../hooks/use-virtual-table-controller';
|
|
4
|
+
import { useVirtualTableViewport } from '../hooks/use-virtual-table-viewport';
|
|
5
|
+
import { VirtualTableRow } from './virtual-table-row';
|
|
6
|
+
|
|
7
|
+
export const VirtualTableShell: FC = () => {
|
|
8
|
+
const { controller, snapshot } = useVirtualTableController();
|
|
9
|
+
const viewportRef = useVirtualTableViewport(controller);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<section className="table-toolbar">
|
|
14
|
+
<div className="toolbar-left">
|
|
15
|
+
<h2>Inventory Snapshot</h2>
|
|
16
|
+
<p>
|
|
17
|
+
{snapshot.totalRows} rows • {snapshot.poolSize} pre-wired models
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="toolbar-right">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => {
|
|
24
|
+
controller.toggleSort('price');
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
Sort by price
|
|
28
|
+
</button>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => {
|
|
32
|
+
controller.toggleSort('quantity');
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
Sort by stock
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => {
|
|
40
|
+
controller.toggleSort('name');
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
Sort by name
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<div className="table-header">
|
|
49
|
+
<span>ID</span>
|
|
50
|
+
<span>Name</span>
|
|
51
|
+
<span>Category</span>
|
|
52
|
+
<span>Price</span>
|
|
53
|
+
<span>Qty</span>
|
|
54
|
+
<span>Total</span>
|
|
55
|
+
<span>Updated</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div
|
|
59
|
+
ref={viewportRef}
|
|
60
|
+
className="table-viewport"
|
|
61
|
+
onScroll={(event) => {
|
|
62
|
+
controller.setScrollTop(event.currentTarget.scrollTop);
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className="table-spacer"
|
|
67
|
+
style={{ height: `${snapshot.spacerHeight}px` }}
|
|
68
|
+
/>
|
|
69
|
+
{snapshot.visibleRows.map((item) => (
|
|
70
|
+
<VirtualTableRow key={item.index} item={item} />
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="table-footer">
|
|
75
|
+
<div>
|
|
76
|
+
Rows in view: {snapshot.rowsInView} • Loaded pages:{' '}
|
|
77
|
+
{snapshot.loadedPageCount}
|
|
78
|
+
</div>
|
|
79
|
+
<div>Scroll to stream pages from a 1,000,000-row virtual dataset.</div>
|
|
80
|
+
</div>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
import { App } from './app/app';
|
|
5
|
+
import { initRsx } from './rsx-bootstrap';
|
|
6
|
+
import './styles.css';
|
|
7
|
+
|
|
8
|
+
async function start(): Promise<void> {
|
|
9
|
+
await initRsx();
|
|
10
|
+
|
|
11
|
+
const rootElement = document.getElementById('root');
|
|
12
|
+
if (!rootElement) {
|
|
13
|
+
throw new Error('Missing #root element');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
createRoot(rootElement).render(
|
|
17
|
+
<StrictMode>
|
|
18
|
+
<App />
|
|
19
|
+
</StrictMode>,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void start();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InjectionContainer } from '@rs-x/core';
|
|
2
|
+
import { RsXExpressionParserModule } from '@rs-x/expression-parser';
|
|
3
|
+
|
|
4
|
+
import { registerRsxAotCompiledExpressions } from './rsx-generated/rsx-aot-compiled.generated';
|
|
5
|
+
import { registerRsxAotParsedExpressionCache } from './rsx-generated/rsx-aot-preparsed.generated';
|
|
6
|
+
|
|
7
|
+
let initialized = false;
|
|
8
|
+
|
|
9
|
+
export async function initRsx(): Promise<void> {
|
|
10
|
+
if (initialized) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerRsxAotParsedExpressionCache();
|
|
15
|
+
registerRsxAotCompiledExpressions();
|
|
16
|
+
await InjectionContainer.load(RsXExpressionParserModule);
|
|
17
|
+
initialized = true;
|
|
18
|
+
}
|