neo.mjs 10.4.0 → 10.4.1

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.
@@ -0,0 +1,16 @@
1
+ # Neo.mjs v10.4.1 Release Notes
2
+
3
+ ## Highlights
4
+
5
+ This patch release polishes the "instant preview" feature for grids introduced in v10.4.0, ensuring the grid's scrollbar and accessibility properties are correctly sized from the very first paint when loading large datasets. The non-buffered `table.Body` also receives a major performance boost for clearing data.
6
+
7
+ ## Enhancements & Bug Fixes
8
+
9
+ ### 1. Polished "Instant Preview" for Large Datasets in Grids
10
+ The chunked loading mechanism for stores and grids has been refined. When adding a large dataset, the initial `load` event now carries the final total record count. The `grid.Container` uses this information immediately to set the correct `aria-rowcount` and to size the vertical scrollbar accurately, preventing layout shifts and providing a more stable and accessible user experience from the very first render.
11
+
12
+ ### 2. Standardized Internal Store `load` Event
13
+ To support the "instant preview" refinements, the `data.Store` `load` event payload has been standardized internally. It now consistently uses an object containing an `items` property (e.g., `{items: [...]}`). Framework components have been updated to use this new signature.
14
+
15
+ ### 3. Performance Fast Path for `table.Body`
16
+ The non-buffered `table.Body` is now significantly faster when clearing its data. It uses the same "fast path" optimization as `grid.Body`, directly clearing the DOM instead of processing a large and unnecessary VDOM diff when an empty dataset is loaded. This aligns the performance of the two components.
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.4.0'
23
+ * @member {String} version='10.4.1'
24
24
  */
25
- version: '10.4.0'
25
+ version: '10.4.1'
26
26
  }
27
27
 
28
28
  /**
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.4.0'
111
+ text : 'v10.4.1'
112
112
  }]
113
113
  }],
114
114
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "10.4.0",
3
+ "version": "10.4.1",
4
4
  "description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.4.0'
302
+ * @default '10.4.1'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.4.0'
307
+ version: '10.4.1'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -144,9 +144,13 @@ class Store extends Base {
144
144
 
145
145
  if (items.length > threshold) {
146
146
  const me = this,
147
+ total = me.count + items.length,
147
148
  chunk = items.splice(0, threshold);
148
149
 
149
- // 1. Add the first chunk. This fires 'mutate' -> 'load' and triggers the initial grid render.
150
+ me.chunkingTotal = total;
151
+
152
+ // 1. Add the first chunk. This fires 'mutate' -> 'load' (via onCollectionMutate)
153
+ // and triggers the initial grid render. The 'load' event will contain the final total count.
150
154
  super.add(me.createRecord(chunk));
151
155
 
152
156
  // 2. Suspend events to prevent the next 'add' from firing 'load'.
@@ -158,13 +162,15 @@ class Store extends Base {
158
162
  // 4. Resume events.
159
163
  me.suspendEvents = false;
160
164
 
161
- // 5. Manually fire a final 'load' event to update the grid's scrollbar.
162
- me.fire('load', me.items);
165
+ // 5. Manually fire a final 'load' event to update the grid's scrollbar and notify other listeners.
166
+ me.fire('load', {items: me.items, total: me.chunkingTotal});
167
+
168
+ delete me.chunkingTotal;
163
169
 
164
- return me.count
170
+ return me.count;
165
171
  }
166
172
 
167
- return super.add(this.createRecord(item))
173
+ return super.add(this.createRecord(item));
168
174
  }
169
175
 
170
176
  /**
@@ -447,7 +453,11 @@ class Store extends Base {
447
453
  let me = this;
448
454
 
449
455
  if (me.isConstructed && !me.isLoading) {
450
- me.fire('load', me.items)
456
+ if (me.chunkingTotal) {
457
+ me.fire('load', {items: me.items, total: me.chunkingTotal});
458
+ } else {
459
+ me.fire('load', {items: me.items});
460
+ }
451
461
  }
452
462
  }
453
463
 
@@ -478,7 +488,7 @@ class Store extends Base {
478
488
  // => break the sync flow to ensure potential listeners got applied
479
489
  Promise.resolve().then(() => {
480
490
  if (me.isLoaded) {
481
- me.fire('load', me.items)
491
+ me.fire('load', {items: me.items})
482
492
  } else if (me.autoLoad) {
483
493
  me.load()
484
494
  }
package/src/grid/Body.mjs CHANGED
@@ -932,7 +932,9 @@ class GridBody extends Component {
932
932
  }
933
933
 
934
934
  /**
935
- * @param {Object[]} data
935
+ * @param {Object} data
936
+ * @param {Object[]} data.items
937
+ * @param {Number} [data.total]
936
938
  * @protected
937
939
  */
