neo.mjs 10.0.0-alpha.3 → 10.0.0-alpha.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/CODING_GUIDELINES.md +1 -1
- package/README.md +52 -11
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/blog/List.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/ContentComponent.mjs +2 -1
- package/apps/portal/view/learn/MainContainerStateProvider.mjs +3 -6
- package/apps/realworld/view/HomeComponent.mjs +1 -1
- package/apps/realworld/view/user/ProfileComponent.mjs +1 -1
- package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
- package/apps/shareddialog/view/MainContainerController.mjs +2 -2
- package/buildScripts/buildThemes.mjs +1 -1
- package/examples/grid/animatedRowSorting/Viewport.mjs +4 -4
- package/examples/grid/bigData/ControlsContainer.mjs +3 -3
- package/examples/grid/bigData/GridContainer.mjs +8 -8
- package/examples/grid/cellEditing/MainContainer.mjs +5 -5
- package/examples/grid/container/MainContainer.mjs +4 -4
- package/examples/grid/nestedRecordFields/Viewport.mjs +5 -5
- package/learn/README.md +83 -0
- package/learn/guides/ApplicationBootstrap.md +352 -0
- package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +500 -0
- package/learn/guides/WorkingWithVDom.md +748 -0
- package/learn/tree.json +53 -0
- package/package.json +2 -2
- package/resources/scss/src/grid/{View.scss → Body.scss} +2 -2
- package/resources/scss/src/grid/VerticalScrollbar.scss +1 -1
- package/resources/scss/src/grid/plugin/AnimateRows.scss +1 -1
- package/resources/scss/src/grid/plugin/CellEditing.scss +1 -1
- package/resources/scss/theme-dark/grid/{View.scss → Body.scss} +1 -1
- package/resources/scss/theme-light/grid/{View.scss → Body.scss} +1 -1
- package/resources/scss/theme-neo-light/grid/{View.scss → Body.scss} +1 -1
- package/src/DefaultConfig.mjs +27 -14
- package/src/Main.mjs +1 -1
- package/src/Neo.mjs +16 -0
- package/src/button/Base.mjs +2 -2
- package/src/calendar/view/MainContainerStateProvider.mjs +1 -1
- package/src/grid/{View.mjs → Body.mjs} +17 -17
- package/src/grid/Container.mjs +58 -58
- package/src/grid/ScrollManager.mjs +56 -56
- package/src/grid/VerticalScrollbar.mjs +2 -2
- package/src/grid/_export.mjs +2 -2
- package/src/grid/column/AnimatedChange.mjs +5 -5
- package/src/grid/column/Base.mjs +1 -1
- package/src/grid/column/Component.mjs +6 -6
- package/src/grid/header/Button.mjs +1 -1
- package/src/grid/header/Toolbar.mjs +9 -9
- package/src/grid/plugin/AnimateRows.mjs +1 -2
- package/src/layout/Cube.mjs +2 -2
- package/src/main/DeltaUpdates.mjs +11 -10
- package/src/main/addon/Navigator.mjs +1 -1
- package/src/main/addon/WindowPosition.mjs +1 -1
- package/src/main/render/StringBasedRenderer.mjs +1 -1
- package/src/tab/header/Toolbar.mjs +1 -1
- package/src/table/header/Button.mjs +1 -1
- package/src/toolbar/Base.mjs +1 -1
- package/src/util/Style.mjs +2 -6
- package/src/util/VDom.mjs +1 -1
- package/src/util/VNode.mjs +1 -1
- package/src/vdom/Helper.mjs +96 -49
- package/src/vdom/VNode.mjs +38 -2
- package/src/worker/App.mjs +8 -19
- package/src/worker/Base.mjs +87 -5
- package/src/worker/Manager.mjs +90 -36
- package/resources/data/deck/learnneo/tree.json +0 -50
- package/resources/data/deck/whyneo.md +0 -80
- /package/{resources/data/deck/learnneo/pages → learn}/Glossary.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/UsingTheseTopics.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/ConfigSystem.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/Effort.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/Features.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/FormsEngine.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/FourEnvironments.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/Introduction.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/MultiWindow.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/OffTheMainThread.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/Quick.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/benefits/Speed.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/ComponentModels.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Config.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/DescribingTheUI.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Events.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Extending.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/References.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Setup.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Workspaces.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/ComponentsAndContainers.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/CustomComponents.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/Forms.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/InstanceLifecycle.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/Layouts.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonExample.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonIntro.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/Mixins.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/MultiWindow.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/PortalApp.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/StateProviders.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/Tables.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/events/CustomEvents.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/guides/events/DomEvents.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/javascript/ClassFeatures.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/javascript/Classes.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/javascript/NewNode.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/javascript/Overrides.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/javascript/Super.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/tutorials/Earthquakes.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/tutorials/RSP.md +0 -0
- /package/{resources/data/deck/learnneo/pages → learn}/tutorials/TodoList.md +0 -0
- /package/resources/data/{deck/learnneo/data/theBeatles.json → theBeatles.json} +0 -0
@@ -0,0 +1,748 @@
|
|
1
|
+
## A Comprehensive Guide to Custom Component Development
|
2
|
+
|
3
|
+
**Target Audience**: Developers building custom Neo.mjs components who need to work directly with the VDom layer for performance optimization, complex animations, or advanced UI patterns.
|
4
|
+
|
5
|
+
**Prerequisites**: Understanding of Neo.mjs's two-tier architecture (Component Tree vs VDom). Read the "Declarative Component Trees vs Imperative VDom" guide first.
|
6
|
+
|
7
|
+
## Overview
|
8
|
+
|
9
|
+
While 99% of Neo.mjs development happens at the Component Tree layer, creating custom components requires working with the VDom layer. This guide covers the patterns, best practices, and techniques for effective VDom manipulation in Neo.mjs.
|
10
|
+
|
11
|
+
## VDom Fundamentals
|
12
|
+
|
13
|
+
### VDom Structure
|
14
|
+
|
15
|
+
Neo.mjs VDom nodes are plain JavaScript objects that represent DOM elements.
|
16
|
+
**Important**: VDom only contains structure, styling, content, and attributes - **never event listeners**.
|
17
|
+
|
18
|
+
```javascript
|
19
|
+
// Basic VDom node structure
|
20
|
+
{
|
21
|
+
tag : 'div', // HTML tag (default: 'div')
|
22
|
+
id : 'unique-id', // DOM element ID
|
23
|
+
cls : ['class1', 'class2'], // CSS classes array
|
24
|
+
style : {color: 'red'}, // Inline CSS object
|
25
|
+
html : 'Text content', // Inner HTML (exclusive with text/cn)
|
26
|
+
text : 'Text content', // Plain text content (exclusive with html/cn)
|
27
|
+
cn : [], // Child nodes array (exclusive with html/text)
|
28
|
+
vtype : 'vnode', // VNode type: 'vnode', 'text', 'root'
|
29
|
+
static : false, // Exclude from delta updates (optimization)
|
30
|
+
removeDom: false, // Hide/show element
|
31
|
+
data : {custom: 'value'}, // data-* attributes
|
32
|
+
// Standard HTML attributes
|
33
|
+
disabled: true,
|
34
|
+
tabIndex: -1,
|
35
|
+
role : 'button'
|
36
|
+
// ❌ NO EVENT LISTENERS IN VDOM
|
37
|
+
}
|
38
|
+
```
|
39
|
+
|
40
|
+
### Component VDom Definition
|
41
|
+
|
42
|
+
Components define their internal DOM structure via the `vdom` config:
|
43
|
+
|
44
|
+
```javascript
|
45
|
+
import Component from './src/component/Base.mjs';
|
46
|
+
|
47
|
+
class CustomButton extends Component {
|
48
|
+
static config = {
|
49
|
+
className: 'Neo.custom.Button',
|
50
|
+
ntype : 'custom-button',
|
51
|
+
|
52
|
+
// Define internal DOM structure for this component
|
53
|
+
vdom:
|
54
|
+
{cls: ['neo-button', 'neo-custom-button'], cn: [
|
55
|
+
{tag: 'span', cls: ['neo-button-icon'], flag: 'iconNode'}, // Flag for easy access
|
56
|
+
{tag: 'span', cls: ['neo-button-text'], flag: 'textNode'},
|
57
|
+
{cls: ['neo-button-badge'], flag: 'badgeNode', removeDom: true} // Initially hidden badge
|
58
|
+
]},
|
59
|
+
|
60
|
+
// DOM event listeners are defined in static config, not VDom
|
61
|
+
domListeners: [{
|
62
|
+
click: 'onButtonClick'
|
63
|
+
}]
|
64
|
+
}
|
65
|
+
|
66
|
+
onButtonClick(data) {
|
67
|
+
console.log('CustomButton clicked:', data);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
---
|
73
|
+
|
74
|
+
## Connecting VDom to User Interaction (DOM Events)
|
75
|
+
|
76
|
+
While VDom nodes define the visual structure and attributes of your components, they do **not** contain event listeners. Neo.mjs uses a robust, delegated global DOM event system where listeners are defined separately within your component's `domListeners` static config.
|
77
|
+
|
78
|
+
This clear separation ensures that your VDom markup remains easily serializable and performant across worker threads, while still providing flexible and powerful event handling.
|
79
|
+
|
80
|
+
For a comprehensive deep dive into all aspects of DOM event handling in Neo.mjs (including static, programmatic, string-based handlers, delegation, bubbling, and more), please refer to the dedicated **[DOM Event Handling Guide](guides.events.DomEvents)**.
|
81
|
+
|
82
|
+
Here's a simple example of how an event handler defined via `domListeners` would interact with a component's VDom:
|
83
|
+
|
84
|
+
```javascript
|
85
|
+
import Component from './src/component/Base.mjs';
|
86
|
+
import VdomUtil from './src/util/Vdom.mjs'; // For accessing VDom nodes by flag
|
87
|
+
|
88
|
+
class InteractiveComponent extends Component {
|
89
|
+
static config = {
|
90
|
+
className: 'Neo.examples.InteractiveComponent',
|
91
|
+
|
92
|
+
vdom: {
|
93
|
+
cls: ['neo-interactive-box'],
|
94
|
+
cn: [
|
95
|
+
{tag: 'button', text: 'Click Me', cls: ['neo-button'], flag: 'myButton'},
|
96
|
+
{tag: 'div', text: 'Hover over me', cls: ['hoverable-area'], flag: 'hoverArea'}
|
97
|
+
]
|
98
|
+
},
|
99
|
+
|
100
|
+
domListeners: [{
|
101
|
+
click : 'onButtonClick',
|
102
|
+
delegate: '.neo-button' // Event delegated to elements with this class
|
103
|
+
}, {
|
104
|
+
mouseenter: 'onHoverAreaEnter',
|
105
|
+
mouseleave: 'onHoverAreaLeave',
|
106
|
+
delegate : '.hoverable-area'
|
107
|
+
}]
|
108
|
+
}
|
109
|
+
|
110
|
+
// Access VDom nodes using VdomUtil (or flags in afterSet methods)
|
111
|
+
get myButton() {
|
112
|
+
return VdomUtil.getByFlag(this, 'myButton');
|
113
|
+
}
|
114
|
+
|
115
|
+
get hoverArea() {
|
116
|
+
return VdomUtil.getByFlag(this, 'hoverArea');
|
117
|
+
}
|
118
|
+
|
119
|
+
onButtonClick(data) {
|
120
|
+
console.log('Button clicked:', data.component.id);
|
121
|
+
// Imperatively modify VDom properties in response to event
|
122
|
+
this.myButton.text = 'Clicked!';
|
123
|
+
this.myButton.disabled = true;
|
124
|
+
this.update(); // Trigger VDom reconciliation
|
125
|
+
}
|
126
|
+
|
127
|
+
onHoverAreaEnter(data) {
|
128
|
+
this.hoverArea.style = {backgroundColor: '#f0f0f0'};
|
129
|
+
this.update();
|
130
|
+
}
|
131
|
+
|
132
|
+
onHoverAreaLeave(data) {
|
133
|
+
this.hoverArea.style = {}; // Reset style
|
134
|
+
this.update();
|
135
|
+
}
|
136
|
+
}
|
137
|
+
```
|
138
|
+
|
139
|
+
---
|
140
|
+
|
141
|
+
## VDom Update Mechanisms
|
142
|
+
|
143
|
+
### Standard Approach: `this.update()`
|
144
|
+
|
145
|
+
The typical way to sync VDom changes to the DOM is through the component's `update()` method:
|
146
|
+
|
147
|
+
```javascript
|
148
|
+
import Component from './src/component/Base.mjs'; // Required import
|
149
|
+
|
150
|
+
class StandardComponent extends Component {
|
151
|
+
// Assuming these flags point to VDom nodes within this component's vdom config
|
152
|
+
get textNode() { return VdomUtil.getByFlag(this, 'myTextNode'); }
|
153
|
+
get iconNode() { return VdomUtil.getByFlag(this, 'myIconNode'); }
|
154
|
+
|
155
|
+
changeContent() {
|
156
|
+
// Modify VDom structure (properties of VDom nodes accessed via flags or direct object mutation)
|
157
|
+
this.textNode.text = 'New content';
|
158
|
+
this.iconNode.cls = ['fa', 'fa-star'];
|
159
|
+
|
160
|
+
// Send component's VDom to VDom worker via engine
|
161
|
+
this.update() // Engine calculates what changed
|
162
|
+
}
|
163
|
+
}
|
164
|
+
```
|
165
|
+
|
166
|
+
**How `this.update()` works:**
|
167
|
+
1. Sends the component's entire VDom tree to the VDom worker.
|
168
|
+
2. VDom worker compares with previous state (diffing).
|
169
|
+
3. Worker calculates minimal deltas needed.
|
170
|
+
4. Deltas are sent to Main thread for efficient DOM updates.
|
171
|
+
|
172
|
+
**Use `this.update()` when:**
|
173
|
+
* Making standard component updates.
|
174
|
+
* You want the framework's VDom engine to handle diffing automatically.
|
175
|
+
* Working with complex VDom structures where manual delta calculation would be error-prone.
|
176
|
+
* This covers ~95% of VDom manipulation use cases.
|
177
|
+
|
178
|
+
### Advanced Approach: `Neo.applyDeltas()`
|
179
|
+
|
180
|
+
For performance-critical scenarios, you can bypass the VDom worker's diffing engine and send manually crafted deltas
|
181
|
+
directly from the App Worker to the Main Thread. This offers precise control but requires careful manual delta construction.
|
182
|
+
|
183
|
+
```javascript
|
184
|
+
import Component from './src/component/Base.mjs'; // Required import
|
185
|
+
|
186
|
+
class AdvancedComponent extends Component {
|
187
|
+
// Assume getItemId method exists to get the VDom ID of an element
|
188
|
+
// within the component's vdom structure.
|
189
|
+
|
190
|
+
optimizedUpdate() {
|
191
|
+
let me = this;
|
192
|
+
// Manually craft precise deltas. Each delta targets a VDom node by its ID.
|
193
|
+
const deltas = [{
|
194
|
+
id : me.getItemId('my-item-id-1'), // Unique VDom ID of the target element
|
195
|
+
style: {opacity: 0.5, backgroundColor: 'blue'}
|
196
|
+
}, {
|
197
|
+
id : me.getItemId('my-item-id-2'),
|
198
|
+
cls: {add: ['highlight'], remove: ['normal']}
|
199
|
+
}, {
|
200
|
+
id : me.getItemId('my-text-node-id'),
|
201
|
+
text: 'Updated text via direct delta'
|
202
|
+
}];
|
203
|
+
|
204
|
+
// Apply deltas directly to the Main Thread.
|
205
|
+
// This bypasses the VDom worker's diffing process and worker roundtrip.
|
206
|
+
Neo.applyDeltas(me.appName, deltas)
|
207
|
+
}
|
208
|
+
}
|
209
|
+
```
|
210
|
+
|
211
|
+
**How `Neo.applyDeltas()` works:**
|
212
|
+
1. You explicitly construct an array of delta objects (e.g., specific changes to style, classes, or text for targeted VDom nodes).
|
213
|
+
2. These pre-calculated deltas are sent directly from the App Worker to the Main Thread.
|
214
|
+
3. The Main Thread immediately applies these changes to the DOM, without any prior diffing or processing by the VDom worker.
|
215
|
+
|
216
|
+
**When to use `Neo.applyDeltas()` (Primary use cases):**
|
217
|
+
* **Extreme Transient Animations**: For highly frequent, localized visual updates (e.g., continuous animations of 300-600 items' transforms, reaching ~40,000 real DOM updates per second via mousewheel events). Here, the benefit of bypassing diffing and worker roundtrips is paramount.
|
218
|
+
* **Buffered Rendering / Virtualized Lists**: For scenarios like virtualized lists or data grids where elements are precisely manipulated (e.g., `shift`/`unshift` operations to recycle DOM nodes). This allows you to generate a single `move` delta directly, completely avoiding the overhead of full tree parsing and worker roundtrips that the standard VDom diffing process would incur.
|
219
|
+
* **Precise Control**: When you have full knowledge of the exact DOM changes needed and require ultimate control over the update timing.
|
220
|
+
|
221
|
+
**Important Limitations and Considerations:**
|
222
|
+
* **State Desynchronization (Critical)**: `Neo.applyDeltas()` modifies the DOM but **does not** automatically update the
|
223
|
+
component's internal VDom (`this.vdom`, your desired state) or the framework's cached `VNode` state (`this.vnode`, the
|
224
|
+
last state known by the VDom engine).
|
225
|
+
* If these internal states are not manually synchronized, subsequent standard `this.update()` calls will compare against
|
226
|
+
stale data, leading to **redundant, incorrect, or "undoing" deltas** being sent to the Main Thread.
|
227
|
+
* **Manual Synchronization for Persistent Changes (Highly Complex)**: For persistent UI changes that must remain synchronized,
|
228
|
+
manually updating both `this.vdom` (to reflect the new visual state) and `this.vnode` (to match the exact state after
|
229
|
+
`Neo.applyDeltas()`) is required. This low-level `VNode` manipulation is generally considered a **framework-internal task**
|
230
|
+
dueled to its complexity and the specific `Neo.vdom.VNode` class structure. It is **not typically recommended for
|
231
|
+
application developers** for general UI management.
|
232
|
+
* **No Automatic Diffing**: You are entirely responsible for calculating the precise deltas. Errors in delta calculation
|
233
|
+
will lead to incorrect or unexpected DOM behavior.
|
234
|
+
|
235
|
+
---
|
236
|
+
|
237
|
+
## VDom Manipulation Patterns
|
238
|
+
|
239
|
+
### 1. Using Flag-Based References
|
240
|
+
|
241
|
+
Flags provide efficient, direct access to specific VDom nodes within a component's `vdom` structure, avoiding the need for DOM queries.
|
242
|
+
|
243
|
+
```javascript
|
244
|
+
import Component from './src/component/Base.mjs';
|
245
|
+
import VdomUtil from './src/util/Vdom.mjs'; // Required import for VdomUtil
|
246
|
+
import NeoArray from './src/util/Array.mjs'; // Required import for NeoArray
|
247
|
+
|
248
|
+
class IconButton extends Component {
|
249
|
+
static config = {
|
250
|
+
vdom:
|
251
|
+
{cls: ['neo-icon-button'], cn: [
|
252
|
+
{tag: 'i', cls: ['neo-icon'], flag: 'iconNode'},
|
253
|
+
{tag: 'span', cls: ['neo-text'], flag: 'textNode'}
|
254
|
+
]},
|
255
|
+
|
256
|
+
// domListeners are included here for context, but their detailed explanation is in the dedicated guide.
|
257
|
+
domListeners: [{
|
258
|
+
click: 'onClick'
|
259
|
+
}]
|
260
|
+
}
|
261
|
+
|
262
|
+
// Define getters for easy access to flagged VDom nodes
|
263
|
+
get iconNode() {
|
264
|
+
return VdomUtil.getByFlag(this, 'iconNode');
|
265
|
+
}
|
266
|
+
|
267
|
+
get textNode() {
|
268
|
+
return VdomUtil.getByFlag(this, 'textNode');
|
269
|
+
}
|
270
|
+
|
271
|
+
// Manipulate VDom nodes in response to config changes
|
272
|
+
afterSetIconCls(value, oldValue) {
|
273
|
+
let {iconNode} = this;
|
274
|
+
|
275
|
+
// Update CSS classes imperatively
|
276
|
+
NeoArray.remove(iconNode.cls, oldValue);
|
277
|
+
NeoArray.add(iconNode.cls, value);
|
278
|
+
|
279
|
+
// Hide/show icon based on value
|
280
|
+
iconNode.removeDom = !value;
|
281
|
+
|
282
|
+
this.update();
|
283
|
+
}
|
284
|
+
|
285
|
+
afterSetText(value, oldValue) {
|
286
|
+
let {textNode} = this;
|
287
|
+
|
288
|
+
textNode.text = value;
|
289
|
+
textNode.removeDom = !value;
|
290
|
+
|
291
|
+
this.update();
|
292
|
+
}
|
293
|
+
|
294
|
+
onClick(data) {
|
295
|
+
this.fire('buttonClick', {
|
296
|
+
iconCls: this.iconCls,
|
297
|
+
text : this.text
|
298
|
+
});
|
299
|
+
}
|
300
|
+
}
|
301
|
+
```
|
302
|
+
|
303
|
+
### 2. Dynamic VDom Creation (List Rendering)
|
304
|
+
|
305
|
+
Build VDom structures programmatically, often in response to data changes. This is common for lists or complex, data-driven UI fragments.
|
306
|
+
|
307
|
+
```javascript
|
308
|
+
import Component from './src/component/Base.mjs'; // Required import
|
309
|
+
|
310
|
+
class DataList extends Component {
|
311
|
+
static config = {
|
312
|
+
data: null, // Reactive config to hold list data
|
313
|
+
|
314
|
+
vdom: {
|
315
|
+
cls: ['neo-data-list'],
|
316
|
+
cn : [] // Will be populated dynamically by createListItems
|
317
|
+
},
|
318
|
+
|
319
|
+
// domListeners are included for context
|
320
|
+
domListeners: [
|
321
|
+
{ click: 'onItemClick', delegate: '.neo-list-item' },
|
322
|
+
{ click: 'onAvatarClick', delegate: '.neo-avatar' }
|
323
|
+
]
|
324
|
+
}
|
325
|
+
|
326
|
+
// Create VDom items from component's data config
|
327
|
+
createListItems() {
|
328
|
+
let {data, vdom} = this,
|
329
|
+
items = [];
|
330
|
+
|
331
|
+
// Ensure data exists before mapping
|
332
|
+
if (Array.isArray(data)) {
|
333
|
+
data.forEach((record) => {
|
334
|
+
items.push({
|
335
|
+
cls: ['neo-list-item'],
|
336
|
+
data: {recordId: record.id}, // Use data attributes for event identification
|
337
|
+
cn: [{
|
338
|
+
tag: 'img',
|
339
|
+
src: record.avatar,
|
340
|
+
cls: ['neo-avatar']
|
341
|
+
}, {
|
342
|
+
cls: ['neo-content'],
|
343
|
+
cn: [
|
344
|
+
{tag: 'h3', text: record.name},
|
345
|
+
{tag: 'p', text: record.description}
|
346
|
+
]
|
347
|
+
}]
|
348
|
+
})
|
349
|
+
})
|
350
|
+
}
|
351
|
+
|
352
|
+
vdom.cn = items; // Assign new VDom children
|
353
|
+
this.update(); // Trigger VDom reconciliation
|
354
|
+
}
|
355
|
+
|
356
|
+
// Automatically re-render list when 'data' config changes
|
357
|
+
afterSetData(value, oldValue) {
|
358
|
+
if (value) { // Only create items if data is set
|
359
|
+
this.createListItems();
|
360
|
+
}
|
361
|
+
}
|
362
|
+
|
363
|
+
onItemClick(data) {
|
364
|
+
let recordId = data.target.closest('.neo-list-item')?.dataset.recordId; // Use optional chaining for safety
|
365
|
+
if (recordId) {
|
366
|
+
this.fire('itemSelect', {recordId});
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
370
|
+
onAvatarClick(data) {
|
371
|
+
let recordId = data.target.closest('.neo-list-item')?.dataset.recordId; // Use optional chaining
|
372
|
+
if (recordId) {
|
373
|
+
this.fire('avatarClick', {recordId});
|
374
|
+
}
|
375
|
+
}
|
376
|
+
}
|
377
|
+
```
|
378
|
+
|
379
|
+
### 3. Complex VDom Transformations (e.g., 3D Animations)
|
380
|
+
|
381
|
+
For sophisticated UI patterns like 3D visualizations or complex dynamic layouts, you might imperatively calculate and apply VDom properties or even use `Neo.applyDeltas()` for maximum performance.
|
382
|
+
|
383
|
+
```javascript
|
384
|
+
import Component from './src/component/Base.mjs'; // Base component class
|
385
|
+
|
386
|
+
class Helix extends Component {
|
387
|
+
/**
|
388
|
+
* This method demonstrates complex VDom transformations by
|
389
|
+
* programmatically building VDom deltas and applying them directly
|
390
|
+
* using `Neo.applyDeltas()`.
|
391
|
+
*
|
392
|
+
* In a real application, the values for 'items', 'rotationAngle', etc.,
|
393
|
+
* would typically come from component configs, a store, or user interaction
|
394
|
+
* (e.g., via event handlers, which are covered in a separate guide).
|
395
|
+
*/
|
396
|
+
transformHelixItems() {
|
397
|
+
let me = this;
|
398
|
+
let deltas = [];
|
399
|
+
|
400
|
+
// --- Simulate input data and transformation parameters for the example ---
|
401
|
+
const simulatedItems = [
|
402
|
+
{ id: 'itemA', name: 'Helix Item 1' },
|
403
|
+
{ id: 'itemB', name: 'Helix Item 2' },
|
404
|
+
{ id: 'itemC', name: 'Helix Item 3' },
|
405
|
+
{ id: 'itemD', name: 'Helix Item 4' },
|
406
|
+
{ id: 'itemE', name: 'Helix Item 5' }
|
407
|
+
];
|
408
|
+
const baseRotation = 0; // Degrees
|
409
|
+
const itemAngleIncrement = 72; // Degrees per item (360 / 5 items)
|
410
|
+
const helixRadius = 150; // Pixels
|
411
|
+
const verticalSpacing = 40; // Pixels per item
|
412
|
+
|
413
|
+
// --- Core VDom Delta Calculation Loop ---
|
414
|
+
for (let i = 0; i < simulatedItems.length; i++) {
|
415
|
+
let item = simulatedItems[i];
|
416
|
+
let currentAngle = baseRotation + i * itemAngleIncrement; // Angle for this item
|
417
|
+
|
418
|
+
// Basic 3D-like position calculation for CSS `transform`
|
419
|
+
let x = helixRadius * Math.sin(currentAngle * Math.PI / 180);
|
420
|
+
let y = i * verticalSpacing; // Stack vertically
|
421
|
+
let z = helixRadius * Math.cos(currentAngle * Math.PI / 180); // For depth effect
|
422
|
+
|
423
|
+
// Construct the CSS 3D transform string
|
424
|
+
let transformStyle = `translate3d(${x}px, ${y}px, ${z}px) rotateY(${currentAngle}deg)`;
|
425
|
+
|
426
|
+
// Calculate opacity based on depth (cosine of angle)
|
427
|
+
let opacity = (Math.cos(currentAngle * Math.PI / 180) + 1) / 2;
|
428
|
+
|
429
|
+
// Push a delta object for this specific VDom node.
|
430
|
+
// The 'id' here must correspond to the actual VDom node's ID in the Main Thread.
|
431
|
+
// This example assumes VDom nodes for helix items already exist or will be created
|
432
|
+
// by a separate render cycle; 'Neo.applyDeltas' is for *updating* existing nodes.
|
433
|
+
deltas.push({
|
434
|
+
id : `${me.id}-helix-element-${item.id}`, // Example ID convention
|
435
|
+
style: { opacity: opacity, transform: transformStyle },
|
436
|
+
text : item.name // Example: update text content
|
437
|
+
})
|
438
|
+
}
|
439
|
+
|
440
|
+
// --- ADVANCED: Directly apply calculated deltas to the DOM ---
|
441
|
+
// This bypasses the VDom worker's diffing engine, sending changes
|
442
|
+
// directly from the App Worker to the Main Thread for immediate application.
|
443
|
+
// It's used for scenarios requiring extreme performance or precise control.
|
444
|
+
Neo.applyDeltas(me.appName, deltas); // 'Neo' is globally available
|
445
|
+
}
|
446
|
+
}
|
447
|
+
```
|
448
|
+
|
449
|
+
---
|
450
|
+
|
451
|
+
## Security Considerations
|
452
|
+
|
453
|
+
### XSS Prevention
|
454
|
+
|
455
|
+
```javascript
|
456
|
+
import Component from './src/component/Base.mjs'; // Required import
|
457
|
+
// import DOMPurify from 'dompurify'; // Example for external sanitization library
|
458
|
+
|
459
|
+
class SecureComponent extends Component {
|
460
|
+
// SECURE: Use text property for user-provided string content
|
461
|
+
setContent(userInput) {
|
462
|
+
this.textNode.text = userInput; // Automatically HTML-escaped by the framework
|
463
|
+
this.update();
|
464
|
+
}
|
465
|
+
|
466
|
+
// SECURE: Use 'tag' property for creating elements with custom names
|
467
|
+
createElement(tagName) {
|
468
|
+
return {
|
469
|
+
tag: tagName, // Safe element creation: tagName is treated as a literal tag name
|
470
|
+
cls: ['user-element']
|
471
|
+
};
|
472
|
+
}
|
473
|
+
|
474
|
+
// AVOID: Direct HTML injection using 'html' property without sanitization
|
475
|
+
unsafeSetContent(userInput) {
|
476
|
+
// This example shows a VDom node named 'containerNode' within the component's vdom.
|
477
|
+
// It directly sets innerHTML, which is a high XSS risk if 'userInput' is not sanitized.
|
478
|
+
this.containerNode.html = userInput; // XSS risk if userInput is untrusted!
|
479
|
+
this.update(); // Don't forget to update!
|
480
|
+
}
|
481
|
+
|
482
|
+
// SECURE: Validate and sanitize if HTML content is absolutely needed
|
483
|
+
setSafeHtml(content) {
|
484
|
+
// If an external library like DOMPurify is used, ensure it's imported and available.
|
485
|
+
// For simplicity, this example just shows the call.
|
486
|
+
// let sanitized = DOMPurify.sanitize(content);
|
487
|
+
// this.containerNode.html = sanitized;
|
488
|
+
// this.update();
|
489
|
+
console.warn('DOMPurify or similar sanitization library should be used here for user-provided HTML.');
|
490
|
+
this.containerNode.html = content; // Still unsafe without actual sanitization
|
491
|
+
this.update();
|
492
|
+
}
|
493
|
+
}
|
494
|
+
```
|
495
|
+
|
496
|
+
---
|
497
|
+
|
498
|
+
## Performance Best Practices
|
499
|
+
|
500
|
+
### 1. Batch VDom Updates
|
501
|
+
|
502
|
+
```javascript
|
503
|
+
import Component from './src/component/Base.mjs'; // Required import
|
504
|
+
import Neo from './src/Neo.mjs'; // Required import for Neo.applyDeltas
|
505
|
+
|
506
|
+
class PerformantComponent extends Component {
|
507
|
+
// Assume getItemNode and getItemId methods exist to access VDom nodes/IDs
|
508
|
+
|
509
|
+
// BAD: Multiple individual updates
|
510
|
+
updateItemsBad(items) {
|
511
|
+
items.forEach(item => {
|
512
|
+
let node = this.getItemNode(item.id); // Get VDom node
|
513
|
+
if (node) { // Ensure node exists
|
514
|
+
node.text = item.name;
|
515
|
+
}
|
516
|
+
this.update(); // Too many VDom worker round-trips!
|
517
|
+
});
|
518
|
+
}
|
519
|
+
|
520
|
+
// GOOD: Single engine update with all changes
|
521
|
+
updateItemsGood(items) {
|
522
|
+
items.forEach(item => {
|
523
|
+
let node = this.getItemNode(item.id); // Get VDom node
|
524
|
+
if (node) {
|
525
|
+
node.text = item.name;
|
526
|
+
}
|
527
|
+
});
|
528
|
+
|
529
|
+
this.update(); // Single VDom diff operation, more efficient
|
530
|
+
}
|
531
|
+
|
532
|
+
// ADVANCED: Bypass engine entirely with manual deltas
|
533
|
+
updateItemsAdvanced(items) {
|
534
|
+
let deltas = [];
|
535
|
+
|
536
|
+
items.forEach(item => {
|
537
|
+
deltas.push({
|
538
|
+
id: this.getItemId(item.id), // Get VDom node ID
|
539
|
+
text: item.name
|
540
|
+
});
|
541
|
+
});
|
542
|
+
|
543
|
+
Neo.applyDeltas(this.appName, deltas); // Directly send pre-calculated deltas to Main Thread
|
544
|
+
}
|
545
|
+
}
|
546
|
+
```
|
547
|
+
|
548
|
+
### 2. Efficient Event Delegation
|
549
|
+
|
550
|
+
```javascript
|
551
|
+
import Component from './src/component/Base.mjs'; // Required import
|
552
|
+
|
553
|
+
class EfficientEventComponent extends Component {
|
554
|
+
construct(config) {
|
555
|
+
super.construct(config);
|
556
|
+
|
557
|
+
// Single delegated listener handles multiple item types
|
558
|
+
this.addDomListeners({
|
559
|
+
click: this.onItemInteraction,
|
560
|
+
delegate: '.interactive-item',
|
561
|
+
scope: this
|
562
|
+
});
|
563
|
+
}
|
564
|
+
|
565
|
+
onItemInteraction(data) {
|
566
|
+
// data.target is a proxy for the DOM element, allowing direct DOM-like calls
|
567
|
+
let element = data.target.closest('.interactive-item');
|
568
|
+
if (!element) { return; } // Safety check
|
569
|
+
|
570
|
+
let itemType = element.dataset.itemType;
|
571
|
+
let itemId = element.dataset.itemId;
|
572
|
+
|
573
|
+
// Route based on item type
|
574
|
+
switch (itemType) {
|
575
|
+
case 'button':
|
576
|
+
this.handleButtonClick(itemId, data);
|
577
|
+
break;
|
578
|
+
case 'card':
|
579
|
+
this.handleCardClick(itemId, data);
|
580
|
+
break;
|
581
|
+
case 'menu-item':
|
582
|
+
this.handleMenuClick(itemId, data);
|
583
|
+
break;
|
584
|
+
default:
|
585
|
+
console.warn('Unknown interactive item type:', itemType);
|
586
|
+
}
|
587
|
+
}
|
588
|
+
|
589
|
+
// Example handlers
|
590
|
+
handleButtonClick(itemId, data) { console.log('Button clicked:', itemId); }
|
591
|
+
handleCardClick(itemId, data) { console.log('Card clicked:', itemId); }
|
592
|
+
handleMenuClick(itemId, data) { console.log('Menu item clicked:', itemId); }
|
593
|
+
}
|
594
|
+
```
|
595
|
+
|
596
|
+
### 3. Memory Management
|
597
|
+
|
598
|
+
```javascript
|
599
|
+
import Component from './src/component/Base.mjs'; // Required import
|
600
|
+
|
601
|
+
class MemoryEfficientComponent extends Component {
|
602
|
+
destroy(...args) {
|
603
|
+
// Clean up internal VDom-related references if they are not automatically managed
|
604
|
+
this._cachedNodes = null;
|
605
|
+
|
606
|
+
// Clear any pending timeouts or intervals to prevent leaks
|
607
|
+
this.transitionTimeouts?.forEach(clearTimeout);
|
608
|
+
this.transitionTimeouts = null;
|
609
|
+
|
610
|
+
// Call super.destroy() last
|
611
|
+
super.destroy(...args);
|
612
|
+
}
|
613
|
+
|
614
|
+
// Avoid memory leaks in event handlers by binding scope correctly or using fat arrows
|
615
|
+
createEventHandler(itemId) {
|
616
|
+
// GOOD: Minimal closure scope, 'this' context handled by 'bind'
|
617
|
+
// This is often for passing handlers to other parts of the app, not for domListeners config
|
618
|
+
return this.processItem.bind(this, itemId);
|
619
|
+
}
|
620
|
+
|
621
|
+
processItem(itemId) {
|
622
|
+
console.log('Processing item:', itemId);
|
623
|
+
}
|
624
|
+
}
|
625
|
+
```
|
626
|
+
|
627
|
+
---
|
628
|
+
|
629
|
+
## Common VDom Patterns
|
630
|
+
|
631
|
+
### 1. Conditional Rendering
|
632
|
+
|
633
|
+
Dynamically show or hide VDom nodes by setting their `removeDom` property. This is efficient as the VDom node remains in the tree, but its corresponding DOM element is removed/added from the document flow by the framework.
|
634
|
+
|
635
|
+
```javascript
|
636
|
+
import Component from './src/component/Base.mjs'; // Required import
|
637
|
+
import VdomUtil from './src/util/Vdom.mjs'; // Required import
|
638
|
+
|
639
|
+
class ConditionalComponent extends Component {
|
640
|
+
static config = {
|
641
|
+
vdom:
|
642
|
+
{cn: [
|
643
|
+
{tag: 'div', flag: 'contentNode', text: 'Main Content'},
|
644
|
+
{tag: 'div', flag: 'loadingNode', text: 'Loading...', removeDom: true} // Initially hidden
|
645
|
+
]}
|
646
|
+
}
|
647
|
+
|
648
|
+
// Access VDom nodes by flag
|
649
|
+
get contentNode() { return VdomUtil.getByFlag(this, 'contentNode'); }
|
650
|
+
get loadingNode() { return VdomUtil.getByFlag(this, 'loadingNode'); }
|
651
|
+
|
652
|
+
toggleVisibility(nodeFlag, visible) {
|
653
|
+
let node = VdomUtil.getByFlag(this, nodeFlag);
|
654
|
+
node.removeDom = !visible; // true to hide, false to show
|
655
|
+
this.update(); // STANDARD: Let engine handle the DOM update
|
656
|
+
}
|
657
|
+
|
658
|
+
showLoadingState() {
|
659
|
+
this.contentNode.removeDom = true;
|
660
|
+
this.loadingNode.removeDom = false;
|
661
|
+
this.update(); // STANDARD: Single engine update for multiple changes
|
662
|
+
}
|
663
|
+
|
664
|
+
showContent() {
|
665
|
+
this.contentNode.removeDom = false;
|
666
|
+
this.loadingNode.removeDom = true;
|
667
|
+
this.update(); // STANDARD: Single engine update
|
668
|
+
}
|
669
|
+
}
|
670
|
+
```
|
671
|
+
|
672
|
+
### 2. List Rendering (Dynamic Children)
|
673
|
+
|
674
|
+
Programmatically create and update lists of VDom nodes, typically from data. This approach is highly efficient as the VDom diffing engine optimizes the DOM updates.
|
675
|
+
|
676
|
+
```javascript
|
677
|
+
import Component from './src/component/Base.mjs'; // Required import
|
678
|
+
|
679
|
+
class ListComponent extends Component {
|
680
|
+
static config = {
|
681
|
+
data: [], // Example: config to hold the list data
|
682
|
+
|
683
|
+
vdom: {
|
684
|
+
cls: ['neo-list'],
|
685
|
+
cn: [] // This array will be populated with VDom nodes dynamically
|
686
|
+
},
|
687
|
+
|
688
|
+
domListeners: [{
|
689
|
+
click : 'onDeleteItem',
|
690
|
+
delegate: '.delete-button' // Delegate click to delete buttons
|
691
|
+
}]
|
692
|
+
}
|
693
|
+
|
694
|
+
// Access the VDom node where list items will be rendered
|
695
|
+
get listNode() {
|
696
|
+
// Assuming the component's root VDom is the list container
|
697
|
+
return this.vdom; // Or VdomUtil.getByFlag(this, 'listContainer') if you have a flag
|
698
|
+
}
|
699
|
+
|
700
|
+
// Method to generate VDom nodes from an array of data
|
701
|
+
renderItems(items) {
|
702
|
+
let listItems = items.map(item => ({
|
703
|
+
cls: ['list-item'],
|
704
|
+
data: {itemId: item.id}, // Use data attributes for identifying the item
|
705
|
+
cn: [
|
706
|
+
{tag: 'span', text: item.name},
|
707
|
+
{tag: 'button', text: 'Delete', cls: ['delete-button'], data: {itemId: item.id}} // Pass itemId to button
|
708
|
+
]
|
709
|
+
}));
|
710
|
+
|
711
|
+
this.listNode.cn = listItems; // Assign new array of VDom children
|
712
|
+
this.update(); // Trigger VDom reconciliation
|
713
|
+
}
|
714
|
+
|
715
|
+
// Example handler for the delegated delete button click
|
716
|
+
onDeleteItem(data) {
|
717
|
+
let itemId = data.target.dataset.itemId; // Get itemId from the clicked button's data attribute
|
718
|
+
console.log('Delete item:', itemId);
|
719
|
+
this.fire('deleteItem', {itemId}); // Fire a component-level event
|
720
|
+
}
|
721
|
+
|
722
|
+
// Example: Trigger list rendering when 'data' config changes
|
723
|
+
afterSetData(value, oldValue) {
|
724
|
+
if (value) {
|
725
|
+
this.renderItems(value);
|
726
|
+
}
|
727
|
+
}
|
728
|
+
}
|
729
|
+
```
|
730
|
+
|
731
|
+
---
|
732
|
+
|
733
|
+
## Conclusion
|
734
|
+
|
735
|
+
Working with VDom in Neo.mjs provides fine-grained control over DOM manipulation while maintaining the framework's performance benefits. Key takeaways:
|
736
|
+
|
737
|
+
* **Separate Concerns**: VDom defines structure/attributes, `domListeners` handle events.
|
738
|
+
* **Use flags for node references**: More efficient than querying the real DOM.
|
739
|
+
* **Batch VDom updates**: Minimize DOM operations with `this.update()` or `Neo.applyDeltas()`.
|
740
|
+
* **Leverage `this.update()`**: Essential for VDom-to-DOM synchronization, letting the framework optimize diffing.
|
741
|
+
* **`Neo.applyDeltas()` for advanced cases**: Bypass diffing for extreme performance needs.
|
742
|
+
* **Follow security best practices**: Use `text` over `html` (unless sanitized), use `tag` for safe element creation.
|
743
|
+
* **Optimize for performance**: Batch updates, use efficient delegation, manage memory.
|
744
|
+
* **Test thoroughly**: VDom logic benefits from comprehensive testing due to its low-level nature.
|
745
|
+
|
746
|
+
The VDom layer is where Neo.mjs's performance optimizations happen, and understanding these patterns enables you to build sophisticated, high-performance custom components that integrate seamlessly with the framework's architecture.
|
747
|
+
|
748
|
+
Remember: Most development should happen at the Component Tree layer. Only drop down to VDom manipulation when you need the additional control and performance optimization it provides.
|