neo.mjs 10.0.0-beta.4 → 10.0.0-beta.5

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 (46) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  5. package/examples/button/effect/MainContainer.mjs +207 -0
  6. package/examples/button/effect/app.mjs +6 -0
  7. package/examples/button/effect/index.html +11 -0
  8. package/examples/button/effect/neo-config.json +6 -0
  9. package/learn/guides/datahandling/StateProviders.md +1 -0
  10. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  11. package/learn/tree.json +1 -0
  12. package/package.json +2 -2
  13. package/src/DefaultConfig.mjs +2 -2
  14. package/src/Neo.mjs +226 -78
  15. package/src/button/Effect.mjs +435 -0
  16. package/src/collection/Base.mjs +7 -2
  17. package/src/component/Base.mjs +67 -46
  18. package/src/container/Base.mjs +28 -24
  19. package/src/core/Base.mjs +138 -19
  20. package/src/core/Config.mjs +123 -32
  21. package/src/core/Effect.mjs +127 -0
  22. package/src/core/EffectBatchManager.mjs +68 -0
  23. package/src/core/EffectManager.mjs +38 -0
  24. package/src/grid/Container.mjs +8 -4
  25. package/src/grid/column/Component.mjs +1 -1
  26. package/src/state/Provider.mjs +343 -452
  27. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  28. package/src/tab/header/EffectButton.mjs +75 -0
  29. package/src/vdom/Helper.mjs +9 -10
  30. package/src/vdom/VNode.mjs +1 -1
  31. package/src/worker/App.mjs +0 -5
  32. package/test/siesta/siesta.js +32 -0
  33. package/test/siesta/tests/CollectionBase.mjs +10 -10
  34. package/test/siesta/tests/VdomHelper.mjs +22 -59
  35. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  36. package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
  37. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  38. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  39. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  40. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  41. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  42. package/test/siesta/tests/core/Effect.mjs +131 -0
  43. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  44. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  45. package/test/siesta/tests/state/Provider.mjs +537 -0
  46. package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
