Skip to content

Commit 085cbee

Browse files
committed
docs: Create Frontend Guide 3 (State Management & Controls) (#9261)
1 parent 5f95e17 commit 085cbee

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# State Management & Component Controllers
2+
3+
The DevIndex application is not just a static table; it is a highly interactive "Fat Client" dashboard managing 50,000+ complex records. Users can dynamically filter by country, search by name, toggle metrics, and adjust render settings on the fly.
4+
5+
Wiring a complex control panel (`ControlsContainer`) directly to a massive data grid (`GridContainer`) using standard DOM event listeners would quickly result in spaghetti code and tightly coupled components.
6+
7+
To solve this, the DevIndex application relies heavily on the **Neo.mjs MVC/MVVM architecture**: `StateProviders` (for declarative data binding) and `Controllers` (for localized business logic).
8+
9+
Crucially, **all of this logic executes within the App Worker**, meaning even complex state recalculations across 50,000 records will never freeze the browser's Main Thread.
10+
11+
---
12+
13+
## The Orchestrator: MainContainer
14+
15+
The central hub for the DevIndex "Home" view is `apps/devindex/view/home/MainContainer.mjs`. It doesn't contain much UI itself; instead, it orchestrates the layout and injects the MVVM foundation for its children.
16+
17+
```javascript readonly
18+
// apps/devindex/view/home/MainContainer.mjs
19+
class MainContainer extends Container {
20+
static config = {
21+
controller : Controller, // Injects MainContainerController
22+
stateProvider: MainContainerStateProvider, // Injects MainContainerStateProvider
23+
24+
items: [{
25+
// ... GridWrapper
26+
items: [{
27+
module : GridContainer,
28+
bind : {store: 'stores.contributors'}, // Data binding
29+
reference: 'grid' // Lookup reference
30+
}, {
31+
module : StatusToolbar,
32+
bind : {store: 'stores.contributors'} // Data binding
33+
}]
34+
}, {
35+
module : ControlsContainer,
36+
reference: 'controls' // Lookup reference
37+
}]
38+
}
39+
}
40+
```
41+
42+
Notice the use of `reference`. Instead of relying on rigid DOM hierarchies or component IDs, children are assigned logical references (e.g., `'grid'`). The Controller uses these references to safely query the component tree.
43+
44+
---
45+
46+
## The Data Hub: StateProviders
47+
48+
Neo.mjs uses a hierarchical state management system. A `StateProvider` attached to a container makes its data available to all descendant components.
49+
50+
### 1. The Local State (MainContainerStateProvider)
51+
52+
The DevIndex doesn't instantiate its 50k-row data store globally or directly inside the Grid. Instead, the store is defined declaratively inside the `MainContainerStateProvider`:
53+
54+
```javascript readonly
55+
// apps/devindex/view/home/MainContainerStateProvider.mjs
56+
class MainContainerStateProvider extends StateProvider {
57+
static config = {
58+
stores: {
59+
contributors: {
60+
module: Contributors // The 50k-row Store
61+
}
62+
}
63+
}
64+
}
65+
```
66+
67+
Because the store lives in the State Provider, multiple independent components can bind to it simultaneously without needing to know about each other. As seen in the `MainContainer` snippet above, both the `GridContainer` and the `StatusToolbar` use `bind: {store: 'stores.contributors'}` to share the exact same dataset instance.
68+
69+
### 2. The Global State (ViewportStateProvider)
70+
71+
The application also has a top-level `ViewportStateProvider` that manages global UI state.
72+
73+
```javascript readonly
74+
// apps/devindex/view/ViewportStateProvider.mjs
75+
class ViewportStateProvider extends StateProvider {
76+
static config = {
77+
data: {
78+
animateVisuals: true,
79+
isScrolling : false
80+
}
81+
}
82+
}
83+
```
84+
85+
This is incredibly powerful for performance optimization. For instance, the `GridContainer` binds to this global state to disable heavy sparkline animations while the user is actively scrolling:
86+
87+
```javascript readonly
88+
// GridContainer.mjs
89+
bind: {
90+
animateVisuals: data => data.animateVisuals
91+
}
92+
```
93+
94+
---
95+
96+
## Business Logic: The Component Controller
97+
98+
The `ControlsContainer` is full of checkboxes, text fields, and combo boxes. When a user interacts with them, the components themselves don't know *what* to do with the data; they just fire events.
99+
100+
The `MainContainerController` (`apps/devindex/view/home/MainContainerController.mjs`) catches these events, queries other components using `getReference()`, and mutates the state or the store.
101+
102+
### Handling Interactive Filtering
103+
104+
When a user types into the "Bio Search" text field, the field fires a `change` event routed to `onFilterChange`.
105+
106+
```javascript readonly
107+
// apps/devindex/view/home/MainContainerController.mjs
108+
onFilterChange(data) {
109+
let grid = this.getReference('grid'),
110+
value = data.component.getSubmitValue();
111+
112+
if (data.component.name === 'countryCode' && value) {
113+
value = value.toUpperCase();
114+
}
115+
116+
// Mutate the bound store's filter
117+
grid.store.getFilter(data.component.name).value = value;
118+
}
119+
```
120+
121+
The Controller finds the Grid, accesses the shared `Contributors` store, and updates the specific filter object matching the component's name.
122+
123+
Because the Store is reactive, mutating the filter immediately triggers a "Soft Hydration" loop (as described in the Backend guide) where the App Worker iterates over all 50,000 raw objects in memory, applies the filter, and commands the Virtual DOM worker to render the new subset.
124+
125+
### Managing Global State from Local Events
126+
127+
When the user scrolls the grid rapidly, we want to pause heavy operations. The grid fires an `isScrollingChange` event, caught by the Controller:
128+
129+
```javascript readonly
130+
onGridIsScrollingChange(data) {
131+
// Updates the global ViewportStateProvider
132+
this.setState('isScrolling', data.value);
133+
}
134+
```
135+
136+
This single line of code updates the global state, which automatically cascades down to any component bound to `isScrolling` across the entire application, instantly optimizing rendering.
137+
138+
---
139+
140+
## Summary
141+
142+
The DevIndex leverages Neo.mjs MVVM patterns to keep its architecture clean while handling massive datasets:
143+
1. **State Providers** act as declarative data hubs, allowing sibling components (like the Grid and the Statusbar) to share stores and global UI flags effortlessly.
144+
2. **Component Controllers** act as the nerve center, decoupling UI events (from the Controls) from the data mutations (on the Store).
145+
3. **App Worker Execution** guarantees that the complex logic required to filter, coordinate, and dispatch updates across 50,000 records never touches the browser's Main Thread.

learn/guides/devindex/tree.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@
1414
{"name": "Data Hygiene & Cleanup", "parentId": "DataFactory", "id": "data-factory/DataHygiene"},
1515
{"name": "Opt-In Service Architecture", "parentId": "DataFactory", "id": "data-factory/OptIn"},
1616
{"name": "Opt-Out Service Architecture", "parentId": "DataFactory", "id": "data-factory/OptOut"},
17-
{"name": "The Backend Twist", "parentId": null, "id": "Backend"}
17+
{"name": "The Backend Twist", "parentId": null, "id": "Backend"},
18+
{"name": "Frontend Architecture", "parentId": null, "isLeaf": false, "id": "Frontend"},
19+
{"name": "App Shell & MVVM", "parentId": "Frontend", "id": "frontend/Architecture"},
20+
{"name": "The 50k-Row Grid", "parentId": "Frontend", "id": "frontend/TheGrid"},
21+
{"name": "State Management & Controls", "parentId": "Frontend", "id": "frontend/StateAndControls"},
22+
{"name": "The Content Engine", "parentId": "Frontend", "id": "frontend/ContentEngine"}
1823
]}

0 commit comments

Comments
 (0)