@reforgium/data-grid 3.2.0 → 3.2.3

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/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [3.2.3]: 13.05.2026
2
+
3
+ ### Fix:
4
+ - `DataGrid`: source/infinity — `totalRowCount` no longer collapses to `0` (empty state) after the internal page buffer evicts early pages on long scrolls when the source does not return `totalElements`; row-count knowledge is now tracked via a monotonic high-water mark (`knownRowCount`) plus end-of-data state (`endReached` / `confirmedTotal`) updated when each page lands, instead of walking the eviction-bound `sourcePages` map
5
+ - `DataGrid`: source/infinity — buffer eviction now anchors `pageBase` and never evicts it, so `localToAbsolutePage` and contiguous-loaded-row accounting stay correct after scrolling past `MAX_BUFFERED_PAGES`
6
+
7
+ ### Test:
8
+ - added regression coverage: monotonic `totalRowCount` without `totalElements` (optimistic probe page, fixed end after short page, post-end full page does not unwind the total), 120-page scroll past `MAX_BUFFERED_PAGES` keeps `totalRowCount` stable, and anchor page survives eviction
9
+
10
+ ### Docs:
11
+ - `GridPagedDataSource.version`: tightened JSDoc — without `totalElements` the grid pins end-of-data on the first short/empty page, so the source must bump `version` on dataset growth/replacement to clear the pin; mutating `items()` in place without a bump leaves the grid with a stale row count
12
+
13
+ ---
14
+
15
+ ## [3.2.1]: 17.04.2026
16
+
17
+ ### Fix:
18
+ - `DataGrid`: source/pagination now respects exact `totalElements` when it is provided instead of collapsing paginator state to the current page payload length
19
+ - `DataGrid`: source/pagination without `totalElements` now exposes one optimistic extra page when the current page is full (`items.length === pageSize`), so the consumer can perform the final probe request and detect the real end on the first short/empty page
20
+
21
+ ### Docs:
22
+ - `README`: documented `GridPagedDataSource.totalElements` behavior for source-driven pagination without a known total
23
+
24
+ ### Test:
25
+ - added regression coverage for source/pagination with exact `totalElements` and for the full-page-without-total probe flow
26
+
27
+ ---
28
+
1
29
  ## [3.2.0]: 14.04.2026
2
30
 
3
31
  ### Feat:
package/README.md CHANGED
@@ -303,7 +303,9 @@ Data source guidance:
303
303
  - Use `data` when the parent already owns the full rendered collection.
304
304
  - Use `source` for server-driven pagination or infinity flows where the parent should expose only the current page.
305
305
  - In `mode="infinity"` + `source`, the grid uses `totalElements` for scroll height when available and stores loaded page chunks internally instead of forcing the parent to append rows into one large array.
306
- - `GridPagedDataSource.totalElements` is optional for infinity sources. If omitted, the grid keeps requesting pages until it receives a short page (`items.length < pageSize`) and then treats that page as the end of the dataset.
306
+ - `GridPagedDataSource.totalElements` is optional for source-driven infinity and pagination flows.
307
+ - In `mode="infinity"`, if `totalElements` is omitted, the grid keeps requesting pages until it receives a short page (`items.length < pageSize`) and then treats that page as the end of the dataset.
308
+ - In `mode="pagination"`, if `totalElements` is omitted, the grid exposes one optimistic extra page while the current page is full. The final size becomes known only after a short page is returned.
307
309
  - In `source` mode, initialize the source from the parent after all filters / params are ready; the grid does not perform an automatic first fetch on mount.
308
310
  - `GridPagedDataSource.error` is optional and can be used by consumers that want to project source-level error UI near the grid.
309
311
  - `GridPagedDataSource.sort`, `updateSort(...)`, and `updateSorts(...)` let the grid use source-owned sort state instead of forcing a separate parent bridge.
@@ -388,21 +390,21 @@ Notes:
388
390
 
389
391
  The optional `source` input accepts a page-oriented contract:
390
392
 