@@ -0,0 +1,435 @@
1
+ import Component from '../component/Base.mjs';
2
+ import Effect from '../core/Effect.mjs';
3
+ import NeoArray from '../util/Array.mjs';
4
+
5
+ /**
6
+ * @class Neo.button.Effect
7
+ * @extends Neo.component.Base
8
+ */
9
+ class EffectButton extends Component {
10
+ /**
11
+ * Valid values for badgePosition
12
+ * @member {String[]} badgePositions=['bottom-left','bottom-right','top-left','top-right']
13
+ * @protected
14
+ * @static
15
+ */
16
+ static badgePositions = ['bottom-left', 'bottom-right', 'top-left', 'top-right']
17
+ /**
18
+ * Valid values for iconPosition
19
+ * @member {String[]} iconPositions=['top','right','bottom','left']
20
+ * @protected
21
+ * @static
22
+ */
23
+ static iconPositions = ['top', 'right', 'bottom', 'left']
24
+
25
+ static config = {
26
+ /**
27
+ * @member {String} className='Neo.button.Effect'
28
+ * @protected
29
+ */
30
+ className: 'Neo.button.Effect',
31
+ /**
32
+ * @member {String} ntype='effect-button'
33
+ * @protected
34
+ */
35
+ ntype: 'effect-button',
36
+ /**
37
+ * @member {String} badgePosition_='top-right'
38
+ */
39
+ badgePosition_: 'top-right',
40
+ /**
41
+ * @member {String|null} badgeText_=null
42
+ */
43
+ badgeText_: null,
44
+ /**
45
+ * @member {String[]} baseCls=['neo-button']
46
+ */
47
+ baseCls: ['neo-button'],
48
+ /**
49
+ * @member {String[]} cls=[]
50
+ */
51
+ cls: [],
52
+ /**
53
+ * false calls Neo.Main.setRoute()
54
+ * @member {Boolean} editRoute=true
55
+ */
56
+ editRoute: true,
57
+ /**
58
+ * Shortcut for domListeners={click:handler}
59
+ * A string based value assumes that the handlerFn lives inside a controller.Component
60
+ * @member {Function|String|null} handler_=null
61
+ */
62
+ handler_: null,
63
+ /**
64
+ * The scope (this pointer) inside the handler function.
65
+ * Points to the button instance by default.
66
+ * You can use 'this' as a string for convenience reasons
67
+ * @member {Object|String|null} handlerScope=null
68
+ */
69
+ handlerScope: null,
70
+ /**
71
+ * The CSS class to use for an icon, e.g. 'fa fa-home'
72
+ * @member {String|null} [iconCls_=null]
73
+ */
74
+ iconCls_: null,
75
+ /**
76
+ * The color to use for an icon, e.g. '#ff0000' [optional]
77
+ * @member {String|null} iconColor_=null
78
+ */
79
+ iconColor_: null,
80
+ /**
81
+ * The position of the icon in case iconCls has a value.
82
+ * Valid values are: 'top', 'right', 'bottom', 'left'
83
+ * @member {String} iconPosition_='left'
84
+ */
85
+ iconPosition_: 'left',
86
+ /**
87
+ * An array representing the configuration of the menu items.
88
+ *
89
+ * Or a configuration object which adds custom configuration to the menu to be
90
+ * created and includes an `items` property to define the menu items.
91
+ * @member {Object|Object[]|null} menu_=null
92
+ */
93
+ menu_: null,
94
+ /**
95
+ * The pressed state of the Button
96
+ * @member {Boolean} pressed_=false
97
+ */
98
+ pressed_: false,
99
+ /**
100
+ * Change the browser hash value on click.
101
+ * Use route for internal navigation and url for external links. Do not use both on the same instance.
102
+ * Transforms the button tag into an a tag [optional]
103
+ * @member {String|null} route_=null
104
+ */
105
+ route_: null,
106
+ /**
107
+ * @member {String} tag='button'
108
+ */
109
+ tag: 'button',
110
+ /**
111
+ * Transforms the button tag into an a tag [optional]
112
+ * @member {String|null} url_=null
113
+ */
114
+ url_: null,
115
+ /**
116
+ * If url is set, applies the target attribute on the top level vdom node [optional]
117
+ * @member {String} urlTarget_='_blank'
118
+ */
119
+ urlTarget_: '_blank',
120
+ /**
121
+ * True adds an expanding circle on click
122
+ * @member {Boolean} useRippleEffect_=true
123
+ */
124
+ useRippleEffect_: true
125
+ }
126
+
127
+ /**
128
+ * Time in ms for the ripple effect when clicking on the button.
129
+ * Only active if useRippleEffect is set to true.
130
+ * @member {Number} rippleEffectDuration=400
131
+ */
132
+ rippleEffectDuration = 400
133
+ /**
134
+ * Internal flag to store the last setTimeout() id for ripple effect remove node callbacks
135
+ * @member {Number} #rippleTimeoutId=null
136
+ * @private
137
+ */
138
+ #rippleTimeoutId = null
139
+
140
+ /**
141
+ * @param {Object} config
142
+ */
143
+ construct(config) {
144
+ super.construct(config);
145
+
146
+ let me = this;
147
+
148
+ me.addDomListeners({
149
+ click: me.onClick,
150
+ scope: me
151
+ });
152
+
153
+ me.createVdomEffect()
154
+ }
155
+
156
+ /**
157
+ * Final. Should not be overridden.
158
+ * This is the core reactive effect.
159
+ * @returns {Neo.core.Effect}
160
+ * @protected
161
+ */
162
+ createVdomEffect() {
163
+ return new Effect({fn: () => {
164
+ // The effect's only job is to get the config and trigger an update.
165
+ this._vdom = this.getVdomConfig();
166
+ this.update();
167
+ }});
168
+ }
169
+
170
+ /**
171
+ * Builds the array of child nodes (the 'cn' property).
172
+ * Subclasses can override this to add or remove children.
173
+ * @returns {Object[]}
174
+ * @protected
175
+ */
176
+ getVdomChildren() {
177
+ return [
178
+ // iconNode
179
+ {tag: 'span', cls: ['neo-button-glyph', ...this._iconCls || []], removeDom: !this.iconCls, style: {color: this.iconColor || null}},
180
+ // textNode
181
+ {tag: 'span', cls: ['neo-button-text'], id: `${this.id}__text`, removeDom: !this.text, text: this.text},
182
+ // badgeNode
183
+ {cls: ['neo-button-badge', 'neo-' + this.badgePosition], removeDom: !this.badgeText, text: this.badgeText},
184
+ // rippleWrapper
185
+ {cls: ['neo-button-ripple-wrapper'], removeDom: !this.useRippleEffect, cn: [{cls: ['neo-button-ripple']}]}
186
+ ];
187
+ }
188
+
189
+ /**
190
+ * Builds the array of CSS classes for the root element.
191
+ * @returns {String[]}
192
+ * @protected
193
+ */
194
+ getVdomCls() {
195
+ let vdomCls = [...this.baseCls, ...this.cls];
196
+
197
+ NeoArray.toggle(vdomCls, 'no-text', !this.text);
198
+ NeoArray.toggle(vdomCls, 'pressed', this.pressed);
199
+ vdomCls.push('icon-' + this.iconPosition);
200
+
201
+ return vdomCls;
202
+ }
203
+
204
+ /**
205
+ * Builds the top-level VDOM object.
206
+ * Subclasses can override this to add or modify root properties.
207
+ * @returns {Object}
208
+ * @protected
209
+ */
210
+ getVdomConfig() {
211
+ let me = this,
212
+ link = !me.editRoute && me.route || me.url,
213
+ tag = link ? 'a' : 'button';
214
+
215
+ return {
216
+ tag,
217
+ cls : this.getVdomCls(),
218
+ href : link ? (link.startsWith('#') ? link : '#' + link) : null,
219
+ id : me.id,
220
+ style : me.style,
221
+ target : me.url ? me.urlTarget : null,
222
+ type : tag === 'button' ? 'button' : null,
223
+ cn : this.getVdomChildren()
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Triggered after the menu config got changed
229
+ * @param {Object|Object[]|null} value
230
+ * @param {Object|Object[]|null} oldValue
231
+ * @protected
232
+ */
233
+ afterSetMenu(value, oldValue) {
234
+ if (value) {
235
+ import('../menu/List.mjs').then(module => {
236
+ let me = this,
237
+ isArray = Array.isArray(value),
238
+ items = isArray ? value : value.items,
239
+ menuConfig = isArray ? {} : value,
240
+ stateProvider = me.getStateProvider(),
241
+ {appName, theme, windowId} = me,
242
+
243
+ config = Neo.merge({
244
+ module : module.default,
245
+ align : {edgeAlign: 't0-b0', target: me.id},
246
+ appName,
247
+ displayField : 'text',
248
+ floating : true,
249
+ hidden : true,
250
+ parentComponent: me,
251
+ theme,
252
+ windowId
253
+ }, menuConfig);
254
+
255
+ if (items) {
256
+ config.items = items
257
+ }
258
+
259
+ if (stateProvider) {
260
+ config.stateProvider = {parent: stateProvider}
261
+ }
262
+
263
+ me.menuList = Neo.create(config)
264
+ })
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Triggered after the theme config got changed
270
+ * @param {String|null} value
271
+ * @param {String|null} oldValue
272
+ * @protected
273
+ */
274
+ afterSetTheme(value, oldValue) {
275
+ super.afterSetTheme(value, oldValue);
276
+
277
+ let {menuList} = this;
278
+
279
+ if (menuList) {
280
+ menuList.theme = value
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Triggered after the windowId config got changed
286
+ * @param {Number|null} value
287
+ * @param {Number|null} oldValue
288
+ * @protected
289
+ */
290
+ afterSetWindowId(value, oldValue) {
291
+ super.afterSetWindowId(value, oldValue);
292
+
293
+ let {menuList} = this;
294
+
295
+ if (menuList) {
296
+ menuList.windowId = value
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Converts the iconCls array into a string on beforeGet
302
+ * @returns {String}
303
+ * @protected
304
+ */
305
+ beforeGetIconCls() {
306
+ let iconCls = this._iconCls;
307
+
308
+ if (Array.isArray(iconCls)) {
309
+ return iconCls.join(' ')
310
+ }
311
+
312
+ return iconCls
313
+ }
314
+
315
+ /**
316
+ * Triggered before the badgePosition config gets changed
317
+ * @param {String} value
318
+ * @param {String} oldValue
319
+ * @returns {String}
320
+ * @protected
321
+ */
322
+ beforeSetBadgePosition(value, oldValue) {
323
+ return this.beforeSetEnumValue(value, oldValue, 'badgePosition')
324
+ }
325
+
326
+ /**
327
+ * Triggered before the iconCls config gets changed. Converts the string into an array if needed.
328
+ * @param {Array|String|null} value
329
+ * @param {Array|String|null} oldValue
330
+ * @returns {Array}
331
+ * @protected
332
+ */
333
+ beforeSetIconCls(value, oldValue) {
334
+ if (value && !Array.isArray(value)) {
335
+ value = value.split(' ').filter(Boolean)
336
+ }
337
+
338
+ return value
339
+ }
340
+
341
+ /**
342
+ * Triggered before the iconPosition config gets changed
343
+ * @param {String} value
344
+ * @param {String} oldValue
345
+ * @protected
346
+ */
347
+ beforeSetIconPosition(value, oldValue) {
348
+ return this.beforeSetEnumValue(value, oldValue, 'iconPosition')
349
+ }
350
+
351
+ /**
352
+ * @protected
353
+ */
354
+ changeRoute() {
355
+ this.editRoute && Neo.Main.editRoute(this.route)
356
+ }
357
+
358
+ /**
359
+ * @param args
360
+ */
361
+ destroy(...args) {
362
+ this.menuList?.destroy(true, false);
363
+ super.destroy(...args)
364
+ }
365
+
366
+ /**
367
+ * @param {Object} data
368
+ */
369
+ onClick(data) {
370
+ let me = this;
371
+
372
+ me.bindCallback(me.handler, 'handler', me.handlerScope || me);
373
+ me.handler?.(data);
374
+
375
+ me.menu && me.toggleMenu();
376
+ me.route && me.changeRoute(); // only relevant for editRoute=true
377
+ me.useRippleEffect && me.showRipple(data)
378
+ }
379
+
380
+ /**
381
+ * @param {Object} data
382
+ */
383
+ async showRipple(data) {
384
+ let me = this,
385
+ buttonRect = data.path[0].rect,
386
+ diameter = Math.max(buttonRect.height, buttonRect.width),
387
+ radius = diameter / 2,
388
+ rippleEffectDuration = me.rippleEffectDuration,
389
+ rippleWrapper = me.getVdomRoot().cn[3],
390
+ rippleEl = rippleWrapper.cn[0],
391
+ rippleTimeoutId;
392
+
393
+ rippleEl.style = Object.assign(rippleEl.style || {}, {
394
+ animation: 'none',
395
+ height : `${diameter}px`,
396
+ left : `${data.clientX - buttonRect.left - radius}px`,
397
+ top : `${data.clientY - buttonRect.top - radius}px`,
398
+ width : `${diameter}px`
399
+ });
400
+
401
+ delete rippleWrapper.removeDom;
402
+ me.update();
403
+
404
+ await me.timeout(1);
405
+
406
+ rippleEl.style.animation = `ripple ${rippleEffectDuration}ms linear`;
407
+ me.update();
408
+
409
+ me.#rippleTimeoutId = rippleTimeoutId = setTimeout(() => {
410
+ // we do not want to break animations when clicking multiple times
411
+ if (me.#rippleTimeoutId === rippleTimeoutId) {
412
+ me.#rippleTimeoutId = null;
413
+
414
+ rippleWrapper.removeDom = true;
415
+ me.update()
416
+ }
417
+ }, rippleEffectDuration)
418
+ }
419
+
420
+ /**
421
+ *
422
+ */
423
+ async toggleMenu() {
424
+ let {menuList} = this,
425
+ hidden = !menuList.hidden;
426
+
427
+ menuList.hidden = hidden;
428
+
429
+ if (!hidden) {
430
+ await this.timeout(50)
431
+ }
432
+ }
433
+ }
434
+
435
+ export default Neo.setupClass(EffectButton);
@@ -454,13 +454,17 @@ class Collection extends Base {
454
454
  filters = me._filters || [],
455
455
  sorters = me._sorters || [];
456
456
 
457
+ // Ensure the keyProperty does not get lost.
458
+ config.keyProperty = me.keyProperty;
459
+
457
460
  delete config.id;
458
461
  delete config.filters;
459
462
  delete config.items;
460
463
  delete config.sorters;
461
464
 
462
465
  if (me._items.length > 0) {
463
- config.items = [...me._items]
466
+ config.items = [...me._items];
467
+ config.count = config.items.length;
464
468
  }
465
469
 
466
470
  config.filters = [];
@@ -694,7 +698,8 @@ class Collection extends Base {
694
698
 
695
699
  me.allItems = Neo.create(Collection, {
696
700
  ...Neo.clone(config, true, true),
697
- id : me.id + '-all',
701
+ id : me.id + '-all',
702
+ items : [...me._items], // Initialize with a shallow copy of current items
698
703
  keyProperty: me.keyProperty,
699
704
  sourceId : me.id
700
705
  })
@@ -54,11 +54,15 @@ class Component extends Base {
54
54
  /**
55
55
  * The default alignment specification to position this Component relative to some other
56
56
  * Component, or Element or Rectangle. Only applies in case floating = true.
57
- * @member {Object|String} align_={edgeAlign:'t-b',constrainTo:'document.body'}
57
+ * @member {Object|String} align_={[isDescriptor]: true, merge: 'deep', value: {edgeAlign: 't-b',constrainTo: 'document.body'}}
58
58
  */
59
59
  align_: {
60
- edgeAlign : 't-b',
61
- constrainTo: 'document.body'
60
+ [isDescriptor]: true,
61
+ merge : 'deep',
62
+ value: {
63
+ edgeAlign : 't-b',
64
+ constrainTo: 'document.body'
65
+ }
62
66
  },
63
67
  /**
64
68
  * The name of the App this component belongs to
@@ -327,9 +331,13 @@ class Component extends Base {
327
331
  stateProvider_: null,
328
332
  /**
329
333
  * Style attributes added to this vdom root. see: getVdomRoot()
330
- * @member {Object} style_=null
334
+ * @member {Object} style={[isDescriptor]: true, merge: 'shallow', value: null}
331
335
  */
332
- style_: null,
336
+ style_: {
337
+ [isDescriptor]: true,
338
+ merge : 'shallow',
339
+ value : null
340
+ },
333
341
  /**
334
342
  * You can pass a used theme directly to any component,
335
343
  * to style specific component trees differently from your main view.
@@ -376,13 +384,15 @@ class Component extends Base {
376
384
  updateDepth_: 1,
377
385
  /**
378
386
  * The component vnode tree. Available after the component got rendered.
379
- * @member {Object} vnode_=null
387
+ * @member {Object} vnode_=={[isDescriptor]: true, value: null, isEqual: (a, b) => a === b,}
380
388
  * @protected
381
389
  */
382
390
  vnode_: {
383
391
  [isDescriptor]: true,
392
+ clone : 'none',
393
+ cloneOnGet : 'none',
394
+ isEqual : (a, b) => a === b, // vnode trees can be huge, and will get compared by the vdom worker.
384
395
  value : null,
385
- isEqual : (a, b) => a === b // vnode trees can be huge, and will get compared by the vdom worker.
386
396
  },
387
397
  /**
388
398
  * Shortcut for style.width, defaults to px
@@ -400,9 +410,13 @@ class Component extends Base {
400
410
  wrapperCls_: null,
401
411
  /**
402
412
  * Top level style attributes. Useful in case getVdomRoot() does not point to the top level DOM node.
403
- * @member {Object|null} wrapperStyle_=null
413
+ * @member {Object|null} wrapperStyle_={[isDescriptor]: true, merge: 'shallow', value: null}
404
414
  */
405
- wrapperStyle_: null,
415
+ wrapperStyle_: {
416
+ [isDescriptor]: true,
417
+ merge : 'shallow',
418
+ value : null
419
+ },
406
420
  /**
407
421
  * The vdom markup for this component.
408
422
  * @member {Object} _vdom={}
@@ -593,7 +607,12 @@ class Component extends Base {
593
607
  afterSetConfig(key, value, oldValue) {
594
608
  let me = this;
595
609
 
596
- if (currentWorker.isUsingStateProviders && me[twoWayBindingSymbol] && oldValue !== undefined) {
610
+ if (Neo.isUsingStateProviders && me[twoWayBindingSymbol]) {
611
+ // When a component config is updated by its state provider, this flag is set to the config's key.
612
+ // This prevents circular updates in two-way data bindings by skipping the push back to the state provider.
613
+ if (me._skipTwoWayPush === key) {
614
+ return;
615
+ }
597
616
  let binding = me.bind?.[key];
598
617
 
599
618
  if (binding?.twoWay) {
@@ -939,6 +958,16 @@ class Component extends Base {
939
958
  }
940
959
  }
941
960
 
961
+ /**
962
+ * Triggered after the stateProvider config got changed
963
+ * @param {Neo.state.Provider} value
964
+ * @param {Object|Neo.state.Provider|null} oldValue
965
+ * @protected
966
+ */
967
+ afterSetStateProvider(value, oldValue) {
968
+ value?.createBindings(this)
969
+ }
970
+
942
971
  /**
943
972
  * Triggered after the style config got changed
944
973
  * @param {Object} value
@@ -1232,8 +1261,7 @@ class Component extends Base {
1232
1261
  }
1233
1262
  }
1234
1263
 
1235
- // merge the incoming alignment specification into the configured default
1236
- return Neo.merge({}, value, me.constructor.config.align)
1264
+ return value
1237
1265
  }
1238
1266
 
1239
1267
  /**
@@ -1793,7 +1821,7 @@ class Component extends Base {
1793
1821
  * @returns {Neo.state.Provider|null}
1794
1822
  */
1795
1823
  getStateProvider(ntype) {
1796
- if (!currentWorker.isUsingStateProviders) {
1824
+ if (!Neo.isUsingStateProviders) {
1797
1825
  return null
1798
1826
  }
1799
1827
 
@@ -1930,16 +1958,11 @@ class Component extends Base {
1930
1958
  }
1931
1959
 
1932
1960
  /**
1933
- * We are using this method as a ctor hook here to add the initial state.Provider & controller.Component parsing
1934
- * @param {Object} config
1935
- * @param {Boolean} [preventOriginalConfig] True prevents the instance from getting an originalConfig property
1961
+ * @param args
1936
1962
  */
1937
- initConfig(config, preventOriginalConfig) {
1938
- super.initConfig(config, preventOriginalConfig);
1939
-
1940
- let me = this;
1941
-
1942
- me.getStateProvider()?.parseConfig(me)
1963
+ initConfig(...args) {
1964
+ super.initConfig(...args);
1965
+ this.getStateProvider()?.createBindings(this)
1943
1966
  }
1944
1967
 
1945
1968
  /**
@@ -2025,28 +2048,15 @@ class Component extends Base {
2025
2048
  * @returns {Object} config
2026
2049
  */
2027
2050
  mergeConfig(...args) {
2028
- let me = this,
2029
- config = super.mergeConfig(...args),
2030
-
2031
- // it should be possible to set custom configs for the vdom on instance level,
2032
- // however there will be already added attributes (e.g. id), so a merge seems to be the best strategy.
2033
- vdom = {...me._vdom || {}, ...config.vdom || {}};
2034
-
2035
- // avoid any interference on prototype level
2036
- // does not clone existing Neo instances
2037
- me._vdom = Neo.clone(vdom, true, true);
2038
-
2039
- if (config.style) {
2040
- // If we are passed an object, merge it with the class's own style
2041
- me.style = Neo.typeOf(config.style) === 'Object' ? {...config.style, ...me.constructor.config.style} : config.style
2042
- }
2051
+ let config = super.mergeConfig(...args),
2052
+ vdom = config.vdom || config._vdom || {};
2043
2053
 
2044
- me.wrapperStyle = Neo.clone(config.wrapperStyle, false);
2054
+ // It should be possible to modify root level vdom attributes on instance level.
2055
+ // Note that vdom is not a real config, but implemented via get() & set().
2056
+ this._vdom = Neo.clone({...vdom, ...this._vdom || {}}, true);
2045
2057
 
2046
- delete config.style;
2047
2058
  delete config._vdom;
2048
2059
  delete config.vdom;
2049
- delete config.wrapperStyle;
2050
2060
 
2051
2061
  return config
2052
2062
  }
@@ -2131,8 +2141,12 @@ class Component extends Base {
2131
2141
  *
2132
2142
  */
2133
2143
  onConstructed() {
2134
- super.onConstructed();
2135
- this.keys?.register(this)
2144
+ super.onConstructed()
2145
+
2146
+ let me = this;
2147
+
2148
+ me.keys?.register(me);
2149
+ me.getStateProvider()?.createBindings(me)
2136
2150
  }
2137
2151
 
2138
2152
  /**
@@ -2305,10 +2319,12 @@ class Component extends Base {
2305
2319
  * @param {Boolean} [mount] Mount the DOM after the vnode got created
2306
2320
  */
2307
2321
  async render(mount) {
2308
- let me = this,
2309
- autoMount = mount || me.autoMount,
2310
- {app} = me,
2311
- {useVdomWorker} = Neo.config;
2322
+ let me = this,
2323
+ autoMount = mount || me.autoMount,
2324
+ {app} = me,
2325
+ {unitTestMode, useVdomWorker} = Neo.config;
2326
+
2327
+ if (unitTestMode) return;
2312
2328
 
2313
2329
  // Verify that the critical rendering path => CSS files for the new tree is in place
2314
2330
  if (autoMount && currentWorker.countLoadingThemeFiles !== 0) {
@@ -2622,6 +2638,11 @@ class Component extends Base {
2622
2638
  * @protected
2623
2639
  */
2624
2640
  updateVdom(resolve, reject) {
2641
+ if (Neo.config.unitTestMode) {
2642
+ reject?.();
2643
+ return
2644
+ }
2645
+
2625
2646
  let me = this,
2626
2647
  {app, mounted, parentId, vnode} = me;
2627
2648