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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/datahandling/StateProviders.md +1 -0
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/tree.json +1 -0
- package/package.json +2 -2
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +226 -78
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +7 -2
- package/src/component/Base.mjs +67 -46
- package/src/container/Base.mjs +28 -24
- package/src/core/Base.mjs +138 -19
- package/src/core/Config.mjs +123 -32
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- 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);
|
package/src/collection/Base.mjs
CHANGED
@@ -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
|
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
|
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
|
})
|
package/src/component/Base.mjs
CHANGED
@@ -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
|
-
|
61
|
-
|
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}
|
334
|
+
* @member {Object} style={[isDescriptor]: true, merge: 'shallow', value: null}
|
331
335
|
*/
|
332
|
-
style_:
|
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_
|
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_:
|
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 (
|
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
|
-
|
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 (!
|
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
|
-
*
|
1934
|
-
* @param {Object} config
|
1935
|
-
* @param {Boolean} [preventOriginalConfig] True prevents the instance from getting an originalConfig property
|
1961
|
+
* @param args
|
1936
1962
|
*/
|
1937
|
-
initConfig(
|
1938
|
-
super.initConfig(
|
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
|
2029
|
-
config
|
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
|
-
|
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
|
-
|
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
|
2309
|
-
autoMount
|
2310
|
-
{app}
|
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
|
|