391
- | Field / method | Type | Description |
392
- |---|---|---|
393
- | `items` | `Signal<T[]>` | Current page items |
394
- | `loading` | `Signal<boolean>` | Source loading state |
395
- | `error` | `Signal<unknown \| null>` | Optional source error state |
396
- | `page` | `number` | Current page index |
397
- | `pageSize` | `number` | Current page size |
398
- | `totalElements` | `number \| undefined` | Optional total size |
399
- | `sort` | `ReadonlyArray<GridSortItem<T>> \| undefined` | Optional source-owned sort state |
400
- | `version` | `Signal<number> \| undefined` | Dataset reset marker for local buffer invalidation |
401
- | `updatePage(page)` | `(page: number) => Promise<unknown>` | Load a page |
402
- | `updatePageSize(size)` | `((size: number) => Promise<unknown>) \| undefined` | Optional page-size handler |
403
- | `updateSort(sort)` | `((sort?: GridSortItem<T> \| null) => Promise<unknown>) \| undefined` | Optional single-sort handler |
404
- | `updateSorts(sort)` | `((sort?: ReadonlyArray<GridSortItem<T>> \| null) => Promise<unknown>) \| undefined` | Optional multi-sort handler |
405
- | `prefetchMode` | `'sequential' \| 'parallel' \| undefined` | Infinity prefetch strategy |
393
+ | Field / method | Type | Description |
394
+ |------------------------|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
395
+ | `items` | `Signal<T[]>` | Current page items |
396
+ | `loading` | `Signal<boolean>` | Source loading state |
397
+ | `error` | `Signal<unknown \| null>` | Optional source error state |
398
+ | `page` | `number` | Current page index |
399
+ | `pageSize` | `number` | Current page size |
400
+ | `totalElements` | `number \| undefined` | Optional exact total size; if omitted, `infinity` stops on the first short page and `pagination` exposes one optimistic extra page while the current page is full |
401
+ | `sort` | `ReadonlyArray<GridSortItem<T>> \| undefined` | Optional source-owned sort state |
402
+ | `version` | `Signal<number> \| undefined` | Dataset reset marker for local buffer invalidation |
403
+ | `updatePage(page)` | `(page: number) => Promise<unknown>` | Load a page |
404
+ | `updatePageSize(size)` | `((size: number) => Promise<unknown>) \| undefined` | Optional page-size handler |
405
+ | `updateSort(sort)` | `((sort?: GridSortItem<T> \| null) => Promise<unknown>) \| undefined` | Optional single-sort handler |
406
+ | `updateSorts(sort)` | `((sort?: ReadonlyArray<GridSortItem<T>> \| null) => Promise<unknown>) \| undefined` | Optional multi-sort handler |
407
+ | `prefetchMode` | `'sequential' \| 'parallel' \| undefined` | Infinity prefetch strategy |
406
408
 
407
409
  This contract is intentionally grid-like, not `statum`-specific, but `PagedQueryStore` already matches it well enough to be used directly.
408
410
 
@@ -1,4 +1,4 @@
1
- import { c as computeScrollbarState, a as clampThumbTop, m as mapThumbTopToScrollTop } from './reforgium-data-grid-reforgium-data-grid-Cb2oAQjG.mjs';
1
+ import { c as computeScrollbarState, a as clampThumbTop, m as mapThumbTopToScrollTop } from './reforgium-data-grid-reforgium-data-grid-DUpCJ9P0.mjs';
2
2
 
