neo.mjs 10.0.0-beta.6 → 10.0.0

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.
Files changed (51) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
  2. package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
  3. package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
  4. package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
  5. package/ServiceWorker.mjs +2 -2
  6. package/apps/portal/index.html +1 -1
  7. package/apps/portal/view/ViewportController.mjs +6 -4
  8. package/apps/portal/view/examples/List.mjs +28 -19
  9. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  10. package/examples/functional/button/base/MainContainer.mjs +207 -0
  11. package/examples/functional/button/base/app.mjs +6 -0
  12. package/examples/functional/button/base/index.html +11 -0
  13. package/examples/functional/button/base/neo-config.json +6 -0
  14. package/learn/blog/v10-deep-dive-functional-components.md +293 -0
  15. package/learn/blog/v10-deep-dive-reactivity.md +522 -0
  16. package/learn/blog/v10-deep-dive-state-provider.md +432 -0
  17. package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
  18. package/learn/blog/v10-post1-love-story.md +383 -0
  19. package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
  20. package/package.json +3 -3
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/Neo.mjs +47 -45
  23. package/src/component/Abstract.mjs +412 -0
  24. package/src/component/Base.mjs +18 -380
  25. package/src/core/Base.mjs +34 -33
  26. package/src/core/Effect.mjs +30 -34
  27. package/src/core/EffectManager.mjs +101 -14
  28. package/src/core/Observable.mjs +69 -65
  29. package/src/form/field/Text.mjs +11 -5
  30. package/src/functional/button/Base.mjs +384 -0
  31. package/src/functional/component/Base.mjs +51 -145
  32. package/src/layout/Cube.mjs +8 -4
  33. package/src/manager/VDomUpdate.mjs +179 -94
  34. package/src/mixin/VdomLifecycle.mjs +4 -1
  35. package/src/state/Provider.mjs +41 -27
  36. package/src/util/VDom.mjs +11 -4
  37. package/src/util/vdom/TreeBuilder.mjs +38 -62
  38. package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
  39. package/test/siesta/siesta.js +15 -3
  40. package/test/siesta/tests/VdomCalendar.mjs +7 -7
  41. package/test/siesta/tests/VdomHelper.mjs +7 -7
  42. package/test/siesta/tests/classic/Button.mjs +113 -0
  43. package/test/siesta/tests/core/EffectBatching.mjs +46 -41
  44. package/test/siesta/tests/functional/Button.mjs +113 -0
  45. package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
  46. package/test/siesta/tests/vdom/Advanced.mjs +14 -8
  47. package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
  48. package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
  49. package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
  50. package/test/siesta/tests/vdom/table/Container.mjs +9 -5
  51. package/src/core/EffectBatchManager.mjs +0 -67
@@ -1,35 +1,27 @@
1
- import Base from '../core/Base.mjs';
1
+ import Abstract from './Abstract.mjs';
2
2
  import ClassSystemUtil from '../util/ClassSystem.mjs';
3
3
  import ComponentManager from '../manager/Component.mjs';
4
- import DomEvents from '../mixin/DomEvents.mjs';
5
4
  import KeyNavigation from '../util/KeyNavigation.mjs';
6
5
  import Logger from '../util/Logger.mjs';
7
6
  import NeoArray from '../util/Array.mjs';
8
- import Observable from '../core/Observable.mjs';
9
7
  import Rectangle from '../util/Rectangle.mjs';
10
8
  import Style from '../util/Style.mjs';
11
- import VdomLifecycle from '../mixin/VdomLifecycle.mjs';
12
9
  import VDomUtil from '../util/VDom.mjs';
13
10
  import VNodeUtil from '../util/VNode.mjs';
14
11
  import {isDescriptor} from '../core/ConfigSymbols.mjs';
15
12
 
16
13
  const
