Skip to content

Commit e1ca4be

Browse files
committed
docs: Add guide for Declarative Config Merging & Structural Injection (#8575)
1 parent 6c60262 commit e1ca4be

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Declarative Config Merging & Structural Injection
2+
3+
One of the most powerful aspects of Neo.mjs is its class-based configuration system. However, as applications grow, you often face the challenge of **"Configuration Drilling"**—configuring a deeply nested component from a parent container without tightly coupling every intermediate layer.
4+
5+
Previously, this often required imperative logic in `construct()` or `afterSetItems` to manually hunt down components and apply settings.
6+
7+
Neo.mjs introduces **Declarative Config Merging** via the `mergeFrom` symbol, enabling a pattern we call **Structural Injection**. This allows you to separate the *definition* of your component hierarchy (structure) from its *configuration* (data/behavior), and merge them declaratively.
8+
9+
## The Problem: Rigid Hierarchies
10+
11+
Imagine a `MainContainer` that contains a `Sidebar`, which contains a `TreeList`.
12+
13+
```javascript
14+
class MainContainer extends Container {
15+
static config = {
16+
items: [{
17+
module: Sidebar, // Nested container
18+
items : [{
19+
module: TreeList // Deeply nested component
20+
// How do we configure this TreeList from MainContainer subclasses?
21+
}]
22+
}]
23+
}
24+
}
25+
```
26+
27+
If a subclass `TicketsContainer` wants to change the `TreeList`'s `displayField`, it traditionally had to:
28+
1. Overwrite the entire `items` array (brittle, duplicates code).
29+
2. Use imperative logic to find the tree and set the config.
30+
31+
## The Solution: `mergeFrom`
32+
33+
The `mergeFrom` symbol allows an item in the `items` structure to declare, "I get my configuration from this property on the container."
34+
35+
### Step 1: Define the Configuration Object
36+
37+
Define a reactive config property to hold the settings. Use `merge: 'deep'` to allow subclasses to override specific properties easily.
38+
39+
```javascript
40+
import {isDescriptor} from '../../src/core/ConfigSymbols.mjs';
41+
42+
class MainContainer extends Container {
43+
static config = {
44+
/**
45+
* Configuration for the nested TreeList.
46+
* Subclasses can override this to change tree behavior.
47+
*/
48+
treeConfig_: {
49+
[isDescriptor]: true,
50+
merge : 'deep',
51+
value : {
52+
displayField: 'text', // Default
53+
navigable : true
54+
}
55+
}
56+
}
57+
}
58+
```
59+
60+
### Step 2: Bind the Item to the Config
61+
62+
Import `mergeFrom` and use it in your `items` definition.
63+
64+
```javascript
65+
import {isDescriptor, mergeFrom} from '../../src/core/ConfigSymbols.mjs';
66+
67+
class MainContainer extends Container {
68+
static config = {
69+
// ... treeConfig_ defined above ...
70+
71+
items: {
72+
[isDescriptor]: true,
73+
merge : 'deep',
74+
clone : 'deep', // Important! See "Prototype Pollution" below.
75+
value : {
76+
sidebar: {
77+
module: Sidebar,
78+
items : {
79+
myTree: {
80+
module : TreeList,
81+
[mergeFrom]: 'treeConfig' // <--- The Magic
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
}
89+
```
90+
91+
When `MainContainer` creates its items:
92+
1. It encounters the `myTree` item definition.
93+
2. It sees `[mergeFrom]: 'treeConfig'`.
94+
3. It looks up `this.treeConfig` on the `MainContainer` instance.
95+
4. It **deeply merges** `this.treeConfig` into the item definition.
96+
5. It instantiates the `TreeList` with the merged result.
97+
98+
### Step 3: Subclassing made Easy
99+
100+
Now, creating a specialized version of the container is trivial. You simply override the config object. The structure remains untouched.
101+
102+
```javascript
103+
class TicketsContainer extends MainContainer {
104+
static config = {
105+
// Override just the specific settings we care about
106+
treeConfig: {
107+
displayField: 'ticketTitle',
108+
rootPath : '/tickets'
109+
}
110+
}
111+
}
112+
```
113+
114+
The `TicketsContainer` will render the exact same structure as `MainContainer`, but the deep-nested `TreeList` will receive the new `displayField` and `rootPath`.
115+
116+
## The Structural Injection Pattern
117+
118+
This pattern encourages a clear separation of concerns:
119+
120+
1. **Structure (`items_`):** Defines the skeleton of your UI (layout, component hierarchy, references). This rarely changes between subclasses.
121+
2. **Configuration (`myConfig_`):** Defines the variable aspects of the UI (text, stores, flags, behavior). This is what subclasses customize.
122+
123+
By injecting configuration into structure using `mergeFrom`, you create highly reusable, "White-Box" containers that are easy to extend and maintain.
124+
125+
## Recursive Support
126+
127+
The `mergeFrom` feature is **recursive**. It works for:
128+
- Direct children.
129+
- Nested children defined via `items` arrays.
130+
- Nested children defined via `items` configuration objects (maps).
131+
132+
This means you can inject configuration into a component nested 10 levels deep, as long as the hierarchy is defined within the same container class.
133+
134+
## Prototype Pollution & `clone: 'deep'`
135+
136+
When defining complex nested structures in `static config`, you must be careful about shared object references.
137+
138+
By default, `Neo.mjs` config objects are shared across all instances of a class. If the framework modifies these objects (e.g., merging configs), it can affect other instances (Prototype Pollution).
139+
140+
To prevent this, **always** use `clone: 'deep'` when using the Structural Injection Pattern with object-based items.
141+
142+
```javascript
143+
items: {
144+
[isDescriptor]: true,
145+
merge : 'deep',
146+
clone : 'deep', // <--- CRITICAL
147+
value : {
148+
// ... your nested structure ...
149+
}
150+
}
151+
```
152+
153+
This ensures that every instance of your container gets its own fresh copy of the item definitions, safe for modification and merging.
154+
155+
## Summary
156+
157+
| Feature | Description |
158+
| :--- | :--- |
159+
| **`mergeFrom`** | A symbol used in item definitions to reference a config property on the parent container. |
160+
| **Injection** | The parent config is deeply merged *on top* of the item's definition. |
161+
| **Recursion** | Works for deeply nested items defined within the container's config. |
162+
| **Overriding** | Subclasses override the *config property*, not the `items` structure. |
163+
| **Safety** | Use `clone: 'deep'` on the `items` descriptor to prevent cross-instance state pollution. |

learn/tree.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
{"name": "Declarative Component Trees VS Imperative Vdom", "parentId": "guides/fundamentals", "id": "guides/fundamentals/DeclarativeComponentTreesVsImperativeVdom"},
4040
{"name": "Declarative VDOM with Effects", "parentId": "guides/fundamentals", "id": "guides/fundamentals/DeclarativeVDOMWithEffects"},
4141
{"name": "Config System Deep Dive", "parentId": "guides/fundamentals", "id": "guides/fundamentals/ConfigSystemDeepDive"},
42+
{"name": "Declarative Config Merging", "parentId": "guides/fundamentals", "id": "guides/fundamentals/DeclarativeConfigMerging"},
4243
{"name": "Extending Neo Classes", "parentId": "guides/fundamentals", "id": "guides/fundamentals/ExtendingNeoClasses"},
4344
{"name": "Main Thread Addons", "parentId": "guides/fundamentals", "id": "guides/fundamentals/MainThreadAddons"},
4445
{"name": "UI Building Blocks", "parentId": "guides", "isLeaf": false, "id": "guides/uibuildingblocks", "collapsed": true},

0 commit comments

Comments
 (0)