neo.mjs 10.1.1 → 10.2.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.2.0.md +34 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/covid/view/country/Gallery.mjs +1 -1
- package/apps/covid/view/country/Helix.mjs +1 -1
- package/apps/covid/view/country/Table.mjs +27 -29
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +12 -0
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/sharedcovid/view/country/Gallery.mjs +1 -1
- package/apps/sharedcovid/view/country/Helix.mjs +1 -1
- package/apps/sharedcovid/view/country/Table.mjs +22 -22
- package/examples/grid/bigData/ControlsContainer.mjs +14 -0
- package/examples/stateProvider/inline/MainContainer.mjs +1 -1
- package/examples/stateProvider/twoWay/MainContainer.mjs +2 -2
- package/examples/treeAccordion/MainContainer.mjs +1 -1
- package/learn/blog/v10-deep-dive-functional-components.md +107 -97
- package/learn/blog/v10-deep-dive-reactivity.md +3 -3
- package/learn/blog/v10-deep-dive-state-provider.md +42 -137
- package/learn/blog/v10-deep-dive-vdom-revolution.md +35 -61
- package/learn/blog/v10-post1-love-story.md +3 -3
- package/learn/gettingstarted/DescribingTheUI.md +108 -33
- package/learn/guides/fundamentals/ConfigSystemDeepDive.md +118 -18
- package/learn/guides/fundamentals/InstanceLifecycle.md +121 -84
- package/learn/tree.json +1 -0
- package/learn/tutorials/CreatingAFunctionalButton.md +179 -0
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/data/Store.mjs +8 -3
- package/src/date/SelectorContainer.mjs +2 -2
- package/src/form/field/Base.mjs +15 -1
- package/src/form/field/ComboBox.mjs +5 -15
- package/src/functional/component/Base.mjs +26 -0
- package/src/functional/util/html.mjs +75 -0
- package/src/state/Provider.mjs +7 -4
- package/src/tree/Accordion.mjs +1 -1
- package/test/siesta/siesta.js +8 -1
- package/test/siesta/tests/form/field/AfterSetValueSequence.mjs +106 -0
- package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +92 -0
- package/test/siesta/tests/state/FeedbackLoop.mjs +159 -0
- package/test/siesta/tests/state/Provider.mjs +56 -0
@@ -1,18 +1,17 @@
|
|
1
|
-
#
|
1
|
+
# The VDOM Revolution: How We Render UIs from a Web Worker
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
The Virtual DOM is a cornerstone of modern frontend development. But what happens when you take this concept and move it
|
4
|
+
off the main thread entirely, into a Web Worker? It's a compelling idea—it promises a world where even the most complex
|
5
|
+
UI rendering and diffing can never block user interactions.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
This revolution is built on two pillars:
|
12
|
-
1. **JSON Blueprints:** A more intelligent, efficient language for describing UIs.
|
13
|
-
2. **Asymmetric Rendering:** Using the right tool for the right job—a specialized renderer for initial insertions and a
|
14
|
-
classic diffing engine for updates.
|
7
|
+
But this architectural shift introduces a new set of fascinating engineering challenges. How do you efficiently
|
8
|
+
communicate UI changes from a worker to the main thread? And what's the best language to describe a UI when it's being
|
9
|
+
built by a machine, for a machine?
|
15
10
|
|
11
|
+
This article explores the solutions to those problems, focusing on two key concepts:
|
12
|
+
1. **JSON Blueprints:** Why using structured data is a more powerful way to define complex UIs than traditional HTML.
|
13
|
+
2. **Asymmetric Rendering:** How using different, specialized strategies for creating new UI vs. updating existing UI
|
14
|
+
3. leads to a more performant and secure system.
|
16
15
|
|
17
16
|
*(Part 4 of 5 in the v10 blog series. Details at the bottom.)*
|
18
17
|
|
@@ -27,7 +26,7 @@ and enterprise dashboards—is sending pre-rendered HTML the ultimate endgame?
|
|
27
26
|
We've seen this movie before. In the world of APIs, the verbose, heavyweight XML standard was supplanted by the lighter,
|
28
27
|
simpler, and more machine-friendly JSON. We believe the same evolution is inevitable for defining complex UIs.
|
29
28
|
|
30
|
-
Instead of the server laboring to render and stream HTML, Neo.mjs is built on the principle of **JSON Blueprints**.
|
29
|
+
Instead of the server laboring to render and stream HTML, Neo.mjs is built on the principle of **JSON Blueprints**.
|
31
30
|
The server's job is to provide a compact, structured description of the component tree—its configuration, state, and
|
32
31
|
relationships. Think of it as sending the architectural plans, not pre-fabricated walls.
|
33
32
|
|
@@ -35,14 +34,22 @@ This approach has profound advantages, especially for the AI-driven applications
|
|
35
34
|
|
36
35
|
* **Extreme Data Efficiency:** A JSON blueprint is drastically smaller than its equivalent rendered HTML, minimizing data transfer.
|
37
36
|
* **Server De-Loading:** This offloads rendering stress from the server, freeing it for core application logic and intensive AI computations.
|
38
|
-
* **AI's Native Language:** This is the most critical advantage for the next generation of applications.
|
39
|
-
natural output is structured text. Asking it to generate a valid JSON object that conforms to a component's
|
37
|
+
* **AI's Native Language:** This is the most critical advantage for the next generation of applications.
|
38
|
+
An LLM's natural output is structured text. Asking it to generate a valid JSON object that conforms to a component's
|
40
39
|
configuration is a far more reliable and constrained task than asking it to generate nuanced HTML with embedded logic
|
41
40
|
and styles. The component's config becomes a clean, well-defined API for the AI to target, making UI generation less
|
42
41
|
error-prone and more predictable.
|
43
42
|
* **True Separation of Concerns:** The server provides the "what" (the UI blueprint); the client's worker-based engine
|
44
43
|
expertly handles the "how" (rendering, interactivity, and state management).
|
45
44
|
|
45
|
+
This philosophy—that structured JSON is the future of UI definition—is not just a theoretical concept for us. It
|
46
|
+
is the core engine behind a new tool we are developing: **Neo Studio**. It's a multi-window, browser-based IDE
|
47
|
+
where we're integrating AI to generate component blueprints from natural language. The AI doesn't write JSX; it
|
48
|
+
generates the clean, efficient JSON that the framework then renders into a live UI. It's the first step towards
|
49
|
+
the vision of scaffolding entire applications this way.
|
50
|
+
|
51
|
+
`[Screenshot of the Neo Studio UI, showcasing a generated component from a prompt]`
|
52
|
+
|
46
53
|
JSON blueprints are the language. Now let's look at the engine that translates them into a live application.
|
47
54
|
|
48
55
|
---
|
@@ -65,11 +72,9 @@ from the VDOM worker and uses the right tool for every job.
|
|
65
72
|
|
66
73
|
#### For Creating New DOM: The `DomApiRenderer`
|
67
74
|
|
68
|
-
Whenever a new piece of UI needs to be created, the VDOM worker sends an `insertNode` command.
|
69
|
-
|
70
|
-
-
|
71
|
-
- Or, in a capability that showcases the power of the multi-threaded architecture,
|
72
|
-
move an entire component tree into a **new browser window**.
|
75
|
+
Whenever a new piece of UI needs to be created, the VDOM worker sends an `insertNode` command. This isn't just for the
|
76
|
+
initial page load. It applies any time you dynamically add a new component to a container or, in a capability that
|
77
|
+
showcases the power of the multi-threaded architecture, move an entire component tree into a **new browser window**.
|
73
78
|
|
74
79
|
For all these creation tasks, our pipeline uses the `DomApiRenderer`. This renderer is not only fast but also
|
75
80
|
**secure by default**. It never parses HTML strings, instead building the DOM programmatically with safe APIs like
|
@@ -106,9 +111,9 @@ performance, security, and flexibility.
|
|
106
111
|
The other half of the revolution happens before an update is even sent. It’s about creating the smartest, most minimal
|
107
112
|
blueprint possible.
|
108
113
|
|
109
|
-
In v9, Neo.mjs already had a powerful solution for this: **Scoped VDOM Updates**. Using an `updateDepth` config, a
|
110
|
-
container could intelligently send its own VDOM changes to the worker while treating its children as simple
|
111
|
-
This prevented wasteful VDOM diffing on child components that weren't part of the update.
|
114
|
+
In v9, Neo.mjs already had a powerful solution for this: **Scoped VDOM Updates**. Using an `updateDepth` config, a
|
115
|
+
parent container could intelligently send its own VDOM changes to the worker while treating its children as simple
|
116
|
+
placeholders. This prevented wasteful VDOM diffing on child components that weren't part of the update.
|
112
117
|
|
113
118
|
However, this had a limitation. The `updateDepth` was an "all or nothing" switch for any given level of the component tree.
|
114
119
|
Consider a toolbar with ten buttons. If the toolbar's own structure needed to change *and* just one of those ten buttons
|
@@ -117,14 +122,14 @@ also needed to update, the v9 model wasn't ideal.
|
|
117
122
|
This is the exact challenge that **v10's Asymmetric Blueprints** were designed to solve.
|
118
123
|
|
119
124
|
The new `VDomUpdate` manager and `TreeBuilder` utility work together to create a far more intelligent update payload.
|
120
|
-
When the toolbar and one button need to change, the manager calculates the precise scope. The `TreeBuilder` then
|
121
|
-
a partial VDOM blueprint that includes:
|
125
|
+
When the toolbar and one button need to change, the manager calculates the precise scope. The `TreeBuilder` then
|
126
|
+
generates a partial VDOM blueprint that includes:
|
122
127
|
1. The full VDOM for the toolbar itself.
|
123
128
|
2. The full VDOM for the *one* button that is changing.
|
124
129
|
3. Lightweight `{componentId: 'neo-ignore'}` placeholders for the other nine buttons.
|
125
130
|
|
126
|
-
The VDOM worker receives this highly optimized, asymmetric blueprint. When it sees a `neo-ignore` node,
|
127
|
-
skips diffing that entire branch of the UI.
|
131
|
+
The VDOM worker receives this highly optimized, asymmetric blueprint. When it sees a `neo-ignore` node,
|
132
|
+
it completely skips diffing that entire branch of the UI.
|
128
133
|
|
129
134
|
It’s the ultimate optimization: instead of sending the entire blueprint for a skyscraper just to fix a window,
|
130
135
|
we now send the floor plan for the lobby *and* the specific blueprint for that one window on the 50th floor,
|
@@ -133,37 +138,6 @@ and the framework automatically creates the most efficient update possible.
|
|
133
138
|
|
134
139
|
---
|
135
140
|
|
136
|
-
#### 2. For Updates: From Scoped to Truly Asymmetric Blueprints
|
137
|
-
|
138
|
-
Once a component is on the screen, we need to handle state changes with surgical precision. In v9, Neo.mjs already had
|
139
|
-
a powerful solution for this: **Scoped VDOM Updates**. Using an `updateDepth` config, a parent container could
|
140
|
-
intelligently send its own VDOM changes to the worker while treating its children as simple placeholders. This prevented
|
141
|
-
wasteful VDOM diffing on child components that weren't part of the update.
|
142
|
-
|
143
|
-
However, this had a limitation. The `updateDepth` was an "all or nothing" switch for any given level of the component tree.
|
144
|
-
Consider a toolbar with ten buttons. If the toolbar's own structure needed to change *and* just one of those ten buttons
|
145
|
-
also needed to update, the v9 model forced a choice: either send two separate, parallel updates (one for the toolbar,
|
146
|
-
one for the button), or have the toolbar update its entire level, including the nine buttons that hadn't changed.
|
147
|
-
|
148
|
-
This is the exact challenge that **v10's Asymmetric Blueprints** were designed to solve.
|
149
|
-
|
150
|
-
The new `VDomUpdate` manager and `TreeBuilder` utility work together to create a far more intelligent update payload.
|
151
|
-
When the toolbar and one button need to change, the manager calculates the precise scope. The `TreeBuilder` then generates
|
152
|
-
a partial VDOM blueprint that includes:
|
153
|
-
1. The full VDOM for the toolbar itself.
|
154
|
-
2. The full VDOM for the *one* button that is changing.
|
155
|
-
3. Lightweight `{componentId: 'neo-ignore'}` placeholders for the other nine buttons.
|
156
|
-
|
157
|
-
The VDOM worker receives this highly optimized, asymmetric blueprint. When it sees a `neo-ignore` node, it completely
|
158
|
-
skips diffing that entire branch of the UI.
|
159
|
-
|
160
|
-
It’s the ultimate optimization: instead of sending the entire blueprint for a skyscraper just to fix a window, we now
|
161
|
-
send the floor plan for the lobby *and* the specific blueprint for that one window on the 50th floor, ignoring everything
|
162
|
-
else in between. The worker focuses only on what matters, resulting in faster diffs and minimal data transfer between
|
163
|
-
threads.
|
164
|
-
|
165
|
-
---
|
166
|
-
|
167
141
|
## Conclusion: An Engine Built for Tomorrow
|
168
142
|
|
169
143
|
The VDOM Revolution in Neo.mjs isn't just a performance enhancement; it's a paradigm shift.
|
@@ -189,6 +163,6 @@ invite you to fall in love with frontend development all over again.
|
|
189
163
|
|
190
164
|
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
191
165
|
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
192
|
-
3. [
|
193
|
-
4.
|
194
|
-
5. [Deep Dive
|
166
|
+
3. [Designing Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
167
|
+
4. The VDOM Revolution: How We Render UIs from a Web Worker
|
168
|
+
5. [Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity](./v10-deep-dive-state-provider.md)
|
@@ -322,9 +322,9 @@ It's time to fall in love with frontend again.
|
|
322
322
|
|
323
323
|
1. A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow
|
324
324
|
2. [Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity](./v10-deep-dive-reactivity.md)
|
325
|
-
3. [
|
326
|
-
4. [
|
327
|
-
5. [Deep Dive
|
325
|
+
3. [Designing Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
326
|
+
4. [The VDOM Revolution: How We Render UIs from a Web Worker](./v10-deep-dive-vdom-revolution.md)
|
327
|
+
5. [Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity](./v10-deep-dive-state-provider.md)
|
328
328
|
|
329
329
|
---
|
330
330
|
|
@@ -1,16 +1,54 @@
|
|
1
1
|
# Describing a View
|
2
2
|
|
3
3
|
A Neo.mjs view is comprised of components and containers. A component is a visual widget, like a button,
|
4
|
-
and a container is a visual collection of components.
|
4
|
+
and a container is a visual collection of components.
|
5
5
|
|
6
|
-
Neo.mjs is declarative, where your code _describes_ or _configures_ the things
|
6
|
+
Neo.mjs is declarative, where your code _describes_ or _configures_ the things it's creating.
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
use to describe the component you're creating> You can also access or set the properties dynamically.
|
8
|
+
There are two primary ways to describe a view: using modern **functional components** or classic **class-based components**.
|
9
|
+
Crucially, these two approaches are fully interoperable, allowing you to mix and match them to fit your needs.
|
11
10
|
|
11
|
+
## The Modern Approach: Functional Components
|
12
12
|
|
13
|
-
|
13
|
+
For most new views, especially those that are primarily presentational (like a button or a hero section), the recommended approach is to use functional components. This method is concise, highly performant, and aligns with modern reactive programming patterns.
|
14
|
+
|
15
|
+
Functional components are defined using the `defineComponent` helper. You provide a configuration object that includes a `createVdom` method. This method is a reactive function that returns the Virtual DOM (VDOM) for your component.
|
16
|
+
|
17
|
+
### A simple functional view
|
18
|
+
|
19
|
+
Here is a simple view that displays a single button. The `createVdom` function returns a VDOM object that describes the button.
|
20
|
+
|
21
|
+
```javascript live-preview
|
22
|
+
import {defineComponent} from '../functional/_export.mjs';
|
23
|
+
|
24
|
+
const MainView = defineComponent({
|
25
|
+
className: 'GS.describing.functional.MainView',
|
26
|
+
|
27
|
+
createVdom(config) {
|
28
|
+
return {
|
29
|
+
ntype: 'container',
|
30
|
+
layout: {ntype: 'vbox', align: 'start'},
|
31
|
+
items: [{
|
32
|
+
ntype: 'button',
|
33
|
+
iconCls: 'fa fa-home',
|
34
|
+
text: 'Home'
|
35
|
+
}]
|
36
|
+
}
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
40
|
+
export default MainView;
|
41
|
+
```
|
42
|
+
|
43
|
+
## The Classic Approach: Class-Based Components
|
44
|
+
|
45
|
+
For more complex, high-order components that require surgical precision and powerful state management (like a buffered grid, a calendar, or an image gallery), the classic class-based approach is more suitable.
|
46
|
+
|
47
|
+
This approach involves extending a framework class (like `Neo.container.Base`) and defining your view within the `static config` block. It gives you access to a rich set of lifecycle methods (`beforeSet...`, `afterSet...`, etc.) for fine-grained control.
|
48
|
+
|
49
|
+
### A simple class-based view
|
50
|
+
|
51
|
+
Here is the same view, but built with the class-based approach.
|
14
52
|
|
15
53
|
```javascript live-preview
|
16
54
|
import Button from '../button/Base.mjs';
|
@@ -18,7 +56,7 @@ import Container from '../container/Base.mjs';
|
|
18
56
|
|
19
57
|
class MainView extends Container {
|
20
58
|
static config = {
|
21
|
-
className: 'GS.
|
59
|
+
className: 'GS.describing.class.MainView',
|
22
60
|
layout : {ntype:'vbox', align:'start'},
|
23
61
|
items : [{
|
24
62
|
module : Button,
|
@@ -31,39 +69,71 @@ class MainView extends Container {
|
|
31
69
|
MainView = Neo.setupClass(MainView);
|
32
70
|
```
|
33
71
|
|
72
|
+
## Interoperability: The Best of Both Worlds
|
34
73
|
|
35
|
-
The
|
36
|
-
properties:
|
74
|
+
The power of Neo.mjs lies in its interoperability layer. You can seamlessly mix both component models.
|
37
75
|
|
38
|
-
|
39
|
-
- `text` is the button's text
|
40
|
-
- `iconCls` is the css class used for the button's icon. Neo.mjs automatically includes Font Awesome,
|
41
|
-
and `fa fa-home` matches a Font Awesome css class.
|
76
|
+
### Using a Class-Based Component in a Functional View
|
42
77
|
|
43
|
-
|
44
|
-
components. Containers have an `items:[]` config, which is an array of the components within the container.
|
45
|
-
Containers also have a `layout` property, which describes how the items are arranged.
|
78
|
+
You can easily instantiate a classic component within the `createVdom` method of a functional component. This is useful when you need to embed a complex, stateful widget inside a simpler, functional layout.
|
46
79
|
|
47
|
-
|
80
|
+
```javascript live-preview
|
81
|
+
import {defineComponent} from '../functional/_export.mjs';
|
82
|
+
import Calendar from '../calendar/Component.mjs'; // A complex, class-based component
|
83
|
+
|
84
|
+
const MainView = defineComponent({
|
85
|
+
className: 'GS.describing.interop.MainView1',
|
86
|
+
|
87
|
+
createVdom(config) {
|
88
|
+
return {
|
89
|
+
ntype: 'container',
|
90
|
+
layout: {ntype: 'vbox', align: 'start'},
|
91
|
+
items: [{
|
92
|
+
ntype: 'component',
|
93
|
+
vdom: {tag: 'h1', html: 'My Functional View'}
|
94
|
+
}, {
|
95
|
+
// Drop the class-based Calendar into our functional view
|
96
|
+
module: Calendar,
|
97
|
+
height: 300,
|
98
|
+
width: 300
|
99
|
+
}]
|
100
|
+
}
|
101
|
+
}
|
102
|
+
});
|
103
|
+
|
104
|
+
export default MainView;
|
105
|
+
```
|
106
|
+
|
107
|
+
### Using a Functional Component in a Class-Based View
|
48
108
|
|
49
|
-
|
109
|
+
Conversely, you can drop a functional component into the `items` array of a classic container. This allows you to compose your complex views from smaller, more manageable functional pieces.
|
50
110
|
|
51
111
|
```javascript live-preview
|
52
|
-
import
|
112
|
+
import {defineComponent} from '../functional/_export.mjs';
|
53
113
|
import Container from '../container/Base.mjs';
|
54
114
|
|
115
|
+
// 1. Define a simple functional component
|
116
|
+
const MyFunctionalButton = defineComponent({
|
117
|
+
className: 'GS.describing.interop.FuncButton',
|
118
|
+
createVdom(config) {
|
119
|
+
return {
|
120
|
+
ntype: 'button',
|
121
|
+
iconCls: config.iconCls,
|
122
|
+
text: config.text
|
123
|
+
}
|
124
|
+
}
|
125
|
+
});
|
126
|
+
|
127
|
+
// 2. Create a class-based MainView
|
55
128
|
class MainView extends Container {
|
56
129
|
static config = {
|
57
|
-
className: 'GS.
|
58
|
-
layout : {ntype:'vbox', align:'start'},
|
130
|
+
className: 'GS.describing.interop.MainView2',
|
131
|
+
layout : {ntype:'vbox', align:'start'},
|
59
132
|
items : [{
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
module : Button,
|
65
|
-
iconCls: 'fa fa-star',
|
66
|
-
text : 'Star'
|
133
|
+
// 3. Use the functional component in the items array
|
134
|
+
module: MyFunctionalButton,
|
135
|
+
iconCls: 'fa fa-rocket',
|
136
|
+
text : 'Launch'
|
67
137
|
}]
|
68
138
|
}
|
69
139
|
}
|
@@ -71,9 +141,14 @@ class MainView extends Container {
|
|
71
141
|
MainView = Neo.setupClass(MainView);
|
72
142
|
```
|
73
143
|
|
74
|
-
|
75
|
-
|
144
|
+
## Choosing Your Approach
|
145
|
+
|
146
|
+
* **Use Functional Components (`defineComponent`) for:**
|
147
|
+
* Simple, reusable components (e.g., buttons, chips, hero sections).
|
148
|
+
* Most of your application's views that are primarily presentational.
|
149
|
+
* When you want a concise, declarative, and highly performant component.
|
76
150
|
|
77
|
-
|
78
|
-
|
79
|
-
|
151
|
+
* **Use Class-Based Components (`class ... extends ...`) for:**
|
152
|
+
* Complex, high-order components requiring surgical precision (e.g., buffered grids, calendars, charts, galleries).
|
153
|
+
* Creating new reusable components that extend the functionality of existing framework classes.
|
154
|
+
* Components with intricate internal logic that benefits from the full range of class lifecycle methods.
|
@@ -4,13 +4,22 @@
|
|
4
4
|
first to understand the foundational concepts and benefits.
|
5
5
|
|
6
6
|
The Neo.mjs class configuration system is a cornerstone of the framework, providing a powerful, declarative, and
|
7
|
-
reactive way to manage the state of your components and classes.
|
8
|
-
|
9
|
-
|
7
|
+
reactive way to manage the state of your components and classes. With the introduction of functional components in v10,
|
8
|
+
this system has evolved into a sophisticated, two-tier reactivity model that combines the robustness of a classic
|
9
|
+
"push" system with the fine-grained efficiency of a modern "pull" system.
|
10
10
|
|
11
|
-
|
11
|
+
This guide will take you on a deep dive into how this hybrid system works, giving you the knowledge to build highly
|
12
|
+
performant and maintainable applications.
|
12
13
|
|
13
|
-
|
14
|
+
## Tier 1: The Classic "Push" System
|
15
|
+
|
16
|
+
The original reactivity model in Neo.mjs is a "push" system. It's imperative, meaning that when you change a value,
|
17
|
+
the system actively "pushes" that change through a series of predefined lifecycle hooks. This system remains a core
|
18
|
+
part of v10 and is the foundation for class-based components.
|
19
|
+
|
20
|
+
### 1. Core Concepts Recap
|
21
|
+
|
22
|
+
At its heart, the push system is built on a few key principles:
|
14
23
|
|
15
24
|
* **`static config` Block:** All configurable properties of a class are declared in a `static config = {}` block.
|
16
25
|
This provides a single, clear source of truth for a class's API.
|
@@ -26,13 +35,13 @@ At its heart, the config system is built on a few key principles:
|
|
26
35
|
automatically runs whenever a specific config property changes, ensuring your UI and application state are always
|
27
36
|
in sync.
|
28
37
|
|
29
|
-
|
38
|
+
### 2. The Internal Mechanics: `set()`, `processConfigs()`, and `configSymbol`
|
30
39
|
|
31
40
|
To truly understand how Neo.mjs handles complex scenarios like simultaneous updates and inter-dependencies, we must
|
32
41
|
look at the internal machinery: the `set()` and `processConfigs()` methods in `Neo.core.Base`, and the special
|
33
42
|
`configSymbol` object.
|
34
43
|
|
35
|
-
|
44
|
+
#### The `set()` Method: Your Gateway to Updates
|
36
45
|
|
37
46
|
The `set()` method is the public interface for changing one or more config properties at once. When you call
|
38
47
|
`this.set({a: 1, b: 2})`, you kick off a carefully orchestrated sequence.
|
@@ -68,7 +77,7 @@ Here’s the breakdown:
|
|
68
77
|
from `configSymbol` to the actual instance properties. The `true` argument (`forceAssign`) is crucial, as we'll
|
69
78
|
see next.
|
70
79
|
|
71
|
-
|
80
|
+
#### The `processConfigs()` Method: The Heart of the Operation
|
72
81
|
|
73
82
|
This internal method iteratively processes the configs stored in `configSymbol`. It's designed as a recursive
|
74
83
|
function to handle the dynamic nature of config processing, where one `afterSet` might trigger another `set()`.
|
@@ -108,7 +117,7 @@ processConfigs(forceAssign=false) {
|
|
108
117
|
has been invoked. This is vital to prevent reprocessing and to mark the config as handled.
|
109
118
|
* **Recursion (E):** The method calls itself to process the next item in `configSymbol` until it's empty.
|
110
119
|
|
111
|
-
|
120
|
+
### 3. Solving the "Circular Reference" Problem
|
112
121
|
|
113
122
|
What happens when two `afterSet` methods depend on each other's properties?
|
114
123
|
|
@@ -174,10 +183,98 @@ Here's the sequence:
|
|
174
183
|
operation. This guarantees that all `afterSet` handlers, regardless of their execution order, operate on the most
|
175
184
|
current and consistent state of all config properties involved in that operation.
|
176
185
|
|
177
|
-
##
|
186
|
+
## Tier 2: The Declarative "Pull" System (v10+)
|
187
|
+
|
188
|
+
With the introduction of functional components, Neo.mjs now includes a "pull" reactivity system. This system is
|
189
|
+
declarative and optimized for fine-grained updates, making it ideal for modern, state-driven UI development. Instead
|
190
|
+
of "pushing" changes through hooks, the system "pulls" data as needed, automatically tracking dependencies and
|
191
|
+
re-running computations only when necessary.
|
192
|
+
|
193
|
+
This tier is powered by three key classes: `Neo.core.Config`, `Neo.core.Effect`, and `EffectManager`.
|
194
|
+
|
195
|
+
### 1. `Neo.core.Config`: The Atomic Unit of State
|
196
|
+
|
197
|
+
A `Neo.core.Config` instance is a lightweight wrapper around a single, reactive piece of data. Think of it as an
|
198
|
+
"atom" of state.
|
199
|
+
|
200
|
+
* **Value Storage:** It holds the current value of a config property.
|
201
|
+
* **Subscription Management:** It maintains a list of subscribers (effects or other logic) that depend on its value.
|
202
|
+
* **Dependency Tracking:** When its `get()` method is called within a reactive context (an "effect"), it registers
|
203
|
+
itself as a dependency of that effect.
|
204
|
+
* **Notification:** When its `set()` method is called and the value changes, it notifies all its subscribers,
|
205
|
+
triggering them to re-run.
|
206
|
+
|
207
|
+
### 2. `Neo.core.Effect`: The Reactive Computation
|
208
|
+
|
209
|
+
An `Neo.core.Effect` represents a reactive computation—a function that depends on one or more `Config` atoms.
|
210
|
+
|
211
|
+
* **Wrapping a Function:** It wraps a function (e.g., a component's rendering logic).
|
212
|
+
* **Automatic Dependency Tracking:** When the effect runs, it automatically detects which `Config` atoms are `get()`
|
213
|
+
inside its function. It then subscribes to them.
|
214
|
+
* **Automatic Re-execution:** If any of its dependencies change (i.e., their `set()` method is called), the effect's
|
215
|
+
function is automatically re-executed, ensuring the computation is always up-to-date.
|
216
|
+
|
217
|
+
### 3. `EffectManager`: The Global Coordinator
|
218
|
+
|
219
|
+
The `EffectManager` is a singleton that orchestrates the entire pull system.
|
220
|
+
|
221
|
+
* **Effect Stack:** It maintains a stack of currently running effects. This is how a `Config` atom knows which effect
|
222
|
+
to register itself with when its `get()` method is called.
|
223
|
+
* **Batching:** It provides `Neo.batch()`, a crucial optimization function. It allows the system to pause effect
|
224
|
+
re-runs, perform multiple state changes, and then resume, running all affected effects only once at the end. This
|
225
|
+
prevents "glitches" and unnecessary intermediate computations.
|
226
|
+
|
227
|
+
### 4. `createVdom` as a Master Effect
|
228
|
+
|
229
|
+
In a functional component, the `createVdom()` method is the perfect example of an effect in action.
|
230
|
+
|
231
|
+
* **The `vdomEffect`:** When a functional component is constructed, the framework automatically wraps its `createVdom()`
|
232
|
+
method in a `Neo.core.Effect`.
|
233
|
+
* **Reading is Subscribing:** When your `createVdom()` function runs, every component config you access (e.g.,
|
234
|
+
`config.text`, `config.items`) is a call to that config's underlying `Neo.core.Config` atom's `get()` method.
|
235
|
+
This automatically subscribes the `vdomEffect` to those configs.
|
236
|
+
* **Automatic UI Updates:** If any of those configs change later, they notify the `vdomEffect`. The effect then
|
237
|
+
re-runs your `createVdom()` function, generating a new virtual DOM based on the new state. The framework then
|
238
|
+
efficiently diffs this new VDOM with the old one and applies the minimal necessary changes to the actual DOM.
|
239
|
+
|
240
|
+
This is the essence of declarative, state-driven UI. You declare what the UI should look like for a given state, and
|
241
|
+
the framework handles the "how" and "when" of updating it.
|
242
|
+
|
243
|
+
## The Bridge: How "Push" and "Pull" Work Together
|
244
|
+
|
245
|
+
The true power of the v10 config system is how these two tiers are seamlessly integrated. This bridge is forged in
|
246
|
+
the auto-generated setters of reactive configs and the `set()` method of `Neo.core.Base`.
|
247
|
+
|
248
|
+
When you change a config on any component (class-based or functional):
|
249
|
+
|
250
|
+
```javascript readonly
|
251
|
+
myComponent.myConfig = 'new value';
|
252
|
+
// or
|
253
|
+
myComponent.set({myConfig: 'new value'});
|
254
|
+
```
|
255
|
+
|
256
|
+
Here's what happens under the hood:
|
257
|
+
|
258
|
+
1. **Batching Begins:** The `set()` method in `Neo.core.Base` immediately calls `EffectManager.pause()`. This tells
|
259
|
+
the "pull" system to queue up any effects that get triggered but not to run them yet.
|
260
|
+
2. **The Setter is Called:** The auto-generated setter for `myConfig` is invoked. This setter is the heart of the
|
261
|
+
bridge. It performs two critical actions:
|
262
|
+
* **Pull System Update:** It retrieves the `Neo.core.Config` instance for `myConfig` (using `this.getConfig('myConfig')`)
|
263
|
+
and calls its `set()` method with the new value. This updates the reactive atom and queues any dependent effects
|
264
|
+
(like a functional component's `vdomEffect`).
|
265
|
+
* **Push System Update:** It proceeds with the classic "push" system logic, adding the new value to the
|
266
|
+
`configSymbol` staging area and eventually calling the `afterSetMyConfig()` hook.
|
267
|
+
3. **Batching Ends:** After the `set()` method in `core.Base` has finished processing all configs in the batch, its
|
268
|
+
`finally` block calls `EffectManager.resume()`. This tells the `EffectManager` to run all the unique effects that
|
269
|
+
were queued during the operation, ensuring that the UI and other reactive computations update exactly once.
|
270
|
+
|
271
|
+
This elegant integration means you get the best of both worlds: the predictable, hook-based logic of the push system
|
272
|
+
and the automatic, fine-grained reactivity of the pull system, all working in harmony.
|
273
|
+
|
274
|
+
## In-depth Example: A Reactive `MainContainer`
|
178
275
|
|
179
276
|
Let's analyze a practical example to see these concepts in action. The `Neo.examples.core.config.MainContainer`
|
180
|
-
demonstrates how to build a reactive UI declaratively.
|
277
|
+
demonstrates how to build a reactive UI declaratively using the classic "push" system.
|
181
278
|
|
182
279
|
**The Goal:** Create a container with two labels. The text of each label is calculated based on the values of two
|
183
280
|
config properties, `a` and `b`. A button allows the user to change `a` and `b` simultaneously.
|
@@ -259,23 +356,26 @@ class MainContainer extends Viewport {
|
|
259
356
|
* `afterSetB` runs. It calculates `label2.text` as `value (10) + this.a (reads 10 from _a) = 20`.
|
260
357
|
* **New State:** `label1` shows "20", `label2` shows "20".
|
261
358
|
|
262
|
-
This example vividly demonstrates the dynamic and reactive nature of the system, where a single declarative state
|
263
|
-
change automatically propagates through the component logic.
|
359
|
+
This example vividly demonstrates the dynamic and reactive nature of the "push" system, where a single declarative state
|
360
|
+
change automatically propagates through the component logic via `afterSet` hooks.
|
264
361
|
|
265
|
-
##
|
362
|
+
## Best Practices for the Hybrid System
|
266
363
|
|
267
|
-
* **Embrace Declarativity:**
|
268
|
-
|
364
|
+
* **Embrace Declarativity:** For functional components, define your UI structure inside `createVdom`. Trust the
|
365
|
+
reactive system to handle updates. For class-based components, define your entire UI structure inside `static config`
|
366
|
+
whenever possible. This improves readability and maintainability.
|
269
367
|
* **Use the `_` Suffix Wisely:** Only add the trailing underscore to configs that need `afterSet`, `beforeSet` or
|
270
368
|
`beforeGet` based logic. For simple value properties, omit it to avoid unnecessary overhead.
|
271
369
|
* **Keep `afterSet` Handlers Pure:** An `afterSet` handler should ideally only react to the change of its own
|
272
370
|
property and update other parts of the application. Avoid triggering complex chains of `set()` calls from within
|
273
371
|
an `afterSet` if possible.
|
274
372
|
* **Batch Updates with `set()`:** When you need to change multiple properties at once, always use a single
|
275
|
-
`set({a: 1, b: 2})` call. This is more efficient and ensures consistency
|
373
|
+
`set({a: 1, b: 2})` call. This is more efficient and ensures consistency across both reactivity systems.
|
276
374
|
* **Use `onConstructed` for Post-Construction Logic:** Use the `onConstructed` lifecycle method to perform any setup
|
277
375
|
that depends on the instance's initial configuration being fully processed. This is the ideal place for logic that
|
278
376
|
requires all configs to be set and potentially other instances to be created (if set-driven).
|
377
|
+
* **Understand Your Dependencies:** Be mindful of which configs you access inside `createVdom` and other effects, as
|
378
|
+
this determines when they will re-run.
|
279
379
|
|
280
380
|
By understanding these internal mechanics and following best practices, you can leverage the full power of Neo.mjs's
|
281
|
-
|
381
|
+
hybrid config system to build highly complex, reactive, and maintainable applications with confidence.
|