17
- addUnits = value => value == null ? value : isNaN(value) ? value : `${value}px`,
18
- closestController = Symbol.for('closestController'),
19
- closestProvider = Symbol.for('closestProvider'),
20
- {currentWorker} = Neo,
21
- lengthRE = /^\d+\w+$/,
22
- twoWayBindingSymbol = Symbol.for('twoWayBinding');
14
+ addUnits = value => value == null ? value : isNaN(value) ? value : `${value}px`,
15
+ closestController = Symbol.for('closestController'),
16
+ {currentWorker} = Neo,
17
+ lengthRE = /^\d+\w+$/;
23
18
 
24
19
  /**
25
20
  * Base class for all Components which have a DOM representation
26
21
  * @class Neo.component.Base
27
- * @extends Neo.core.Base
28
- * @mixes Neo.component.mixin.DomEvents
29
- * @mixes Neo.core.Observable
30
- * @mixes Neo.component.mixin.VdomLifecycle
22
+ * @extends Neo.component.Abstract
31
23
  */
32
- class Component extends Base {
24
+ class Component extends Abstract {
33
25
  /**
34
26
  * Valid values for hideMode
35
27
  * @member {String[]} hideModes=['removeDom','visibility']
@@ -63,36 +55,11 @@ class Component extends Base {
63
55
  constrainTo: 'document.body'
64
56
  }
65
57
  },
66
- /**
67
- * The name of the App this component belongs to
68
- * @member {String|null} appName_=null
69
- * @reactive
70
- */
71
- appName_: null,
72
58
  /**
73
59
  * CSS selectors to apply to the root level node of this component
74
60
  * @member {String[]} baseCls=[]
75
61
  */
76
62
  baseCls: [],
77
- /**
78
- * Bind configs to state.Provider data properties.
79
- * Example for a button.Base:
80
- * @example
81
- * bind: {
82
- * iconCls: data => `fa fa-{$data.icon}`,
83
- * text : data => data.foo.bar
84
- * }
85
- * @see https://github.com/neomjs/neo/blob/dev/examples/stateProvider
86
- * @member {Object|null} bind=null
87
- */
88
- bind: null,
89
- /**
90
- * Custom CSS selectors to apply to the root level node of this component
91
- * You can override baseCls to remove default selectors.
92
- * @member {String[]} cls_=null
93
- * @reactive
94
- */
95
- cls_: null,
96
63
  /**
97
64
  * manager.Focus will change this flag on focusin & out dom events
98
65
  * @member {Boolean} containsFocus_=false
@@ -106,20 +73,6 @@ class Component extends Base {
106
73
  * @reactive
107
74
  */
108
75
  controller_: null,
109
- /**
110
- * Convenience shortcut to access the data config of the closest state.Provider.
111
- * Read only.
112
- * @member {Object} data_=null
113
- * @protected
114
- * @reactive
115
- */
116
- data_: null,
117
- /**
118
- * Disabled components will get the neo-disabled cls applied and won't receive DOM events
119
- * @member {Boolean} disabled_=false
120
- * @reactive
121
- */
122
- disabled_: false,
123
76
  /**
124
77
  * Set this config to true to dynamically import a DropZone module & create an instance
125
78
  * @member {Boolean} droppable_=false
@@ -214,31 +167,6 @@ class Component extends Base {
214
167
  * @reactive
215
168
  */
216
169
  minWidth_: null,
217
- /**
218
- * @member {Neo.core.Base[]} mixins=[DomEvents, Observable, VdomLifecycle]
219
- */
220
- mixins: [DomEvents, Observable, VdomLifecycle],
221
- /**
222
- * Override specific stateProvider data properties.
223
- * This will merge the content.
224
- * @member {Object|null} modelData=null
225
- */
226
- modelData: null,
227
- /**
228
- * If the parentId does not match a neo component id, you can manually set this value for finding
229
- * view controllers or state providers.
230
- * Use case: manually dropping components into a vdom structure
231
- * @member {Neo.component.Base|null} parentComponent_=null
232
- * @protected
233
- * @reactive
234
- */
235
- parentComponent_: null,
236
- /**
237
- * The parent component id or document.body
238
- * @member {String} parentId_='document.body'
239
- * @reactive
240
- */
241
- parentId_: 'document.body',
242
170
  /**
243
171
  * Array of Plugin Modules and / or config objects
244
172
  * @member {Array|null} plugins_=null
@@ -276,12 +204,6 @@ class Component extends Base {
276
204
  * @reactive
277
205
  */
278
206
  scrollable_: false,
279
- /**
280
- * Optionally add a state.Provider to share state data with child components
281
- * @member {Object|null} stateProvider_=null
282
- * @reactive
283
- */
284
- stateProvider_: null,
285
207
  /**
286
208
  * Style attributes added to this vdom root. see: getVdomRoot()
287
209
  * @member {Object} style={[isDescriptor]: true, merge: 'shallow', value: null}
@@ -337,12 +259,6 @@ class Component extends Base {
337
259
  * @reactive
338
260
  */
339
261
  width_: null,
340
- /**
341
- * The custom windowIs (timestamp) this component belongs to
342
- * @member {Number|null} windowId_=null
343
- * @reactive
344
- */
345
- windowId_: null,
346
262
  /**
347
263
  * @member {String[]|null} wrapperCls_=null
348
264
  * @reactive
@@ -365,21 +281,6 @@ class Component extends Base {
365
281
  _vdom: {}
366
282
  }
367
283
 
368
- /**
369
- * Internal flag which will get set to true while a component is waiting for its mountedPromise
370
- * @member {Boolean} isAwaitingMount=false
371
- * @protected
372
- */
373
- isAwaitingMount = false
374
-
375
- /**
376
- * Convenience shortcut to access the App this component belongs to
377
- * @returns {Neo.controller.Application|null}
378
- */
379
- get app() {
380
- return Neo.apps[this.appName] || null
381
- }
382
-
383
284
  /**
384
285
  * Returns true if this Component is fully visible, that is it is not hidden and has no hidden ancestors
385
286
  */
@@ -387,54 +288,6 @@ class Component extends Base {
387
288
  return this.mounted && !this.hidden && (!this.parent || this.parent.isVisible);
388
289
  }
389
290
 
390
- /**
391
- * Apply component based listeners
392
- * @member {Object} listeners={}
393
- */
394
- get listeners() {
395
- return this._listeners || {}
396
- }
397
- set listeners(value) {
398
- this._listeners = value
399
- }
400
-
401
- /**
402
- * A Promise that resolves when the component is mounted to the DOM.
403
- * This provides a convenient way to wait for the component to be fully
404
- * available and interactive before executing subsequent logic.
405
- *
406
- * It also handles unmounting by resetting the promise, so it can be safely
407
- * awaited again if the component is remounted.
408
- * @returns {Promise<Neo.component.Base>}
409
- */
410
- get mountedPromise() {
411
- let me = this;
412
-
413
- if (!me._mountedPromise) {
414
- me._mountedPromise = new Promise(resolve => {
415
- if (me.mounted) {
416
- // If already mounted, resolve immediately.
417
- resolve(me)
418
- } else {
419
- // Otherwise, store the resolver to be called by afterSetMounted.
420
- me.mountedPromiseResolve = resolve
421
- }
422
- })
423
- }
424
-
425
- return this._mountedPromise
426
- }
427
-
428
- /**
429
- * Convenience method to access the parent component
430
- * @returns {Neo.component.Base|null}
431
- */
432
- get parent() {
433
- let me = this;
434
-
435
- return me.parentComponent || (me.parentId === 'document.body' ? null : Neo.getComponent(me.parentId))
436
- }
437
-
438
291
  /**
439
292
  * True after the component render() method was called. Also fires the rendered event.
440
293
  * @member {Boolean} rendered=false
@@ -541,30 +394,6 @@ class Component extends Base {
541
394
  me.update()
542
395
  }
543
396
 
544
- /**
545
- * Triggered after any config got changed
546
- * @param {String} key
547
- * @param {*} value
548
- * @param {*} oldValue
549
- * @protected
550
- */
551
- afterSetConfig(key, value, oldValue) {
552
- let me = this;
553
-
554
- if (Neo.isUsingStateProviders && me[twoWayBindingSymbol]) {
555
- // When a component config is updated by its state provider, this flag is set to the config's key.
556
- // This prevents circular updates in two-way data bindings by skipping the push back to the state provider.
557
- if (me._skipTwoWayPush === key) {
558
- return;
559
- }
560
- let binding = me.bind?.[key];
561
-
562
- if (binding?.twoWay) {
563
- this.getStateProvider()?.setData(binding.key, value)
564
- }
565
- }
566
- }
567
-
568
397
  /**
569
398
  * Triggered after the disabled config got changed
570
399
  * @param {Boolean} value
@@ -655,10 +484,7 @@ class Component extends Base {
655
484
  */
656
485
  afterSetId(value, oldValue) {
657
486
  super.afterSetId(value, oldValue);
658
- this.changeVdomRootKey('id', value);
659
-
660
- oldValue && ComponentManager.unregister(oldValue);
661
- value && ComponentManager.register(this)
487
+ this.changeVdomRootKey('id', value)
662
488
  }
663
489
 
664
490
  /**
@@ -746,31 +572,24 @@ class Component extends Base {
746
572
  * @protected
747
573
  */
748
574
  afterSetMounted(value, oldValue) {
575
+ super.afterSetMounted(value, oldValue);
576
+
749
577
  if (oldValue !== undefined) {
750
- let me = this,
751
- {id, windowId} = me;
578
+ let me = this;
752
579
 
753
580
  if (value) { // mount
754
581
  me.hasBeenMounted = true;
755
582
 
756
- me.initDomEvents();
757
-
758
583
  if (me.floating) {
759
584
  me.alignTo();
760
585
 
761
586
  // Focus will be pushed into the first input field or other focusable item
762
- me.focus(id, true)
587
+ me.focus(me.id, true)
763
588
  }
764
589
 
765
- me.mountedPromiseResolve?.(this);
766
- delete me.mountedPromiseResolve;
767
-
768
590
  me.fire('mounted', me.id);
769
591
  } else { // unmount
770
- me.revertFocus();
771
-
772
- // The promise needs to get reset, in case the component gets remounted.
773
- delete me._mountedPromise;
592
+ me.revertFocus()
774
593
  }
775
594
  }
776
595
  }
@@ -856,16 +675,6 @@ class Component extends Base {
856
675
  }
857
676
  }
858
677
 
859
- /**
860
- * Triggered after the stateProvider config got changed
861
- * @param {Neo.state.Provider} value
862
- * @param {Object|Neo.state.Provider|null} oldValue
863
- * @protected
864
- */
865
- afterSetStateProvider(value, oldValue) {
866
- value?.createBindings(this)
867
- }
868
-
869
678
  /**
870
679
  * Triggered after the style config got changed
871
680
  * @param {Object} value
@@ -987,21 +796,12 @@ class Component extends Base {
987
796
  * @protected
988
797
  */
989
798
  afterSetWindowId(value, oldValue) {
990
- let me = this,
991
- controller = me.controller;
992
-
993
- if (value) {
994
- currentWorker.insertThemeFiles(value, me.__proto__);
799
+ super.afterSetWindowId(value, oldValue);
995
800
 
996
- if (controller) {
997
- controller.windowId = value
998
- }
999
- }
801
+ let controller = this.controller;
1000
802
 
1001
- // If a component gets moved into a different window, an update cycle might still be running.
1002
- // Since the update might no longer get mapped, we want to re-enable this instance for future updates.
1003
- if (oldValue) {
1004
- me.isVdomUpdating = false
803
+ if (controller) {
804
+ controller.windowId = value
1005
805
  }
1006
806
  }
1007
807
 
@@ -1085,16 +885,6 @@ class Component extends Base {
1085
885
  return value ? [...value] : []
1086
886
  }
1087
887
 
1088
- /**
1089
- * Triggered when accessing the data config
1090
- * Convenience shortcut which is expensive to use, since it will generate a merged parent state providers data map.
1091
- * @param {Object} value
1092
- * @protected
1093
- */
1094
- beforeGetData(value) {
1095
- return this.getStateProvider().getHierarchyData()
1096
- }
1097
-
1098
888
  /**
1099
889
  * Triggered when accessing the style config
1100
890
  * @param {Object} value
@@ -1255,31 +1045,6 @@ class Component extends Base {
1255
1045
  return (Neo.isNumber(oldValue) && oldValue > 0) ? (oldValue - 1) : 0
1256
1046
  }
1257
1047
 
1258
- /**
1259
- * Triggered before the stateProvider config gets changed.
1260
- * Creates a state.Provider instance if needed.
1261
- * @param {Object} value
1262
- * @param {Object} oldValue
1263
- * @returns {Neo.state.Provider}
1264
- * @protected
1265
- */
1266
- beforeSetStateProvider(value, oldValue) {
1267
- oldValue?.destroy();
1268
-
1269
- if (value) {
1270
- let me = this,
1271
- defaultValues = {component: me};
1272
-
1273
- if (me.modelData) {
1274
- defaultValues.data = me.modelData
1275
- }
1276
-
1277
- return ClassSystemUtil.beforeSetInstance(value, 'Neo.state.Provider', defaultValues)
1278
- }
1279
-
1280
- return null
1281
- }
1282
-
1283
1048
  /**
1284
1049
  * Triggered before the updateDepth config gets changed.
1285
1050
  * @param {Number} value
@@ -1377,14 +1142,10 @@ class Component extends Base {
1377
1142
 
1378
1143
  me.revertFocus();
1379
1144
 
1380
- me.removeDomEvents();
1381
-
1382
1145
  me.controller = null; // triggers destroy()
1383
1146
 
1384
1147
  me.reference && me.getController()?.removeReference(me); // remove own reference from parent controllers
1385
1148
 
1386
- me.stateProvider = null; // triggers destroy()
1387
-
1388
1149
  me.plugins?.forEach(plugin => {
1389
1150
  plugin.destroy()
1390
1151
  });
@@ -1400,8 +1161,6 @@ class Component extends Base {
1400
1161
  }
1401
1162
  }
1402
1163
 
1403
- ComponentManager.unregister(me);
1404
-
1405
1164
  super.destroy();
1406
1165
 
1407
1166
  // We do want to prevent delayed calls after a component instance got destroyed.
@@ -1442,35 +1201,6 @@ class Component extends Base {
1442
1201
  return result
1443
1202
  }
1444
1203
 
1445
- /**
1446
- * Find an instance stored inside a config via optionally passing a ntype.
1447
- * Returns this[configName] or the closest parent component with a match.
1448
- * Used by getController() & getStateProvider()
1449
- * @param {String} configName
1450
- * @param {String} [ntype]
1451
- * @returns {Neo.core.Base|null}
1452
- */
1453
- getConfigInstanceByNtype(configName, ntype) {
1454
- let me = this,
1455
- config = me[configName],
1456
- {parentComponent} = me;
1457
-
1458
- if (config && (!ntype || ntype === config.ntype)) {
1459
- return config
1460
- }
1461
-
1462
- if (!parentComponent && me.parentId) {
1463
- parentComponent = me.parent || Neo.get(me.parentId);
1464
- }
1465
-
1466
- if (parentComponent) {
1467
- // todo: We need ?. until functional.component.Base supports controllers
1468
- return parentComponent.getConfigInstanceByNtype?.(configName, ntype)
1469
- }
1470
-
1471
- return null
1472
- }
1473
-
1474
1204
  /**
1475
1205
  * Returns this.controller or the closest parent controller
1476
1206
  * @param {String} [ntype]
@@ -1564,45 +1294,6 @@ class Component extends Base {
1564
1294
  return this.down({reference: value})
1565
1295
  }
1566
1296
 
1567
- /**
1568
- * Convenience shortcut
1569
- * @param args
1570
- * @returns {*}
1571
- */
1572
- getState(...args) {
1573
- return this.getStateProvider().getData(...args)
1574
- }
1575
-
1576
- /**
1577
- * Returns this.stateProvider or the closest parent stateProvider
1578
- * @param {String} [ntype]
1579
- * @returns {Neo.state.Provider|null}
1580
- */
1581
- getStateProvider(ntype) {
1582
- if (!Neo.isUsingStateProviders) {
1583
- return null
1584
- }
1585
-
1586
- let me = this,
1587
- provider;
1588
-
1589
- if (!ntype) {
1590
- provider = me[closestProvider];
1591
-
1592
- if (provider) {
1593
- return provider
1594
- }
1595
- }
1596
-
1597
- provider = me.getConfigInstanceByNtype('stateProvider', ntype);
1598
-
1599
- if (!ntype) {
1600
- me[closestProvider] = provider
1601
- }
1602
-
1603
- return provider
1604
- }
1605
-
1606
1297
  /**
1607
1298
  * Walks up the vdom tree and returns the closest theme found
1608
1299
  * @returns {String}
@@ -1677,14 +1368,6 @@ class Component extends Base {
1677
1368
  this.autoRender && this.render()
1678
1369
  }
1679
1370
 
1680
- /**
1681
- * @param args
1682
- */
1683
- initConfig(...args) {
1684
- super.initConfig(...args);
1685
- this.getStateProvider()?.createBindings(this)
1686
- }
1687
-
1688
1371
  /**
1689
1372
  * Check if this component or any of its parents is floating
1690
1373
  * @returns {Boolean}
@@ -1794,12 +1477,11 @@ class Component extends Base {
1794
1477
  *
1795
1478
  */
1796
1479
  onConstructed() {
1797
- super.onConstructed()
1480
+ super.onConstructed();
1798
1481
 
1799
1482
  let me = this;
1800
1483
 
1801
1484
  me.keys?.register(me);
1802
- me.getStateProvider()?.createBindings(me)
1803
1485
  }
1804
1486
 
1805
1487
  /**
@@ -1892,50 +1574,6 @@ class Component extends Base {
1892
1574
  }
1893
1575
  }
1894
1576
 
1895
- /**
1896
- * Change multiple configs at once, ensuring that all afterSet methods get all new assigned values
1897
- * @param {Object} values={}
1898
- * @param {Boolean} silent=false
1899
- * @returns {Promise<*>}
1900
- */
1901
- set(values={}, silent=false) {
1902
- const
1903
- me = this,
1904
- wasHidden = me.hidden;
1905
-
1906
- me.setSilent(values);
1907
-
1908
- if (!silent && me.needsVdomUpdate) {
1909
- if (wasHidden && !me.hidden) {
1910
- me.show();
1911
- return Promise.resolve()
1912
- }
1913
-
1914
- return me.promiseUpdate()
1915
- }
1916
-
1917
- return Promise.resolve()
1918
- }
1919
-
1920
- /**
1921
- * A silent version of set(), which does not trigger a vdom update at the end.
1922
- * Useful for batching multiple config changes.
1923
- * @param {Object} values={}
1924
- */
1925
- setSilent(values={}) {
1926
- this.silentVdomUpdate = true;
1927
- super.set(values);
1928
- this.silentVdomUpdate = false
1929
- }
1930
-
1931
- /**
1932
- * Convenience shortcut
1933
- * @param args
1934
- */
1935
- setState(...args) {
1936
- this.getStateProvider().setData(...args)
1937
- }
1938
-
1939
1577
  /**
1940
1578
  * Show the component.
1941
1579
  * hideMode: 'removeDom' uses vdom removeDom.
package/src/core/Base.mjs CHANGED
@@ -4,7 +4,8 @@ import Util from '../core/Ut
4
4
  import Config from './Config.mjs';
5
5
  import {isDescriptor} from './ConfigSymbols.mjs';
6
6
  import IdGenerator from './IdGenerator.mjs';
7
- import EffectBatchManager from './EffectBatchManager.mjs';
7
+ import EffectManager from './EffectManager.mjs';
8
+
8
9
 
9
10
  const configSymbol = Symbol.for('configSymbol'),
10
11
  forceAssignConfigs = Symbol('forceAssignConfigs'),
@@ -233,8 +234,6 @@ class Base {
233
234
  me.id = config.id || IdGenerator.getId(this.getIdKey());
234
235
  delete config.id;
235
236
 
236
- me.getStaticConfig('observable') && me.initObservable(config);
237
-
238
237
  // assign class field values prior to configs
239
238
  config = me.setFields(config);
240
239
 
@@ -325,9 +324,9 @@ class Base {
325
324
  *
326
325
  * @example
327
326
  * // Imagine you have hundreds of buttons in your app, and you want all of them
328
- * // to have `labelPosition: 'top'` instead of the default `'left'`.
327
+ * // to have `labelPosition: 'top'` instead of the default `'left'`.
329
328
  * // Instead of configuring each instance, you can define an overwrite.
330
- *
329
+ *
331
330
  * // inside an Overwrites.mjs file loaded by your app:
332
331
  * Neo.overwrites = {
333
332
  * Neo: {
@@ -338,7 +337,7 @@ class Base {
338
337
  * }
339
338
  * }
340
339
  * };
341
- *
340
+ *
342
341
  * // Now, every `Neo.button.Base` (and any class that extends it) will have this
343
342
  * // new default value on its prototype.
344
343
  *
@@ -826,40 +825,42 @@ class Base {
826
825
  let me = this,
827
826
  classFieldsViaSet = {};
828
827
 
829
- // Start batching for effects
830
- EffectBatchManager.startBatch();
831
-
832
- values = me.setFields(values);
828
+ // Prevent Effects from running for bulk changes
829
+ EffectManager.pause();
833
830
 
834
- // If the initial config processing is still running,
835
- // finish this one first before dropping new values into the configSymbol.
836
- // See: https://github.com/neomjs/neo/issues/2201
837
- if (me[forceAssignConfigs] !== true && Object.keys(me[configSymbol]).length > 0) {
838
- me.processConfigs()
839
- }
831
+ try {
832
+ values = me.setFields(values);
840
833
 
841
- // Store class fields which are defined via get() & set() and ensure they won't get added to the config symbol.
842
- Object.entries(values).forEach(([key, value]) => {
843
- if (!me.isConfig(key)) {
844
- classFieldsViaSet[key] = value;
845
- delete values[key]
834
+ // If the initial config processing is still running,
835
+ // finish this one first before dropping new values into the configSymbol.
836
+ // See: https://github.com/neomjs/neo/issues/2201
837
+ if (me[forceAssignConfigs] !== true && Object.keys(me[configSymbol]).length > 0) {
838
+ me.processConfigs()
846
839
  }
847
- })
848
840
 
849
- // Add reactive configs to the configSymbol
850
- Object.assign(me[configSymbol], values);
841
+ // Store class fields which are defined via get() & set() and ensure they won't get added to the config symbol.
842
+ Object.entries(values).forEach(([key, value]) => {
843
+ if (!me.isConfig(key)) {
844
+ classFieldsViaSet[key] = value;
845
+ delete values[key]
846
+ }
847
+ })
851
848
 
852
- // Process class fields which are defined via get() & set() => now they can access the latest values
853
- // for reactive and non-reactive configs, as well as class fields defined with values.
854
- Object.entries(classFieldsViaSet).forEach(([key, value]) => {
855
- me[key] = value
856
- })
849
+ // Add reactive configs to the configSymbol
850
+ Object.assign(me[configSymbol], values);
857
851
 
858
- // Process reactive configs
859
- me.processConfigs(true);
852
+ // Process class fields which are defined via get() & set() => now they can access the latest values
853
+ // for reactive and non-reactive configs, as well as class fields defined with values.
854
+ Object.entries(classFieldsViaSet).forEach(([key, value]) => {
855
+ me[key] = value
856
+ })
860
857
 
861
- // End batching for effects
862
- EffectBatchManager.endBatch();
858
+ // Process reactive configs
859
+ me.processConfigs(true);
860
+ } finally {
861
+ // Trigger the skipped Effect, if needed
862
+ EffectManager.resume()
863
+ }
863
864
  }
864
865
 
865
866
  /**