3
3
  function createGridOverlayScrollFeature(ctx) {
4
4
  const showScrollbar = () => {
@@ -76,4 +76,4 @@ function createGridOverlayScrollFeature(ctx) {
76
76
  }
77
77
 
78
78
  export { createGridOverlayScrollFeature };
79
- //# sourceMappingURL=reforgium-data-grid-grid-overlay-scroll.feature-6tkNtIuO.mjs.map
79
+ //# sourceMappingURL=reforgium-data-grid-grid-overlay-scroll.feature-DQp-sU6c.mjs.map
@@ -2162,6 +2162,12 @@ function createGridSourceDataFeature(ctx) {
2162
2162
  let requestGeneration = 0;
2163
2163
  let hasReceivedData = false;
2164
2164
  let pageBase = 0;
2165
+ // Row knowledge accounting. Decoupled from `sourcePages` so that buffer eviction
2166
+ // (MAX_BUFFERED_PAGES) cannot collapse totalRowCount back to 0 once a source
2167
+ // without `totalElements` has scrolled past the buffer window.
2168
+ let knownRowCount = 0;
2169
+ let endReached = false;
2170
+ let confirmedTotal = null;
2165
2171
  const hasSessionState = () => sourcePages.size > 0 || pendingPages.size > 0 || queuedPages.length > 0 || activeRequests > 0 || hasReceivedData;
2166
2172
  const resolvedLoading = () => ctx.getSource()?.loading() ?? ctx.getFallbackLoading();
2167
2173
  const totalRowCount = () => {
@@ -2177,16 +2183,19 @@ function createGridSourceDataFeature(ctx) {
2177
2183
  const shiftedTotal = Math.max(0, knownTotal - pageBase * pageSize);
2178
2184
  return shiftedTotal > 0 ? shiftedTotal : (source.items()?.length ?? 0);
2179
2185
  }
2180
- const loadedRows = contiguousLoadedRowCount(source);
2181
- if (loadedRows <= 0) {
2182
- return 0;
2186
+ if (endReached) {
2187
+ return confirmedTotal ?? 0;
2183
2188
  }
2184
- if (hasReachedInfinityEnd(source)) {
2185
- return loadedRows;
2189
+ if (knownRowCount <= 0) {
2190
+ return 0;
2186
2191
  }
2187
- return loadedRows + Math.max(1, source.pageSize || ctx.getFallbackPageSize());
2192
+ return knownRowCount + Math.max(1, source.pageSize || ctx.getFallbackPageSize());
2188
2193
  }
2189
- return source.items()?.length ?? 0;
2194
+ const knownTotal = resolvedTotalElements(source);
2195
+ if (knownTotal !== null) {
2196
+ return knownTotal;
2197
+ }
2198
+ return estimateOpenEndedPaginationTotal(source);
2190
2199
  };
2191
2200
  const selectionRows = () => {
2192
2201
  const source = ctx.getSource();
@@ -2337,7 +2346,10 @@ function createGridSourceDataFeature(ctx) {
2337
2346
  }
2338
2347
  pageBase = sourcePages.size === 0 ? Math.max(0, source.page || 0) : pageBase;
2339
2348
  const pageItems = source.items() ?? [];
2340
- sourcePages.set(Math.max(0, source.page || 0), pageItems);
2349
+ const sourcePage = Math.max(0, source.page || 0);
2350
+ const pageSize = Math.max(1, source.pageSize || ctx.getFallbackPageSize());
2351
+ sourcePages.set(sourcePage, pageItems);
2352
+ recordPageKnowledge(sourcePage, pageItems, pageSize);
2341
2353
  if (!hasReceivedData && pageItems.length > 0) {
2342
2354
  hasReceivedData = true;
2343
2355
  }
@@ -2359,8 +2371,37 @@ function createGridSourceDataFeature(ctx) {
2359
2371
  pendingPages.clear();
2360
2372
  activeRequests = 0;
2361
2373
  sourcePages.clear();
2374
+ knownRowCount = 0;
2375
+ endReached = false;
2376
+ confirmedTotal = null;
2362
2377
  bufferVersion.update((current) => current + 1);
2363
2378
  };
2379
+ const recordPageKnowledge = (sourcePage, pageItems, pageSize) => {
2380
+ if (sourcePage < pageBase) {
2381
+ return;
2382
+ }
2383
+ const relPage = sourcePage - pageBase;
2384
+ if (pageItems.length === 0) {
2385
+ // Empty page at or beyond the first known page implies the dataset ends here.
2386
+ // For the initial empty sync (relPage === 0) we stay non-committal so that a
2387
+ // later in-place items() update can still surface real rows.
2388
+ if (relPage > 0) {
2389
+ endReached = true;
2390
+ const total = relPage * pageSize;
2391
+ confirmedTotal = confirmedTotal === null ? total : Math.min(confirmedTotal, total);
2392
+ }
2393
+ return;
2394
+ }
2395
+ if (pageItems.length < pageSize) {
2396
+ endReached = true;
2397
+ const total = relPage * pageSize + pageItems.length;
2398
+ confirmedTotal = confirmedTotal === null ? total : Math.min(confirmedTotal, total);
2399
+ return;
2400
+ }
2401
+ if (!endReached) {
2402
+ knownRowCount = Math.max(knownRowCount, (relPage + 1) * pageSize);
2403
+ }
2404
+ };
2364
2405
  const pushPage = (pages, page, maxPage) => {
2365
2406
  if (page < 0 || page > maxPage || pages.includes(page)) {
2366
2407
  return;
@@ -2400,7 +2441,10 @@ function createGridSourceDataFeature(ctx) {
2400
2441
  // только последнюю из страниц, если несколько запросов завершились до
2401
2442
  // того, как эффект успел выполниться, и все промежуточные страницы теряются.
2402
2443
  if (ctx.getMode() === 'infinity' && !sourcePages.has(nextPage)) {
2403
- sourcePages.set(nextPage, activeSource.items() ?? []);
2444
+ const capturedItems = activeSource.items() ?? [];
2445
+ const pageSize = Math.max(1, activeSource.pageSize || ctx.getFallbackPageSize());
2446
+ sourcePages.set(nextPage, capturedItems);
2447
+ recordPageKnowledge(nextPage, capturedItems, pageSize);
2404
2448
  }
2405
2449
  })
2406
2450
  .finally(() => {
@@ -2427,24 +2471,20 @@ function createGridSourceDataFeature(ctx) {
2427
2471
  }
2428
2472
  return loaded;
2429
2473
  };
2430
- const hasReachedInfinityEnd = (source) => {
2431
- const pageSize = Math.max(1, source.pageSize || ctx.getFallbackPageSize());
2432
- let page = pageBase;
2433
- while (sourcePages.has(page)) {
2434
- const items = sourcePages.get(page) ?? [];
2435
- if (items.length < pageSize) {
2436
- return true;
2437
- }
2438
- page++;
2439
- }
2440
- return false;
2441
- };
2442
2474
  const resolvedTotalElements = (source) => {
2443
2475
  if (typeof source.totalElements !== 'number' || !Number.isFinite(source.totalElements)) {
2444
2476
  return null;
2445
2477
  }
2446
2478
  return Math.max(0, source.totalElements);
2447
2479
  };
2480
+ const estimateOpenEndedPaginationTotal = (source) => {
2481
+ const pageSize = Math.max(1, source.pageSize || ctx.getFallbackPageSize());
2482
+ const page = Math.max(0, source.page || 0);
2483
+ const itemsCount = source.items()?.length ?? 0;
2484
+ const loadedBefore = page * pageSize;
2485
+ const hasUnknownNextPage = itemsCount === pageSize;
2486
+ return loadedBefore + itemsCount + (hasUnknownNextPage ? pageSize : 0);
2487
+ };
2448
2488
  const evictExcessPages = (keepPages) => {
2449
2489
  if (sourcePages.size <= MAX_BUFFERED_PAGES) {
2450
2490
  return;
@@ -2453,6 +2493,12 @@ function createGridSourceDataFeature(ctx) {
2453
2493
  if (sourcePages.size <= MAX_BUFFERED_PAGES) {
2454
2494
  break;
2455
2495
  }
2496
+ // Never evict the anchor page: `localToAbsolutePage` and the contiguous-loaded
2497
+ // walk both start from `pageBase`, so dropping it would cascade into wrong
2498
+ // request-progress numbers even though totalRowCount is now buffer-independent.
2499
+ if (page === pageBase) {
2500
+ continue;
2501
+ }
2456
2502
  if (!keepPages.has(page)) {
2457
2503
  sourcePages.delete(page);
2458
2504
  }
@@ -4075,7 +4121,7 @@ class DataGrid {
4075
4121
  if (this.overlayScrollFeaturePromise) {
4076
4122
  return this.overlayScrollFeaturePromise;
4077
4123
  }
4078
- this.overlayScrollFeaturePromise = import('./reforgium-data-grid-grid-overlay-scroll.feature-6tkNtIuO.mjs').then(({ createGridOverlayScrollFeature }) => {
4124
+ this.overlayScrollFeaturePromise = import('./reforgium-data-grid-grid-overlay-scroll.feature-DQp-sU6c.mjs').then(({ createGridOverlayScrollFeature }) => {
4079
4125
  const feature = createGridOverlayScrollFeature({
4080
4126
  getScrollElement: () => this.scrollEl()?.nativeElement ?? null,
4081
4127
  getThumbTop: () => this.vm.thumbTopPx(),
@@ -4200,4 +4246,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
4200
4246
  */
4201
4247
 
4202
4248
  export { DataGridTypeCellTemplateDirective as D, clampThumbTop as a, DataGridCellTemplateDirective as b, computeScrollbarState as c, DataGridHeaderTemplateDirective as d, DataGridRowDirective as e, DataGridDeclarativeColumn as f, DataGridDeclarativeHeaderDirective as g, DataGridDeclarativeCellDirective as h, DataGridCellEmptyDirective as i, DataGridCellLoadingDirective as j, DataGridStickyRowDirective as k, DataGridSortIconDirective as l, mapThumbTopToScrollTop as m, DataGridExpanderIconDirective as n, DATA_GRID_CONFIG as o, DATA_GRID_HEADER_TEXT_RESOLVER as p, DATA_GRID_TYPE_RENDERERS as q, DATA_GRID_TYPE_TRANSFORMERS as r, DEFAULT_DATA_GRID_DEFAULTS as s, provideDataGridDefaults as t, provideDataGridHeaderTextResolver as u, provideDataGridHeaderTextResolverWithParent as v, provideDataGridTypeRenderers as w, provideDataGridTypeTransformers as x, DataGrid as y };
4203
- //# sourceMappingURL=reforgium-data-grid-reforgium-data-grid-Cb2oAQjG.mjs.map
4249
+ //# sourceMappingURL=reforgium-data-grid-reforgium-data-grid-DUpCJ9P0.mjs.map
@@ -1,2 +1,2 @@
1
- export { o as DATA_GRID_CONFIG, p as DATA_GRID_HEADER_TEXT_RESOLVER, q as DATA_GRID_TYPE_RENDERERS, r as DATA_GRID_TYPE_TRANSFORMERS, s as DEFAULT_DATA_GRID_DEFAULTS, y as DataGrid, i as DataGridCellEmptyDirective, j as DataGridCellLoadingDirective, b as DataGridCellTemplateDirective, h as DataGridDeclarativeCellDirective, f as DataGridDeclarativeColumn, g as DataGridDeclarativeHeaderDirective, n as DataGridExpanderIconDirective, d as DataGridHeaderTemplateDirective, e as DataGridRowDirective, l as DataGridSortIconDirective, k as DataGridStickyRowDirective, D as DataGridTypeCellTemplateDirective, t as provideDataGridDefaults, u as provideDataGridHeaderTextResolver, v as provideDataGridHeaderTextResolverWithParent, w as provideDataGridTypeRenderers, x as provideDataGridTypeTransformers } from './reforgium-data-grid-reforgium-data-grid-Cb2oAQjG.mjs';
1
+ export { o as DATA_GRID_CONFIG, p as DATA_GRID_HEADER_TEXT_RESOLVER, q as DATA_GRID_TYPE_RENDERERS, r as DATA_GRID_TYPE_TRANSFORMERS, s as DEFAULT_DATA_GRID_DEFAULTS, y as DataGrid, i as DataGridCellEmptyDirective, j as DataGridCellLoadingDirective, b as DataGridCellTemplateDirective, h as DataGridDeclarativeCellDirective, f as DataGridDeclarativeColumn, g as DataGridDeclarativeHeaderDirective, n as DataGridExpanderIconDirective, d as DataGridHeaderTemplateDirective, e as DataGridRowDirective, l as DataGridSortIconDirective, k as DataGridStickyRowDirective, D as DataGridTypeCellTemplateDirective, t as provideDataGridDefaults, u as provideDataGridHeaderTextResolver, v as provideDataGridHeaderTextResolverWithParent, w as provideDataGridTypeRenderers, x as provideDataGridTypeTransformers } from './reforgium-data-grid-reforgium-data-grid-DUpCJ9P0.mjs';
2
2
  //# sourceMappingURL=reforgium-data-grid.mjs.map
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.2.0",
2
+ "version": "3.2.3",
3
3
  "name": "@reforgium/data-grid",
4
4
  "description": "reforgium DataGrid component",
5
5
  "author": "rtommievich",
@@ -759,11 +759,23 @@ interface GridPagedDataSource<Data extends AnyDict = AnyDict> {
759
759
  /**
760
760
  * Total number of rows available in the dataset.
761
761
  *
762
- * Optional in infinity mode. When omitted, the grid treats the dataset as open-ended
763
- * until it receives a short page (`items.length < pageSize`) and then fixes the final size.
762
+ * Optional in source-driven pagination/infinity mode.
763
+ *
764
+ * - In `infinity`, the grid treats the dataset as open-ended until it receives
765
+ * a short page (`items.length < pageSize`) and then fixes the final size.
766
+ * - In `pagination`, the grid exposes one optimistic extra page while the current
767
+ * page is full, because without `totalElements` the end can only be detected
768
+ * after the next request returns a short page.
764
769
  */
765
770
  totalElements?: number;
766
- /** Optional dataset revision signal. Increment to force the grid to clear its local page buffer. */
771
+ /**
772
+ * Optional dataset revision signal. Increment to force the grid to clear its local page buffer.
773
+ *
774
+ * Required (when present) to be incremented on any dataset growth, replacement, or filter/sort change:
775
+ * in `infinity` mode without `totalElements` the grid pins end-of-data state on the first short/empty
776
+ * page it sees, and only a version bump clears that pin. Mutating `items()` in place without bumping
777
+ * `version` will leave the grid with a stale row count.
778
+ */
767
779
  version?: Signal<number>;
768
780
  /** Current source-side sort state. */
769
781
  sort?: ReadonlyArray<GridSortItem<Data>>;