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
@@ -0,0 +1,412 @@
1
+ import Base from '../core/Base.mjs';
2
+ import ClassSystemUtil from '../util/ClassSystem.mjs';
3
+ import ComponentManager from '../manager/Component.mjs';
4
+ import DomEvents from '../mixin/DomEvents.mjs';
5
+ import Observable from '../core/Observable.mjs';
6
+ import VdomLifecycle from '../mixin/VdomLifecycle.mjs';
7
+
8
+ const
9
+ closestController = Symbol.for('closestController'),
10
+ closestProvider = Symbol.for('closestProvider'),
11
+ twoWayBindingSymbol = Symbol.for('twoWayBinding');
12
+
13
+ /**
14
+ * @class Neo.component.Abstract
15
+ * @extends Neo.core.Base
16
+ * @mixes Neo.component.mixin.DomEvents
17
+ * @mixes Neo.core.Observable
18
+ * @mixes Neo.component.mixin.VdomLifecycle
19
+ */
20
+ class Abstract extends Base {
21
+ static config = {
22
+ /**
23
+ * @member {String} className='Neo.component.Abstract'
24
+ * @protected
25
+ */
26
+ className: 'Neo.component.Abstract',
27
+ /**
28
+ * @member {String} ntype='abstract-component'
29
+ * @protected
30
+ */
31
+ ntype: 'abstract-component',
32
+ /**
33
+ * The name of the App this component belongs to
34
+ * @member {String|null} appName_=null
35
+ * @reactive
36
+ */
37
+ appName_: null,
38
+ /**
39
+ * Bind configs to state.Provider data properties.
40
+ * @member {Object|null} bind=null
41
+ */
42
+ bind: null,
43
+ /**
44
+ * Custom CSS selectors to apply to the root level node of this component
45
+ * @member {String[]} cls_=null
46
+ * @reactive
47
+ */
48
+ cls_: null,
49
+ /**
50
+ * Convenience shortcut to access the data config of the closest state.Provider.
51
+ * Read only.
52
+ * @member {Object} data_=null
53
+ * @protected
54
+ * @reactive
55
+ */
56
+ data_: null,
57
+ /**
58
+ * Disabled components will get the neo-disabled cls applied and won't receive DOM events
59
+ * @member {Boolean} disabled_=false
60
+ * @reactive
61
+ */
62
+ disabled_: false,
63
+ /**
64
+ * @member {Neo.core.Base[]} mixins=[DomEvents, Observable, VdomLifecycle]
65
+ */
66
+ mixins: [DomEvents, Observable, VdomLifecycle],
67
+ /**
68
+ * Override specific stateProvider data properties.
69
+ * This will merge the content.
70
+ * @member {Object|null} modelData=null
71
+ */
72
+ modelData: null,
73
+ /**
74
+ * True after the component render() method was called. Also fires the rendered event.
75
+ * @member {Boolean} mounted_=false
76
+ * @protected
77
+ * @reactive
78
+ */
79
+ mounted_: false,
80
+ /**
81
+ * If the parentId does not match a neo component id, you can manually set this value for finding
82
+ * view controllers or state providers.
83
+ * Use case: manually dropping components into a vdom structure
84
+ * @member {Neo.component.Base|null} parentComponent_=null
85
+ * @protected
86
+ * @reactive
87
+ */
88
+ parentComponent_: null,
89
+ /**
90
+ * @member {String|null} parentId_=null
91
+ * @protected
92
+ * @reactive
93
+ */
94
+ parentId_: null,
95
+ /**
96
+ * Optionally add a state.Provider to share state data with child components
97
+ * @member {Object|null} stateProvider_=null
98
+ * @reactive
99
+ */
100
+ stateProvider_: null,
101
+ /**
102
+ * The custom windowIs (timestamp) this component belongs to
103
+ * @member {Number|null} windowId_=null
104
+ * @reactive
105
+ */
106
+ windowId_: null
107
+ }
108
+
109
+ /**
110
+ * Internal flag which will get set to true while a component is waiting for its mountedPromise
111
+ * @member {Boolean} isAwaitingMount=false
112
+ * @protected
113
+ */
114
+ isAwaitingMount = false
115
+
116
+ /**
117
+ * Convenience shortcut to access the App this component belongs to
118
+ * @returns {Neo.controller.Application|null}
119
+ */
120
+ get app() {
121
+ return Neo.apps[this.appName] || null
122
+ }
123
+
124
+ /**
125
+ * A Promise that resolves when the component is mounted to the DOM.
126
+ * This provides a convenient way to wait for the component to be fully
127
+ * available and interactive before executing subsequent logic.
128
+ *
129
+ * It also handles unmounting by resetting the promise, so it can be safely
130
+ * awaited again if the component is remounted.
131
+ * @returns {Promise<Neo.component.Base>}
132
+ */
133
+ get mountedPromise() {
134
+ let me = this;
135
+
136
+ if (!me._mountedPromise) {
137
+ me._mountedPromise = new Promise(resolve => {
138
+ if (me.mounted) {
139
+ resolve(me);
140
+ } else {
141
+ me.mountedPromiseResolve = resolve
142
+ }
143
+ })
144
+ }
145
+
146
+ return me._mountedPromise
147
+ }
148
+
149
+ /**
150
+ * Convenience method to access the parent component
151
+ * @returns {Neo.component.Base|null}
152
+ */
153
+ get parent() {
154
+ let me = this;
155
+
156
+ return me.parentComponent || (me.parentId === 'document.body' ? null : Neo.getComponent(me.parentId))
157
+ }
158
+
159
+ /**
160
+ * Triggered after any config got changed
161
+ * @param {String} key
162
+ * @param {*} value
163
+ * @param {*} oldValue
164
+ * @protected
165
+ */
166
+ afterSetConfig(key, value, oldValue) {
167
+ let me = this;
168
+
169
+ if (Neo.isUsingStateProviders && me[twoWayBindingSymbol]) {
170
+ // When a component config is updated by its state provider, this flag is set to the config's key.
171
+ // This prevents circular updates in two-way data bindings by skipping the push back to the state provider.
172
+ if (me._skipTwoWayPush === key) {
173
+ return;
174
+ }
175
+ let binding = me.bind?.[key];
176
+
177
+ if (binding?.twoWay) {
178
+ this.getStateProvider()?.setData(binding.key, value)
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Triggered after the id config got changed
185
+ * @param {String|null} value
186
+ * @param {String|null} oldValue
187
+ * @protected
188
+ */
189
+ afterSetId(value, oldValue) {
190
+ super.afterSetId(value, oldValue);
191
+
192
+ oldValue && ComponentManager.unregister(oldValue);
193
+ value && ComponentManager.register(this)
194
+ }
195
+
196
+ /**
197
+ * Triggered after the mounted config got changed
198
+ * @param {Boolean} value
199
+ * @param {Boolean} oldValue
200
+ * @protected
201
+ */
202
+ afterSetMounted(value, oldValue) {
203
+ if (oldValue !== undefined) {
204
+ const me = this;
205
+
206
+ if (value) { // mount
207
+ me.initDomEvents?.();
208
+ me.mountedPromiseResolve?.(this);
209
+ delete me.mountedPromiseResolve
210
+ } else { // unmount
211
+ delete me._mountedPromise
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Triggered after the stateProvider config got changed
218
+ * @param {Neo.state.Provider} value
219
+ * @param {Object|Neo.state.Provider|null} oldValue
220
+ * @protected
221
+ */
222
+ afterSetStateProvider(value, oldValue) {
223
+ value?.createBindings(this)
224
+ }
225
+
226
+ /**
227
+ * Triggered after the windowId config got changed
228
+ * @param {Number|null} value
229
+ * @param {Number|null} oldValue
230
+ * @protected
231
+ */
232
+ afterSetWindowId(value, oldValue) {
233
+ const me = this;
234
+
235
+ if (value) {
236
+ Neo.currentWorker.insertThemeFiles(value, me.__proto__)
237
+ }
238
+
239
+ // If a component gets moved into a different window, an update cycle might still be running.
240
+ // Since the update might no longer get mapped, we want to re-enable this instance for future updates.
241
+ if (oldValue) {
242
+ me.isVdomUpdating = false
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Triggered when accessing the data config
248
+ * Convenience shortcut which is expensive to use, since it will generate a merged parent state providers data map.
249
+ * @param {Object} value
250
+ * @protected
251
+ */
252
+ beforeGetData(value) {
253
+ return this.getStateProvider()?.getHierarchyData()
254
+ }
255
+
256
+ /**
257
+ * Triggered before the stateProvider config gets changed.
258
+ * Creates a state.Provider instance if needed.
259
+ * @param {Object} value
260
+ * @param {Object} oldValue
261
+ * @returns {Neo.state.Provider}
262
+ * @protected
263
+ */
264
+ beforeSetStateProvider(value, oldValue) {
265
+ oldValue?.destroy();
266
+
267
+ if (value) {
268
+ let me = this,
269
+ defaultValues = {component: me};
270
+
271
+ if (me.modelData) {
272
+ defaultValues.data = me.modelData
273
+ }
274
+
275
+ return ClassSystemUtil.beforeSetInstance(value, 'Neo.state.Provider', defaultValues)
276
+ }
277
+
278
+ return null
279
+ }
280
+
281
+ /**
282
+ *
283
+ */
284
+ destroy() {
285
+ this.removeDomEvents();
286
+ ComponentManager.unregister(this);
287
+ this.stateProvider = null; // triggers destroy()
288
+ super.destroy()
289
+ }
290
+
291
+ /**
292
+ * Find an instance stored inside a config via optionally passing a ntype.
293
+ * Returns this[configName] or the closest parent component with a match.
294
+ * Used by getController() & getStateProvider()
295
+ * @param {String} configName
296
+ * @param {String} [ntype]
297
+ * @returns {Neo.core.Base|null}
298
+ */
299
+ getConfigInstanceByNtype(configName, ntype) {
300
+ let me = this,
301
+ config = me[configName],
302
+ {parentComponent} = me;
303
+
304
+ if (config && (!ntype || ntype === config.ntype)) {
305
+ return config
306
+ }
307
+
308
+ if (!parentComponent && me.parentId) {
309
+ parentComponent = me.parent || Neo.get(me.parentId);
310
+ }
311
+
312
+ if (parentComponent) {
313
+ // todo: We need ?. until functional.component.Base supports controllers
314
+ return parentComponent.getConfigInstanceByNtype?.(configName, ntype)
315
+ }
316
+
317
+ return null
318
+ }
319
+
320
+ /**
321
+ * Convenience shortcut
322
+ * @param args
323
+ * @returns {*}
324
+ */
325
+ getState(...args) {
326
+ return this.getStateProvider().getData(...args)
327
+ }
328
+
329
+ /**
330
+ * Returns this.stateProvider or the closest parent stateProvider
331
+ * @param {String} [ntype]
332
+ * @returns {Neo.state.Provider|null}
333
+ */
334
+ getStateProvider(ntype) {
335
+ if (!Neo.isUsingStateProviders) {
336
+ return null
337
+ }
338
+
339
+ let me = this,
340
+ provider;
341
+
342
+ if (!ntype) {
343
+ provider = me[closestProvider];
344
+
345
+ if (provider) {
346
+ return provider
347
+ }
348
+ }
349
+
350
+ provider = me.getConfigInstanceByNtype('stateProvider', ntype);
351
+
352
+ if (!ntype) {
353
+ me[closestProvider] = provider
354
+ }
355
+
356
+ return provider
357
+ }
358
+
359
+ /**
360
+ * @param args
361
+ */
362
+ initConfig(...args) {
363
+ super.initConfig(...args);
364
+ this.getStateProvider()?.createBindings(this)
365
+ }
366
+
367
+ /**
368
+ * Change multiple configs at once, ensuring that all afterSet methods get all new assigned values
369
+ * @param {Object} values={}
370
+ * @param {Boolean} silent=false
371
+ * @returns {Promise<*>}
372
+ */
373
+ set(values={}, silent=false) {
374
+ const
375
+ me = this,
376
+ wasHidden = me.hidden;
377
+
378
+ me.setSilent(values);
379
+
380
+ if (!silent && me.needsVdomUpdate) {
381
+ if (wasHidden && !me.hidden) {
382
+ me.show?.(); // show() is not part of the abstract base class
383
+ return Promise.resolve()
384
+ }
385
+
386
+ return me.promiseUpdate()
387
+ }
388
+
389
+ return Promise.resolve()
390
+ }
391
+
392
+ /**
393
+ * A silent version of set(), which does not trigger a vdom update at the end.
394
+ * Useful for batching multiple config changes.
395
+ * @param {Object} values={}
396
+ */
397
+ setSilent(values={}) {
398
+ this.silentVdomUpdate = true;
399
+ super.set(values);
400
+ this.silentVdomUpdate = false
401
+ }
402
+
403
+ /**
404
+ * Convenience shortcut
405
+ * @param args
406
+ */
407
+ setState(...args) {
408
+ this.getStateProvider().setData(...args)
409
+ }
410
+ }
411
+
412
+ export default Neo.setupClass(Abstract);