938
940
  onStoreLoad(data) {
@@ -944,7 +946,7 @@ class GridBody extends Component {
944
946
  * This logic bypasses the standard update() cycle by directly clearing the vdom,
945
947
  * vnode cache and the real DOM via textContent.
946
948
  */
947
- if (data?.length < 1) {
949
+ if (data?.items.length < 1) {
948
950
  const vdomRoot = me.getVdomRoot();
949
951
 
950
952
  // No change, opt out
@@ -565,13 +565,16 @@ class GridContainer extends BaseContainer {
565
565
  }
566
566
 
567
567
  /**
568
- * @param {Object[]} data
568
+ * @param {Object} data
569
+ * @param {Object[]} data.items
570
+ * @param {Number} [data.total]
569
571
  * @protected
570
572
  */
571
573
  onStoreLoad(data) {
572
- let me = this;
574
+ let me = this,
575
+ totalCount = data.total ? data.total : this.store.count;
573
576
 
574
- me.updateRowCount();
577
+ me.updateRowCount(totalCount);
575
578
 
576
579
  if (me.store.sorters?.length < 1) {
577
580
  me.removeSortingCss()
@@ -664,13 +667,15 @@ class GridContainer extends BaseContainer {
664
667
  }
665
668
 
666
669
  /**
667
- * @param {Boolean} silent=false
670
+ * @param {Number} [count] The total number of rows in the store. Optional, will use store.count if not provided.
671
+ * @param {Boolean} [silent=false]
668
672
  */
669
- updateRowCount(silent=false) {
670
- let me = this;
673
+ updateRowCount(count, silent=false) {
674
+ let me = this,
675
+ finalCount = count ? count : me.store.count;
671
676
 
672
- this.getVdomRoot()['aria-rowcount'] = me.store.getCount() + 2; // 1 based & the header row counts as well
673
- !silent && this.update()
677
+ me.getVdomRoot()['aria-rowcount'] = finalCount + 2;
678
+ !silent && me.update()
674
679
  }
675
680
  }
676
681
 
@@ -100,18 +100,19 @@ class VerticalScrollbar extends Component {
100
100
  filter: me.updateScrollHeight,
101
101
  load : me.updateScrollHeight,
102
102
  scope : me
103
- });
104
-
105
- value.getCount() > 0 && me.updateScrollHeight()
103
+ })
106
104
  }
107
105
  }
108
106
 
109
107
  /**
110
- *
108
+ * @param {Object} data
109
+ * @param {Object[]} data.items
110
+ * @param {Number} [data.total]
111
+ * @protected
111
112
  */
112
- updateScrollHeight() {
113
+ updateScrollHeight(data) {
113
114
  let me = this,
114
- countRecords = me.store.getCount(),
115
+ countRecords = data.total ? data.total : me.store.count,
115
116
  {rowHeight} = me;
116
117
 
117
118
  if (countRecords > 0 && rowHeight > 0) {
@@ -558,12 +558,39 @@ class TableBody extends Component {
558
558
  }
559
559
 
560
560
  /**
561
- * @param {Object[]} data
561
+ * @param {Object} data
562
+ * @param {Object[]} data.items
563
+ * @param {Number} [data.total]
562
564
  * @protected
563
565
  */
564
566
  onStoreLoad(data) {
565
567
  let me = this;
566
568
 
569
+ /*
570
+ * Fast path to handle clearing all rows (e.g., store.removeAll()).
571
+ * A full vdom diff against all existing rows is a performance bottleneck.
572
+ * This logic bypasses the standard update() cycle by directly clearing the vdom,
573
+ * vnode cache and the real DOM via textContent.
574
+ */
575
+ if (data?.items.length < 1) {
576
+ const vdomRoot = me.getVdomRoot();
577
+
578
+ // No change, opt out
579
+ if (vdomRoot.cn.length < 1) {
580
+ return
581
+ }
582
+
583
+ vdomRoot.cn = [];
584
+ me.getVnodeRoot().childNodes = [];
585
+
586
+ Neo.applyDeltas(me.appName, {
587
+ id : vdomRoot.id,
588
+ textContent: ''
589
+ });
590
+
591
+ return
592
+ }
593
+
567
594
  me.createViewData();
568
595
 
569
596
  if (me.mounted) {