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.
- 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/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,384 @@
|
|
1
|
+
import FunctionalBase from '../../functional/component/Base.mjs';
|
2
|
+
import NeoArray from '../../util/Array.mjs';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @class Neo.functional.button.Base
|
6
|
+
* @extends Neo.functional.component.Base
|
7
|
+
*/
|
8
|
+
class Button extends FunctionalBase {
|
9
|
+
/**
|
10
|
+
* Valid values for badgePosition
|
11
|
+
* @member {String[]} badgePositions=['bottom-left','bottom-right','top-left','top-right']
|
12
|
+
* @protected
|
13
|
+
* @static
|
14
|
+
*/
|
15
|
+
static badgePositions = ['bottom-left', 'bottom-right', 'top-left', 'top-right']
|
16
|
+
/**
|
17
|
+
* Valid values for iconPosition
|
18
|
+
* @member {String[]} iconPositions=['top','right','bottom','left']
|
19
|
+
* @protected
|
20
|
+
* @static
|
21
|
+
*/
|
22
|
+
static iconPositions = ['top', 'right', 'bottom', 'left']
|
23
|
+
|
24
|
+
static config = {
|
25
|
+
/**
|
26
|
+
* @member {String} className='Neo.functional.button.Base'
|
27
|
+
* @protected
|
28
|
+
*/
|
29
|
+
className: 'Neo.functional.button.Base',
|
30
|
+
/**
|
31
|
+
* @member {String} ntype='functional-button'
|
32
|
+
* @protected
|
33
|
+
*/
|
34
|
+
ntype: 'functional-button',
|
35
|
+
/**
|
36
|
+
* @member {String} badgePosition_='top-right'
|
37
|
+
* @reactive
|
38
|
+
*/
|
39
|
+
badgePosition_: 'top-right',
|
40
|
+
/**
|
41
|
+
* @member {String|null} badgeText_=null
|
42
|
+
* @reactive
|
43
|
+
*/
|
44
|
+
badgeText_: null,
|
45
|
+
/**
|
46
|
+
* @member {String[]} baseCls=['neo-button']
|
47
|
+
*/
|
48
|
+
baseCls: ['neo-button'],
|
49
|
+
/**
|
50
|
+
* false calls Neo.Main.setRoute()
|
51
|
+
* @member {Boolean} editRoute=true
|
52
|
+
*/
|
53
|
+
editRoute: true,
|
54
|
+
/**
|
55
|
+
* @member {Function|String|null} handler_=null
|
56
|
+
* @reactive
|
57
|
+
*/
|
58
|
+
handler_: null,
|
59
|
+
/**
|
60
|
+
* @member {Object|String|null} handlerScope=null
|
61
|
+
*/
|
62
|
+
handlerScope: null,
|
63
|
+
/**
|
64
|
+
* @member {String[]|null} [iconCls_=null]
|
65
|
+
* @reactive
|
66
|
+
*/
|
67
|
+
iconCls_: null,
|
68
|
+
/**
|
69
|
+
* @member {String|null} iconColor_=null
|
70
|
+
* @reactive
|
71
|
+
*/
|
72
|
+
iconColor_: null,
|
73
|
+
/**
|
74
|
+
* @member {String} iconPosition_='left'
|
75
|
+
* @reactive
|
76
|
+
*/
|
77
|
+
iconPosition_: 'left',
|
78
|
+
/**
|
79
|
+
* @member {Object|Object[]|null} menu_=null
|
80
|
+
* @reactive
|
81
|
+
*/
|
82
|
+
menu_: null,
|
83
|
+
/**
|
84
|
+
* @member {Boolean} pressed_=false
|
85
|
+
* @reactive
|
86
|
+
*/
|
87
|
+
pressed_: false,
|
88
|
+
/**
|
89
|
+
* Internal state for managing the ripple effect animations.
|
90
|
+
* Each object in the array requires a unique `id`.
|
91
|
+
* @member {Array} ripples_=[]
|
92
|
+
* @protected
|
93
|
+
* @reactive
|
94
|
+
*/
|
95
|
+
ripples_: [],
|
96
|
+
/**
|
97
|
+
* @member {String|null} route_=null
|
98
|
+
* @reactive
|
99
|
+
*/
|
100
|
+
route_: null,
|
101
|
+
/**
|
102
|
+
* @member {String} tag='button'
|
103
|
+
* @reactive
|
104
|
+
*/
|
105
|
+
tag: 'button',
|
106
|
+
/**
|
107
|
+
* @member {String|null} text_=null
|
108
|
+
* @reactive
|
109
|
+
*/
|
110
|
+
text_: null,
|
111
|
+
/**
|
112
|
+
* @member {String|null} url_=null
|
113
|
+
* @reactive
|
114
|
+
*/
|
115
|
+
url_: null,
|
116
|
+
/**
|
117
|
+
* @member {String} urlTarget_='_blank'
|
118
|
+
* @reactive
|
119
|
+
*/
|
120
|
+
urlTarget_: '_blank',
|
121
|
+
/**
|
122
|
+
* @member {Boolean} useRippleEffect_=true
|
123
|
+
* @reactive
|
124
|
+
*/
|
125
|
+
useRippleEffect_: true
|
126
|
+
}
|
127
|
+
|
128
|
+
/**
|
129
|
+
* @member {Number} rippleEffectDuration=400
|
130
|
+
*/
|
131
|
+
rippleEffectDuration = 400
|
132
|
+
/**
|
133
|
+
* @member {Number|null} rippleCleanupTimeout=null
|
134
|
+
* @protected
|
135
|
+
*/
|
136
|
+
rippleCleanupTimeout = null
|
137
|
+
|
138
|
+
/**
|
139
|
+
* @param {Object} config
|
140
|
+
*/
|
141
|
+
construct(config) {
|
142
|
+
super.construct(config);
|
143
|
+
|
144
|
+
let me = this;
|
145
|
+
|
146
|
+
me.addDomListeners({
|
147
|
+
click: me.onClick,
|
148
|
+
scope: me
|
149
|
+
})
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* The single source of truth for the button's VDOM.
|
154
|
+
* This method is automatically re-run when any of its dependent configs change.
|
155
|
+
* @param {Object} config The component's config object (this instance).
|
156
|
+
* @param {Object} data The hierarchical data from state.Provider.
|
157
|
+
* @returns {Object}
|
158
|
+
*/
|
159
|
+
createVdom(config, data) {
|
160
|
+
const me = this,
|
161
|
+
{
|
162
|
+
badgePosition, badgeText, cls, editRoute, iconCls, iconColor, iconPosition,
|
163
|
+
pressed, ripples, route, style, tag, text, url, urlTarget, useRippleEffect
|
164
|
+
} = config;
|
165
|
+
|
166
|
+
const vdomCls = [...me.baseCls, ...cls || []];
|
167
|
+
NeoArray.toggle(vdomCls, 'no-text', !text);
|
168
|
+
NeoArray.toggle(vdomCls, 'pressed', pressed);
|
169
|
+
vdomCls.push('icon-' + iconPosition);
|
170
|
+
|
171
|
+
const link = !editRoute && route || url;
|
172
|
+
const finalTag = link ? 'a' : tag;
|
173
|
+
|
174
|
+
return {
|
175
|
+
tag : finalTag,
|
176
|
+
cls : vdomCls,
|
177
|
+
style,
|
178
|
+
href : link ? (link.startsWith('#') ? link : '#' + link) : null,
|
179
|
+
target: url ? urlTarget : null,
|
180
|
+
type : finalTag === 'button' ? 'button' : null,
|
181
|
+
cn : [
|
182
|
+
// iconNode
|
183
|
+
{
|
184
|
+
tag : 'span',
|
185
|
+
cls : ['neo-button-glyph', ...iconCls || []],
|
186
|
+
removeDom: !iconCls,
|
187
|
+
style : {color: iconColor || null}
|
188
|
+
},
|
189
|
+
// textNode
|
190
|
+
{
|
191
|
+
tag : 'span',
|
192
|
+
cls : ['neo-button-text'],
|
193
|
+
removeDom: !text,
|
194
|
+
text
|
195
|
+
},
|
196
|
+
// badgeNode
|
197
|
+
{
|
198
|
+
tag : 'span',
|
199
|
+
cls : ['neo-button-badge', 'neo-' + badgePosition],
|
200
|
+
removeDom: !badgeText,
|
201
|
+
text : badgeText
|
202
|
+
},
|
203
|
+
// rippleWrapper
|
204
|
+
{
|
205
|
+
cls : ['neo-button-ripple-wrapper'],
|
206
|
+
removeDom: !(useRippleEffect && ripples.length > 0),
|
207
|
+
cn : ripples.map(ripple => ({
|
208
|
+
cls : ['neo-button-ripple'],
|
209
|
+
id : ripple.id,
|
210
|
+
style: {
|
211
|
+
animation: `ripple ${me.rippleEffectDuration}ms linear`,
|
212
|
+
height : `${ripple.diameter}px`,
|
213
|
+
left : `${ripple.left}px`,
|
214
|
+
top : `${ripple.top}px`,
|
215
|
+
width : `${ripple.diameter}px`
|
216
|
+
}
|
217
|
+
}))
|
218
|
+
}
|
219
|
+
]
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
* @param {Object} data
|
225
|
+
*/
|
226
|
+
onClick(data) {
|
227
|
+
let me = this;
|
228
|
+
|
229
|
+
me.bindCallback(me.handler, 'handler', me.handlerScope || me);
|
230
|
+
me.handler?.(data);
|
231
|
+
|
232
|
+
me.menu && me.toggleMenu();
|
233
|
+
me.route && me.changeRoute();
|
234
|
+
|
235
|
+
if (me.useRippleEffect) {
|
236
|
+
const buttonRect = data.path[0].rect;
|
237
|
+
const diameter = Math.max(buttonRect.height, buttonRect.width);
|
238
|
+
const radius = diameter / 2;
|
239
|
+
const rippleId = Neo.getId('ripple');
|
240
|
+
|
241
|
+
// Add a new ripple to the state
|
242
|
+
me.ripples = [...me.ripples, {
|
243
|
+
id : rippleId,
|
244
|
+
diameter,
|
245
|
+
left: data.clientX - buttonRect.left - radius,
|
246
|
+
top : data.clientY - buttonRect.top - radius
|
247
|
+
}];
|
248
|
+
|
249
|
+
// Clear any previously scheduled cleanup to ensure only the last
|
250
|
+
// click's timer will execute the cleanup.
|
251
|
+
clearTimeout(me.rippleCleanupTimeout);
|
252
|
+
|
253
|
+
// Schedule the cleanup for the entire ripples array.
|
254
|
+
me.rippleCleanupTimeout = me.timeout(me.rippleEffectDuration).then(() => {
|
255
|
+
me.ripples = [];
|
256
|
+
});
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
/**
|
261
|
+
* Triggered after the menu config got changed
|
262
|
+
* @param {Object|Object[]|null} value
|
263
|
+
* @param {Object|Object[]|null} oldValue
|
264
|
+
* @protected
|
265
|
+
*/
|
266
|
+
afterSetMenu(value, oldValue) {
|
267
|
+
const me = this;
|
268
|
+
|
269
|
+
if (value) {
|
270
|
+
// Ensure menuList is destroyed before creating a new one
|
271
|
+
me.menuList?.destroy(true, false);
|
272
|
+
me.menuList = null;
|
273
|
+
|
274
|
+
import('../../menu/List.mjs').then(module => {
|
275
|
+
const isArray = Array.isArray(value),
|
276
|
+
items = isArray ? value : value.items,
|
277
|
+
menuConfig = isArray ? {} : value,
|
278
|
+
stateProvider = me.getStateProvider(),
|
279
|
+
{appName, theme, windowId} = me,
|
280
|
+
|
281
|
+
config = Neo.merge({
|
282
|
+
module : module.default,
|
283
|
+
align : {edgeAlign: 't0-b0', target: me.id},
|
284
|
+
appName,
|
285
|
+
displayField : 'text',
|
286
|
+
floating : true,
|
287
|
+
hidden : true,
|
288
|
+
parentComponent: me,
|
289
|
+
theme,
|
290
|
+
windowId
|
291
|
+
}, menuConfig);
|
292
|
+
|
293
|
+
if (items) {
|
294
|
+
config.items = items
|
295
|
+
}
|
296
|
+
|
297
|
+
if (stateProvider) {
|
298
|
+
config.stateProvider = {parent: stateProvider}
|
299
|
+
}
|
300
|
+
|
301
|
+
me.menuList = Neo.create(config)
|
302
|
+
})
|
303
|
+
} else if (me.menuList) {
|
304
|
+
me.menuList.destroy(true, false);
|
305
|
+
me.menuList = null;
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
309
|
+
/**
|
310
|
+
* Triggered before the badgePosition config gets changed
|
311
|
+
* @param {String} value
|
312
|
+
* @param {String} oldValue
|
313
|
+
* @returns {String}
|
314
|
+
* @protected
|
315
|
+
*/
|
316
|
+
beforeSetBadgePosition(value, oldValue) {
|
317
|
+
// Using `this.constructor.badgePositions` is superior to `Button.badgePositions` for future class extensions
|
318
|
+
return this.beforeSetEnumValue(value, oldValue, 'badgePosition', this.constructor.badgePositions)
|
319
|
+
}
|
320
|
+
|
321
|
+
/**
|
322
|
+
* Triggered before the iconCls config gets changed. Converts the string into an array if needed.
|
323
|
+
* @param {Array|String|null} value
|
324
|
+
* @param {Array|String|null} oldValue
|
325
|
+
* @returns {Array|null}
|
326
|
+
* @protected
|
327
|
+
*/
|
328
|
+
beforeSetIconCls(value, oldValue) {
|
329
|
+
if (value && !Array.isArray(value)) {
|
330
|
+
return value.split(' ').filter(Boolean)
|
331
|
+
}
|
332
|
+
|
333
|
+
return value || null
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* Triggered before the iconPosition config gets changed
|
338
|
+
* @param {String} value
|
339
|
+
* @param {String} oldValue
|
340
|
+
* @protected
|
341
|
+
*/
|
342
|
+
beforeSetIconPosition(value, oldValue) {
|
343
|
+
// Using `this.constructor.iconPositions` is superior to `Button.iconPositions` for future class extensions
|
344
|
+
return this.beforeSetEnumValue(value, oldValue, 'iconPosition', this.constructor.iconPositions)
|
345
|
+
}
|
346
|
+
|
347
|
+
/**
|
348
|
+
* @protected
|
349
|
+
*/
|
350
|
+
changeRoute() {
|
351
|
+
const me = this;
|
352
|
+
me.editRoute && Neo.Main.editRoute(me.route)
|
353
|
+
}
|
354
|
+
|
355
|
+
/**
|
356
|
+
* @param args
|
357
|
+
*/
|
358
|
+
destroy(...args) {
|
359
|
+
const me = this;
|
360
|
+
|
361
|
+
clearTimeout(me.rippleCleanupTimeout);
|
362
|
+
me.menuList?.destroy(true, false);
|
363
|
+
super.destroy(...args)
|
364
|
+
}
|
365
|
+
|
366
|
+
/**
|
367
|
+
*
|
368
|
+
*/
|
369
|
+
async toggleMenu() {
|
370
|
+
const me = this;
|
371
|
+
let {menuList} = me,
|
372
|
+
hidden = !menuList?.hidden;
|
373
|
+
|
374
|
+
if (menuList) {
|
375
|
+
menuList.hidden = hidden;
|
376
|
+
|
377
|
+
if (!hidden) {
|
378
|
+
await me.timeout(50)
|
379
|
+
}
|
380
|
+
}
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
export default Neo.setupClass(Button);
|
@@ -1,10 +1,6 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import
|
4
|
-
import Effect from '../../core/Effect.mjs';
|
5
|
-
import NeoArray from '../../util/Array.mjs';
|
6
|
-
import Observable from '../../core/Observable.mjs';
|
7
|
-
import VdomLifecycle from '../../mixin/VdomLifecycle.mjs';
|
1
|
+
import Abstract from '../../component/Abstract.mjs';
|
2
|
+
import Effect from '../../core/Effect.mjs';
|
3
|
+
import NeoArray from '../../util/Array.mjs';
|
8
4
|
|
9
5
|
const
|
10
6
|
activeDomListenersSymbol = Symbol.for('activeDomListeners'),
|
@@ -15,12 +11,9 @@ const
|
|
15
11
|
|
16
12
|
/**
|
17
13
|
* @class Neo.functional.component.Base
|
18
|
-
* @extends Neo.
|
19
|
-
* @mixes Neo.component.mixin.DomEvents
|
20
|
-
* @mixes Neo.core.Observable
|
21
|
-
* @mixes Neo.component.mixin.VdomLifecycle
|
14
|
+
* @extends Neo.component.Abstract
|
22
15
|
*/
|
23
|
-
class FunctionalBase extends
|
16
|
+
class FunctionalBase extends Abstract {
|
24
17
|
static config = {
|
25
18
|
/**
|
26
19
|
* @member {String} className='Neo.functional.component.Base'
|
@@ -32,40 +25,11 @@ class FunctionalBase extends Base {
|
|
32
25
|
* @protected
|
33
26
|
*/
|
34
27
|
ntype: 'functional-component',
|
35
|
-
/**
|
36
|
-
* Custom CSS selectors to apply to the root level node of this component
|
37
|
-
* @member {String[]} cls=null
|
38
|
-
* @reactive
|
39
|
-
*/
|
40
|
-
cls: null,
|
41
|
-
/**
|
42
|
-
* @member {Neo.core.Base[]} mixins=[DomEvents, Observable, VdomLifecycle]
|
43
|
-
*/
|
44
|
-
mixins: [DomEvents, Observable, VdomLifecycle],
|
45
|
-
/**
|
46
|
-
* True after the component render() method was called. Also fires the rendered event.
|
47
|
-
* @member {Boolean} mounted_=false
|
48
|
-
* @protected
|
49
|
-
* @reactive
|
50
|
-
*/
|
51
|
-
mounted_: false,
|
52
|
-
/**
|
53
|
-
* @member {String|null} parentId_=null
|
54
|
-
* @protected
|
55
|
-
* @reactive
|
56
|
-
*/
|
57
|
-
parentId_: null,
|
58
28
|
/**
|
59
29
|
* The vdom markup for this component.
|
60
30
|
* @member {Object} vdom={}
|
61
31
|
*/
|
62
|
-
vdom: {}
|
63
|
-
/**
|
64
|
-
* The custom windowIs (timestamp) this component belongs to
|
65
|
-
* @member {Number|null} windowId_=null
|
66
|
-
* @reactive
|
67
|
-
*/
|
68
|
-
windowId_: null
|
32
|
+
vdom: {}
|
69
33
|
}
|
70
34
|
|
71
35
|
/**
|
@@ -73,12 +37,6 @@ class FunctionalBase extends Base {
|
|
73
37
|
* @member {Map|null} childComponents=null
|
74
38
|
*/
|
75
39
|
childComponents = null
|
76
|
-
/**
|
77
|
-
* Internal flag which will get set to true while a component is waiting for its mountedPromise
|
78
|
-
* @member {Boolean} isAwaitingMount=false
|
79
|
-
* @protected
|
80
|
-
*/
|
81
|
-
isAwaitingMount = false
|
82
40
|
/**
|
83
41
|
* Internal Map to store the next set of components after the createVdom() Effect has run.
|
84
42
|
* @member {Map|null} nextChildComponents=null
|
@@ -86,41 +44,6 @@ class FunctionalBase extends Base {
|
|
86
44
|
*/
|
87
45
|
#nextChildComponents = null
|
88
46
|
|
89
|
-
/**
|
90
|
-
* A Promise that resolves when the component is mounted to the DOM.
|
91
|
-
* This provides a convenient way to wait for the component to be fully
|
92
|
-
* available and interactive before executing subsequent logic.
|
93
|
-
*
|
94
|
-
* It also handles unmounting by resetting the promise, so it can be safely
|
95
|
-
* awaited again if the component is remounted.
|
96
|
-
* @returns {Promise<Neo.component.Base>}
|
97
|
-
*/
|
98
|
-
get mountedPromise() {
|
99
|
-
let me = this;
|
100
|
-
|
101
|
-
if (!me._mountedPromise) {
|
102
|
-
me._mountedPromise = new Promise(resolve => {
|
103
|
-
if (me.mounted) {
|
104
|
-
resolve(me);
|
105
|
-
} else {
|
106
|
-
me.mountedPromiseResolve = resolve
|
107
|
-
}
|
108
|
-
})
|
109
|
-
}
|
110
|
-
|
111
|
-
return me._mountedPromise
|
112
|
-
}
|
113
|
-
|
114
|
-
/**
|
115
|
-
* Convenience method to access the parent component
|
116
|
-
* @returns {Neo.component.Base|null}
|
117
|
-
*/
|
118
|
-
get parent() {
|
119
|
-
let me = this;
|
120
|
-
|
121
|
-
return me.parentComponent || (me.parentId === 'document.body' ? null : Neo.getComponent(me.parentId))
|
122
|
-
}
|
123
|
-
|
124
47
|
/**
|
125
48
|
* @param {Object} config
|
126
49
|
*/
|
@@ -143,7 +66,7 @@ class FunctionalBase extends Base {
|
|
143
66
|
fn: () => {
|
144
67
|
me[hookIndexSymbol] = 0;
|
145
68
|
me[pendingDomEventsSymbol] = []; // Clear pending events for new render
|
146
|
-
me[vdomToApplySymbol] = me.createVdom(me
|
69
|
+
me[vdomToApplySymbol] = me.createVdom(me)
|
147
70
|
},
|
148
71
|
componentId: me.id,
|
149
72
|
subscriber : {
|
@@ -154,19 +77,6 @@ class FunctionalBase extends Base {
|
|
154
77
|
})
|
155
78
|
}
|
156
79
|
|
157
|
-
/**
|
158
|
-
* Triggered after the id config got changed
|
159
|
-
* @param {String|null} value
|
160
|
-
* @param {String|null} oldValue
|
161
|
-
* @protected
|
162
|
-
*/
|
163
|
-
afterSetId(value, oldValue) {
|
164
|
-
super.afterSetId(value, oldValue);
|
165
|
-
|
166
|
-
oldValue && ComponentManager.unregister(oldValue);
|
167
|
-
value && ComponentManager.register(this)
|
168
|
-
}
|
169
|
-
|
170
80
|
/**
|
171
81
|
* Triggered after the mounted config got changed
|
172
82
|
* @param {Boolean} value
|
@@ -174,19 +84,14 @@ class FunctionalBase extends Base {
|
|
174
84
|
* @protected
|
175
85
|
*/
|
176
86
|
afterSetMounted(value, oldValue) {
|
87
|
+
super.afterSetMounted(value, oldValue);
|
88
|
+
|
177
89
|
if (oldValue !== undefined) {
|
178
90
|
const me = this;
|
179
91
|
|
180
92
|
if (value) { // mount
|
181
|
-
me.initDomEvents();
|
182
|
-
|
183
93
|
// Initial registration of DOM event listeners when component mounts
|
184
94
|
me.applyPendingDomListeners();
|
185
|
-
|
186
|
-
me.mountedPromiseResolve?.(this);
|
187
|
-
delete me.mountedPromiseResolve
|
188
|
-
} else { // unmount
|
189
|
-
delete me._mountedPromise
|
190
95
|
}
|
191
96
|
}
|
192
97
|
}
|
@@ -198,21 +103,11 @@ class FunctionalBase extends Base {
|
|
198
103
|
* @protected
|
199
104
|
*/
|
200
105
|
afterSetWindowId(value, oldValue) {
|
201
|
-
|
202
|
-
|
203
|
-
if (value) {
|
204
|
-
Neo.currentWorker.insertThemeFiles(value, me.__proto__)
|
205
|
-
}
|
106
|
+
super.afterSetWindowId(value, oldValue);
|
206
107
|
|
207
|
-
|
108
|
+
this.childComponents?.forEach(childData => {
|
208
109
|
childData.instance.windowId = value
|
209
110
|
})
|
210
|
-
|
211
|
-
// If a component gets moved into a different window, an update cycle might still be running.
|
212
|
-
// Since the update might no longer get mapped, we want to re-enable this instance for future updates.
|
213
|
-
if (oldValue) {
|
214
|
-
me.isVdomUpdating = false
|
215
|
-
}
|
216
111
|
}
|
217
112
|
|
218
113
|
/**
|
@@ -242,14 +137,44 @@ class FunctionalBase extends Base {
|
|
242
137
|
}
|
243
138
|
}
|
244
139
|
|
140
|
+
/**
|
141
|
+
* A lifecycle hook that runs after a state change has been detected but before the
|
142
|
+
* VDOM update is dispatched. It provides a dedicated place for logic that needs to
|
143
|
+
* execute before rendering, such as calculating derived data or caching values.
|
144
|
+
*
|
145
|
+
* You can prevent the VDOM update by returning `false` from this method. This is
|
146
|
+
* useful for advanced cases where you might want to manually trigger a different
|
147
|
+
* update after modifying other component configs.
|
148
|
+
*
|
149
|
+
* **IMPORTANT**: Do not change the value of any config that is used as a dependency
|
150
|
+
* within the `createVdom` method from inside this hook, as it will cause an
|
151
|
+
* infinite update loop. This hook is for one-way data flow, not for triggering
|
152
|
+
* cascading reactive changes.
|
153
|
+
*
|
154
|
+
* @returns {Boolean|undefined} Return `false` to cancel the upcoming VDOM update.
|
155
|
+
* @example
|
156
|
+
* beforeUpdate() {
|
157
|
+
* // Perform an expensive calculation and cache the result on the instance
|
158
|
+
* this.processedData = this.processRawData(this.rawData);
|
159
|
+
*
|
160
|
+
* // Example of conditionally cancelling an update
|
161
|
+
* if (this.processedData.length === 0 && this.vdom.cn?.length === 0) {
|
162
|
+
* return false; // Don't re-render if there's nothing to show
|
163
|
+
* }
|
164
|
+
* }
|
165
|
+
*/
|
166
|
+
beforeUpdate() {
|
167
|
+
// This method can be overridden by subclasses
|
168
|
+
}
|
169
|
+
|
245
170
|
/**
|
246
171
|
* Override this method in your functional component to return its VDOM structure.
|
247
172
|
* This method will be automatically re-executed when any of the component's configs change.
|
173
|
+
* To access data from a state provider, use `config.data`.
|
248
174
|
* @param {Neo.functional.component.Base} config - Mental model: while it contains the instance, it makes it clear to access configs
|
249
|
-
* @param {Object} data - Convenience shortcut for accessing `state.Provider` data
|
250
175
|
* @returns {Object} The VDOM structure for the component.
|
251
176
|
*/
|
252
|
-
createVdom(config
|
177
|
+
createVdom(config) {
|
253
178
|
// This method should be overridden by subclasses
|
254
179
|
return {}
|
255
180
|
}
|
@@ -268,13 +193,9 @@ class FunctionalBase extends Base {
|
|
268
193
|
});
|
269
194
|
me.childComponents?.clear();
|
270
195
|
|
271
|
-
me.removeDomEvents();
|
272
|
-
|
273
196
|
// Remove any pending DOM event listeners that might not have been mounted
|
274
197
|
me[pendingDomEventsSymbol] = null;
|
275
198
|
|
276
|
-
ComponentManager.unregister(me);
|
277
|
-
|
278
199
|
super.destroy()
|
279
200
|
}
|
280
201
|
|
@@ -382,7 +303,14 @@ class FunctionalBase extends Base {
|
|
382
303
|
root.id = me.id
|
383
304
|
}
|
384
305
|
|
385
|
-
|
306
|
+
// Re-hydrate the new vdom with stable IDs from the previous vnode tree.
|
307
|
+
// This is crucial for functional components where the vdom is recreated on every render,
|
308
|
+
// ensuring the diffing algorithm can track nodes correctly.
|
309
|
+
me.syncVdomIds();
|
310
|
+
|
311
|
+
if (me.beforeUpdate() !== false) {
|
312
|
+
me.updateVdom()
|
313
|
+
}
|
386
314
|
|
387
315
|
// Update DOM event listeners based on the new render
|
388
316
|
if (me.mounted) {
|
@@ -472,28 +400,6 @@ class FunctionalBase extends Base {
|
|
472
400
|
|
473
401
|
return vdomTree
|
474
402
|
}
|
475
|
-
|
476
|
-
/**
|
477
|
-
* Change multiple configs at once, ensuring that all afterSet methods get all new assigned values
|
478
|
-
* @param {Object} values={}
|
479
|
-
* @param {Boolean} silent=false
|
480
|
-
* @returns {Promise<*>}
|
481
|
-
*/
|
482
|
-
set(values={}, silent=false) {
|
483
|
-
let me = this;
|
484
|
-
|
485
|
-
me.silentVdomUpdate = true;
|
486
|
-
|
487
|
-
super.set(values);
|
488
|
-
|
489
|
-
me.silentVdomUpdate = false;
|
490
|
-
|
491
|
-
if (silent || !me.needsVdomUpdate) {
|
492
|
-
return Promise.resolve()
|
493
|
-
}
|
494
|
-
|
495
|
-
return me.promiseUpdate()
|
496
|
-
}
|
497
403
|
}
|
498
404
|
|
499
405
|
export default Neo.setupClass(FunctionalBase);
|
package/src/layout/Cube.mjs
CHANGED
@@ -114,11 +114,15 @@ class Cube extends Card {
|
|
114
114
|
|
115
115
|
me.nestVdom();
|
116
116
|
|
117
|
-
container
|
118
|
-
|
119
|
-
me.timeout(50).then(() => {
|
120
|
-
container.addCls('neo-animate')
|
117
|
+
me.observeConfig(container, 'mounted', value => {
|
118
|
+
value && container.addCls('neo-animate')
|
121
119
|
})
|
120
|
+
|
121
|
+
if (container.mounted) {
|
122
|
+
container.promiseUpdate().then(() => {
|
123
|
+
container.addCls('neo-animate')
|
124
|
+
})
|
125
|
+
}
|
122
126
|
}
|
123
127
|
|
124
128
|
/**
|