neo.mjs 10.0.0-beta.6 → 10.0.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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
- package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +24 -0
- package/apps/portal/view/ViewportController.mjs +6 -4
- package/apps/portal/view/examples/List.mjs +28 -19
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/functional/button/base/MainContainer.mjs +207 -0
- package/examples/functional/button/base/app.mjs +6 -0
- package/examples/functional/button/base/index.html +11 -0
- package/examples/functional/button/base/neo-config.json +6 -0
- package/learn/blog/v10-deep-dive-functional-components.md +293 -0
- package/learn/blog/v10-deep-dive-reactivity.md +522 -0
- package/learn/blog/v10-deep-dive-state-provider.md +432 -0
- package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
- package/learn/blog/v10-post1-love-story.md +383 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +47 -45
- package/src/component/Abstract.mjs +412 -0
- package/src/component/Base.mjs +18 -380
- package/src/core/Base.mjs +34 -33
- package/src/core/Effect.mjs +30 -34
- package/src/core/EffectManager.mjs +101 -14
- package/src/core/Observable.mjs +69 -65
- package/src/form/field/Text.mjs +11 -5
- package/src/functional/button/Base.mjs +384 -0
- package/src/functional/component/Base.mjs +51 -145
- package/src/layout/Cube.mjs +8 -4
- package/src/manager/VDomUpdate.mjs +179 -94
- package/src/mixin/VdomLifecycle.mjs +4 -1
- package/src/state/Provider.mjs +41 -27
- package/src/util/VDom.mjs +11 -4
- package/src/util/vdom/TreeBuilder.mjs +38 -62
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
- package/test/siesta/siesta.js +15 -3
- package/test/siesta/tests/VdomCalendar.mjs +7 -7
- package/test/siesta/tests/VdomHelper.mjs +7 -7
- package/test/siesta/tests/classic/Button.mjs +113 -0
- package/test/siesta/tests/core/EffectBatching.mjs +46 -41
- package/test/siesta/tests/functional/Button.mjs +113 -0
- package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
- package/test/siesta/tests/vdom/Advanced.mjs +14 -8
- package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
- package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
- package/test/siesta/tests/vdom/table/Container.mjs +9 -5
- 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);
|