@actdim/dynstruct 1.2.5 → 1.2.7
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/README.md +2338 -18
- package/dist/appDomain/appContracts.d.ts.map +1 -1
- package/dist/appDomain/appContracts.es.js +1 -1
- package/dist/appDomain/appContracts.es.js.map +1 -1
- package/dist/appDomain/navigation.es.js.map +1 -1
- package/dist/appDomain/security/securityContracts.es.js.map +1 -1
- package/dist/appDomain/security/securityProvider.d.ts +6 -5
- package/dist/appDomain/security/securityProvider.d.ts.map +1 -1
- package/dist/appDomain/security/securityProvider.es.js +70 -45
- package/dist/appDomain/security/securityProvider.es.js.map +1 -1
- package/dist/appDomain/util.es.js.map +1 -1
- package/dist/componentModel/DynamicContent.es.js.map +1 -1
- package/dist/componentModel/adapters.es.js.map +1 -1
- package/dist/componentModel/componentContext.d.ts +5 -5
- package/dist/componentModel/componentContext.d.ts.map +1 -1
- package/dist/componentModel/componentContext.es.js.map +1 -1
- package/dist/componentModel/contracts.d.ts +23 -19
- package/dist/componentModel/contracts.d.ts.map +1 -1
- package/dist/componentModel/contracts.es.js +1 -1
- package/dist/componentModel/contracts.es.js.map +1 -1
- package/dist/componentModel/core.d.ts +2 -2
- package/dist/componentModel/core.d.ts.map +1 -1
- package/dist/componentModel/core.es.js +115 -111
- package/dist/componentModel/core.es.js.map +1 -1
- package/dist/componentModel/react/errorBoundary.d.ts +19 -0
- package/dist/componentModel/react/errorBoundary.d.ts.map +1 -0
- package/dist/componentModel/react/errorBoundary.es.js +52 -0
- package/dist/componentModel/react/errorBoundary.es.js.map +1 -0
- package/dist/componentModel/react.d.ts +2 -2
- package/dist/componentModel/react.d.ts.map +1 -1
- package/dist/componentModel/react.es.js +156 -139
- package/dist/componentModel/react.es.js.map +1 -1
- package/dist/componentModel/scope.d.ts +1 -1
- package/dist/componentModel/scope.d.ts.map +1 -1
- package/dist/componentModel/scope.es.js +1 -1
- package/dist/componentModel/scope.es.js.map +1 -1
- package/dist/globals.es.js.map +1 -1
- package/dist/net/apiError.es.js.map +1 -1
- package/dist/net/client.d.ts +9 -5
- package/dist/net/client.d.ts.map +1 -1
- package/dist/net/client.es.js +96 -80
- package/dist/net/client.es.js.map +1 -1
- package/dist/net/request.es.js.map +1 -1
- package/dist/reactHooks.d.ts +1 -1
- package/dist/reactHooks.d.ts.map +1 -1
- package/dist/reactHooks.es.js.map +1 -1
- package/dist/services/ServiceProvider.es.js.map +1 -1
- package/dist/services/StorageService.es.js.map +1 -1
- package/dist/services/react/NavService.d.ts.map +1 -1
- package/dist/services/react/NavService.es.js +15 -15
- package/dist/services/react/NavService.es.js.map +1 -1
- package/dist/services/react/ServiceProvider.d.ts +2 -2
- package/dist/services/react/ServiceProvider.d.ts.map +1 -1
- package/dist/services/react/ServiceProvider.es.js.map +1 -1
- package/dist/services/react/StorageService.d.ts.map +1 -1
- package/dist/services/react/StorageService.es.js +15 -19
- package/dist/services/react/StorageService.es.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,21 +2,2341 @@
|
|
|
2
2
|
|
|
3
3
|
Build scalable applications with dynamic structured components, explicit wiring, and decoupled message flow. Keep architecture clean and modular.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@actdim/dynstruct)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
**@actdim/dynstruct** is a sophisticated TypeScript-based component system and architectural framework for building large-scale, modular applications. It provides a structure-first, declarative approach to component design with:
|
|
12
|
+
|
|
13
|
+
- **Type-safe component model** with explicit dependency wiring
|
|
14
|
+
- **Decoupled messaging architecture** using a message bus for inter-component communication
|
|
15
|
+
- **Component lifecycle management** with proper initialization and cleanup
|
|
16
|
+
- **Automatic reactive state** - properties become reactive after component creation
|
|
17
|
+
- **Type-safe component events** - automatic event handlers for lifecycle and property changes with full IntelliSense
|
|
18
|
+
- **Built-in service integration** via adapter pattern
|
|
19
|
+
- **Parent-child component relationships** with message routing
|
|
20
|
+
|
|
21
|
+
### Framework Support
|
|
22
|
+
|
|
23
|
+
**Currently Supported:**
|
|
24
|
+
- ✅ **React** (with MobX for reactivity)
|
|
25
|
+
|
|
26
|
+
**Planned Support:**
|
|
27
|
+
- 🚧 **SolidJS** - In development
|
|
28
|
+
- 🚧 **Vue.js** - Planned
|
|
29
|
+
|
|
30
|
+
The architectural core is framework-agnostic, allowing the same component structures and patterns to work across different UI frameworks.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
✨ **Structure-First Design** - Define components with explicit props, actions, children, and message channels
|
|
35
|
+
|
|
36
|
+
🔒 **Full Type Safety** - TypeScript generics throughout for compile-time verification
|
|
37
|
+
|
|
38
|
+
📡 **Message Bus Communication** - Decoupled component interaction via publish/subscribe pattern
|
|
39
|
+
|
|
40
|
+
⚡ **Reactive by Default** - Properties automatically trigger UI updates when changed
|
|
41
|
+
|
|
42
|
+
🔌 **Service Adapters** - Clean integration of backend services with message bus
|
|
43
|
+
|
|
44
|
+
🧩 **Modular Architecture** - Clear component hierarchies with parent-child relationships
|
|
45
|
+
|
|
46
|
+
🔄 **Lifecycle Management** - Proper initialization, layout, ready states, and cleanup
|
|
47
|
+
|
|
48
|
+
⚡ **Component Events** - Automatic type-safe event handlers for lifecycle and property changes
|
|
49
|
+
|
|
50
|
+
🎯 **Navigation & Routing** - Built-in navigation contracts with React Router integration
|
|
51
|
+
|
|
52
|
+
🔐 **Security Provider** - Authentication, authorization, and ACL support
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
Try @actdim/dynstruct instantly in your browser without any installation:
|
|
57
|
+
|
|
58
|
+
[](https://stackblitz.com/~/github.com/actdim/dynstruct)
|
|
59
|
+
|
|
60
|
+
Once the project loads, run Storybook to see examples:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm run storybook
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## How It Works
|
|
67
|
+
|
|
68
|
+
The core pattern in dynstruct is **structure-first composition** where parent component structures explicitly reference child component structures. This makes all dependencies visible at the type level.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install @actdim/dynstruct
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Peer Dependencies
|
|
77
|
+
|
|
78
|
+
This package requires the following peer dependencies:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install react react-dom mobx mobx-react-lite mobx-utils \
|
|
82
|
+
@actdim/msgmesh @actdim/utico react-router react-router-dom \
|
|
83
|
+
rxjs uuid path-to-regexp jwt-decode http-status
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or with pnpm:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm add @actdim/dynstruct @actdim/msgmesh @actdim/utico \
|
|
90
|
+
react react-dom mobx mobx-react-lite mobx-utils \
|
|
91
|
+
react-router react-router-dom rxjs uuid path-to-regexp \
|
|
92
|
+
jwt-decode http-status
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick Start (React)
|
|
96
|
+
|
|
97
|
+
> **Note:** All examples below are for the **React** implementation. SolidJS and Vue.js versions will have similar structure with framework-specific adapters.
|
|
98
|
+
|
|
99
|
+
### 1. Define Child Components
|
|
100
|
+
|
|
101
|
+
First, create simple child components (Button and Input):
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// React implementation
|
|
105
|
+
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
106
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
107
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
108
|
+
|
|
109
|
+
// Button component structure
|
|
110
|
+
type ButtonStruct = ComponentStruct<AppMsgStruct, {
|
|
111
|
+
props: {
|
|
112
|
+
label: string;
|
|
113
|
+
onClick: () => void;
|
|
114
|
+
};
|
|
115
|
+
}>;
|
|
116
|
+
|
|
117
|
+
// Button hook-constructor
|
|
118
|
+
const useButton = (params: ComponentParams<ButtonStruct>) => {
|
|
119
|
+
const def: ComponentDef<ButtonStruct> = {
|
|
120
|
+
props: {
|
|
121
|
+
label: params.label ?? 'Click',
|
|
122
|
+
onClick: params.onClick ?? (() => {})
|
|
123
|
+
},
|
|
124
|
+
view: (_, c) => (
|
|
125
|
+
<button onClick={c.model.onClick}>{c.model.label}</button>
|
|
126
|
+
)
|
|
127
|
+
};
|
|
128
|
+
return useComponent(def, params);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Input component structure
|
|
132
|
+
type InputStruct = ComponentStruct<AppMsgStruct, {
|
|
133
|
+
props: {
|
|
134
|
+
value: string;
|
|
135
|
+
onChange: (v: string) => void;
|
|
136
|
+
};
|
|
137
|
+
}>;
|
|
138
|
+
|
|
139
|
+
// Input hook-constructor
|
|
140
|
+
const useInput = (params: ComponentParams<InputStruct>) => {
|
|
141
|
+
const def: ComponentDef<InputStruct> = {
|
|
142
|
+
props: {
|
|
143
|
+
value: params.value ?? '',
|
|
144
|
+
onChange: params.onChange ?? (() => {})
|
|
145
|
+
},
|
|
146
|
+
view: (_, c) => (
|
|
147
|
+
<input
|
|
148
|
+
value={c.model.value}
|
|
149
|
+
onChange={(e) => c.model.onChange(e.target.value)}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
};
|
|
153
|
+
return useComponent(def, params);
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 2. Define Parent Component with Children
|
|
158
|
+
|
|
159
|
+
The parent component structure **references child structures** - this makes dependencies explicit:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// React implementation
|
|
163
|
+
// Parent component structure with children
|
|
164
|
+
type CounterPanelStruct = ComponentStruct<AppMsgStruct, {
|
|
165
|
+
props: {
|
|
166
|
+
counter: number;
|
|
167
|
+
message: string;
|
|
168
|
+
};
|
|
169
|
+
children: {
|
|
170
|
+
incrementBtn: ButtonStruct; // References child structure
|
|
171
|
+
resetBtn: ButtonStruct; // References child structure
|
|
172
|
+
messageInput: InputStruct; // References child structure
|
|
173
|
+
};
|
|
174
|
+
}>;
|
|
175
|
+
|
|
176
|
+
// Parent hook-constructor
|
|
177
|
+
const useCounterPanel = (params: ComponentParams<CounterPanelStruct>) => {
|
|
178
|
+
let c: Component<CounterPanelStruct>;
|
|
179
|
+
let m: ComponentModel<CounterPanelStruct>;
|
|
180
|
+
|
|
181
|
+
const def: ComponentDef<CounterPanelStruct> = {
|
|
182
|
+
props: {
|
|
183
|
+
counter: params.counter ?? 0,
|
|
184
|
+
message: params.message ?? 'Hello'
|
|
185
|
+
},
|
|
186
|
+
// Component events with full IntelliSense support!
|
|
187
|
+
events: {
|
|
188
|
+
// Automatically typed event for 'message' property
|
|
189
|
+
onChangeMessage: (oldValue, newValue) => {
|
|
190
|
+
console.log(`Message changed from "${oldValue}" to "${newValue}"`);
|
|
191
|
+
// You can also update other properties or sync with children
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// Event for 'counter' property
|
|
195
|
+
onChangeCounter: (oldValue, newValue) => {
|
|
196
|
+
if (newValue > 10) {
|
|
197
|
+
m.message = 'Counter is getting high!';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
// Children are created at runtime via their hook-constructors
|
|
202
|
+
children: {
|
|
203
|
+
incrementBtn: useButton({
|
|
204
|
+
label: 'Increment',
|
|
205
|
+
onClick: () => { m.counter++; }
|
|
206
|
+
}),
|
|
207
|
+
resetBtn: useButton({
|
|
208
|
+
label: 'Reset',
|
|
209
|
+
onClick: () => { m.counter = 0; }
|
|
210
|
+
}),
|
|
211
|
+
messageInput: useInput({
|
|
212
|
+
value: bind(() => m.message, v => { m.message = v; })
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
view: (_, c) => (
|
|
216
|
+
<div>
|
|
217
|
+
<h3>{m.message}</h3>
|
|
218
|
+
<p>Counter: {m.counter}</p>
|
|
219
|
+
{/* Use children via c.children.xxx.View */}
|
|
220
|
+
<c.children.incrementBtn.View />
|
|
221
|
+
<c.children.resetBtn.View />
|
|
222
|
+
<c.children.messageInput.View />
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
c = useComponent(def, params);
|
|
228
|
+
m = c.model;
|
|
229
|
+
return c;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 3. Using Components
|
|
234
|
+
|
|
235
|
+
**Primary way** - Use as children in parent components (shown above).
|
|
236
|
+
|
|
237
|
+
**Alternative way** - Use `toReact` adapter for integration with standard React:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// React adapter
|
|
241
|
+
// Create React adapter (only when needed for standard React integration)
|
|
242
|
+
export const CounterPanel = toReact(useCounterPanel);
|
|
243
|
+
|
|
244
|
+
// Now can be used in standard React components
|
|
245
|
+
function App() {
|
|
246
|
+
return (
|
|
247
|
+
<div>
|
|
248
|
+
<CounterPanel counter={5} message="Welcome!" />
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Note**: `toReact` is an adapter for compatibility with standard React components. The **primary pattern** is to use components through `children` property in parent structures, as this makes all dependencies explicit at the type level.
|
|
255
|
+
|
|
256
|
+
## Key Advantages (React Examples)
|
|
257
|
+
|
|
258
|
+
> **Note:** Examples in this section demonstrate the **React** implementation.
|
|
259
|
+
|
|
260
|
+
### Clean JSX Without Clutter
|
|
261
|
+
|
|
262
|
+
The combination of **bindings** (`bind`), **events**, and **`.View` wrappers** creates clean, readable JSX that clearly shows component structure without logic clutter:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// React example
|
|
266
|
+
// ❌ Traditional React - cluttered with inline handlers and logic
|
|
267
|
+
<div>
|
|
268
|
+
<h3>{message}</h3>
|
|
269
|
+
<p>Counter: {counter}</p>
|
|
270
|
+
<button onClick={() => setCounter(counter + 1)}>Increment</button>
|
|
271
|
+
<button onClick={() => setCounter(0)}>Reset</button>
|
|
272
|
+
<input
|
|
273
|
+
value={message}
|
|
274
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
// ✅ dynstruct - clean JSX showing structure
|
|
279
|
+
<div>
|
|
280
|
+
<h3>{m.message}</h3>
|
|
281
|
+
<p>Counter: {m.counter}</p>
|
|
282
|
+
<c.children.incrementBtn.View />
|
|
283
|
+
<c.children.resetBtn.View />
|
|
284
|
+
<c.children.messageInput.View />
|
|
285
|
+
</div>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Performance Problems in Traditional React
|
|
289
|
+
|
|
290
|
+
#### Problem 1: Inline Functions Break Memoization
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// ❌ PROBLEM: New function created on every render
|
|
294
|
+
function TodoList({ todos }) {
|
|
295
|
+
const [filter, setFilter] = useState('');
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div>
|
|
299
|
+
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
|
300
|
+
{todos.map(todo => (
|
|
301
|
+
<ExpensiveTodoItem
|
|
302
|
+
key={todo.id}
|
|
303
|
+
todo={todo}
|
|
304
|
+
// NEW FUNCTION on every render - breaks React.memo!
|
|
305
|
+
onToggle={() => toggleTodo(todo.id)}
|
|
306
|
+
/>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// React.memo is USELESS here - onToggle is always new
|
|
313
|
+
const ExpensiveTodoItem = React.memo(({ todo, onToggle }) => {
|
|
314
|
+
console.log('Render:', todo.id); // Logs on EVERY keystroke in filter!
|
|
315
|
+
return <div onClick={onToggle}>{todo.text}</div>;
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Result:** Every keystroke in filter input re-renders ALL todo items, even though they haven't changed.
|
|
320
|
+
|
|
321
|
+
#### Problem 2: Inline Objects Break Memoization
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// ❌ PROBLEM: New object created on every render
|
|
325
|
+
function UserTable({ users }) {
|
|
326
|
+
const [sort, setSort] = useState('name');
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<Table
|
|
330
|
+
data={users}
|
|
331
|
+
// NEW OBJECT on every render!
|
|
332
|
+
config={{ sortable: true, filterable: true }}
|
|
333
|
+
// NEW OBJECT on every render!
|
|
334
|
+
style={{ padding: 10, margin: 5 }}
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// React.memo is USELESS - config and style are always new references
|
|
340
|
+
const Table = React.memo(({ data, config, style }) => {
|
|
341
|
+
console.log('Table rendered'); // Renders constantly!
|
|
342
|
+
return <table style={style}>...</table>;
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Problem 3: useCallback/useMemo Boilerplate
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ✅ "Fixed" with hooks, but verbose and error-prone
|
|
350
|
+
function TodoList({ todos }) {
|
|
351
|
+
const [filter, setFilter] = useState('');
|
|
352
|
+
|
|
353
|
+
// Must wrap in useCallback
|
|
354
|
+
const handleToggle = useCallback((id) => {
|
|
355
|
+
toggleTodo(id);
|
|
356
|
+
}, [toggleTodo]); // Don't forget dependencies!
|
|
357
|
+
|
|
358
|
+
// Must wrap in useMemo
|
|
359
|
+
const config = useMemo(() =>
|
|
360
|
+
({ sortable: true, filterable: true }), []
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const style = useMemo(() =>
|
|
364
|
+
({ padding: 10, margin: 5 }), []
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div>
|
|
369
|
+
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
|
370
|
+
{todos.map(todo => (
|
|
371
|
+
<ExpensiveTodoItem
|
|
372
|
+
key={todo.id}
|
|
373
|
+
todo={todo}
|
|
374
|
+
onToggle={handleToggle}
|
|
375
|
+
config={config}
|
|
376
|
+
style={style}
|
|
377
|
+
/>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Issues:**
|
|
385
|
+
- Verbose boilerplate everywhere
|
|
386
|
+
- Easy to forget dependencies
|
|
387
|
+
- Hard to maintain
|
|
388
|
+
- Still need to wrap everything carefully
|
|
389
|
+
|
|
390
|
+
### MobX Reactivity Pitfalls
|
|
391
|
+
|
|
392
|
+
While MobX is powerful, it has subtle issues that cause unexpected re-renders and are hard to debug:
|
|
393
|
+
|
|
394
|
+
#### Problem 1: Computed Returns New Object
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// ❌ computed recalculates on dependency changes
|
|
398
|
+
class UserStore {
|
|
399
|
+
user = { name: "Pavel", email: "pavel@mail.com" };
|
|
400
|
+
|
|
401
|
+
constructor() {
|
|
402
|
+
makeAutoObservable(this);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
get userViewModel() {
|
|
406
|
+
// ❌ Returns NEW OBJECT every time
|
|
407
|
+
return {
|
|
408
|
+
name: this.user.name,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const userStore = new UserStore();
|
|
414
|
+
|
|
415
|
+
export const Header = observer(() => {
|
|
416
|
+
const vm = userStore.userViewModel; // NEW OBJECT every render!
|
|
417
|
+
|
|
418
|
+
return <div>Hello, {vm.name}</div>;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Passing to child components breaks memoization
|
|
422
|
+
const App = observer(() => {
|
|
423
|
+
const vm = userStore.userViewModel; // NEW reference
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<div>
|
|
427
|
+
{/* ChildComponent re-renders ALWAYS, even with React.memo! */}
|
|
428
|
+
<ChildComponent user={vm} />
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const ChildComponent = React.memo(({ user }) => {
|
|
434
|
+
console.log('Child rendered'); // Logs constantly!
|
|
435
|
+
return <div>{user.name}</div>;
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Issue:** Computed returns new object each time, even if fields are the same. React sees new reference, so React.memo is useless. When you pass this object to child components, everything "falls apart" with constant re-renders.
|
|
440
|
+
|
|
441
|
+
#### Problem 2: Accidental Reactive Dependencies
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// ❌ Reading observables creates unwanted subscriptions
|
|
445
|
+
export const UsersList = observer(() => {
|
|
446
|
+
const users = userStore.users
|
|
447
|
+
.filter(u => u.isActive) // 👈 reading isActive on ALL users
|
|
448
|
+
.map(u => u.name); // 👈 reading name on ALL users
|
|
449
|
+
|
|
450
|
+
return <div>{users.join(", ")}</div>;
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Issue:** Now changing `isActive` or `name` on ANY user triggers re-render of the entire list.
|
|
455
|
+
|
|
456
|
+
**Common causes of unwanted subscriptions:**
|
|
457
|
+
- ❌ `toJS(observable)` - reads all nested properties
|
|
458
|
+
- ❌ `{ ...observableObject }` - spread operator reads all properties
|
|
459
|
+
- ❌ `Object.keys/values/entries(observable)` - reads all properties
|
|
460
|
+
- ❌ `JSON.stringify(observable)` - reads everything deeply
|
|
461
|
+
- ❌ `map/filter/reduce` on observable arrays directly in render - creates subscriptions to all items
|
|
462
|
+
- ❌ Returning new objects from `computed` - breaks React.memo (see Problem 1)
|
|
463
|
+
|
|
464
|
+
#### Problem 3: Complex Combinations
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// ❌ Combining observable, computed, autorun gets complex quickly
|
|
468
|
+
class UserStore {
|
|
469
|
+
@observable users = [];
|
|
470
|
+
@observable filter = '';
|
|
471
|
+
@observable sortOrder = 'asc';
|
|
472
|
+
|
|
473
|
+
@computed get filteredUsers() {
|
|
474
|
+
return this.users.filter(u => u.name.includes(this.filter));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@computed get sortedUsers() {
|
|
478
|
+
return this.filteredUsers.slice().sort((a, b) =>
|
|
479
|
+
this.sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
constructor() {
|
|
484
|
+
// Need runInAction for mutations
|
|
485
|
+
autorun(() => {
|
|
486
|
+
if (this.filter.length > 3) {
|
|
487
|
+
runInAction(() => {
|
|
488
|
+
this.sortOrder = 'asc';
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Issues:**
|
|
497
|
+
- Need `runInAction` for mutations inside reactions
|
|
498
|
+
- Complex dependency chains hard to trace
|
|
499
|
+
- Debugging reactive flows is difficult
|
|
500
|
+
- Easy to create circular dependencies
|
|
501
|
+
- Performance issues not immediately obvious
|
|
502
|
+
|
|
503
|
+
#### Problem 4: RxJS Complexity
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// ❌ RxJS adds another layer of complexity
|
|
507
|
+
import { BehaviorSubject, combineLatest } from 'rxjs';
|
|
508
|
+
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
509
|
+
|
|
510
|
+
const users$ = new BehaviorSubject([]);
|
|
511
|
+
const filter$ = new BehaviorSubject('');
|
|
512
|
+
|
|
513
|
+
const filteredUsers$ = combineLatest([users$, filter$]).pipe(
|
|
514
|
+
debounceTime(300),
|
|
515
|
+
map(([users, filter]) => users.filter(u => u.name.includes(filter))),
|
|
516
|
+
distinctUntilChanged()
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// Component must subscribe/unsubscribe
|
|
520
|
+
const UserList = () => {
|
|
521
|
+
const [users, setUsers] = useState([]);
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
const sub = filteredUsers$.subscribe(setUsers);
|
|
525
|
+
return () => sub.unsubscribe(); // Don't forget cleanup!
|
|
526
|
+
}, []);
|
|
527
|
+
|
|
528
|
+
return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>;
|
|
529
|
+
};
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Issues:**
|
|
533
|
+
- Entire paradigm to learn (operators, streams, subscriptions)
|
|
534
|
+
- Multiple abstractions (Subject, Observable, Operators)
|
|
535
|
+
- Manual subscription management
|
|
536
|
+
- Hard to debug async flows
|
|
537
|
+
- Easy to create memory leaks
|
|
538
|
+
|
|
539
|
+
#### Problem 5: Passing Objects Down the Hierarchy
|
|
540
|
+
|
|
541
|
+
Often, to quickly ship features, developers **cut corners** by passing objects down from parent components and using them in child components, including calling callbacks to affect upper levels.
|
|
542
|
+
|
|
543
|
+
This is **not officially an anti-pattern**, though in my opinion it should be considered one. It's a common way of parent-child interaction in React, but it **violates component isolation** and is often the cause of **unnecessary re-renders**, even when components seem independent by their properties.
|
|
544
|
+
|
|
545
|
+
**Example:**
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
export function Parent() {
|
|
549
|
+
const [count, setCount] = useState(0);
|
|
550
|
+
|
|
551
|
+
const config = { pageSize: 20 }; // ❌ new object every render
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<>
|
|
555
|
+
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
556
|
+
|
|
557
|
+
<Child config={config} />
|
|
558
|
+
<Child config={config} />
|
|
559
|
+
<Child config={config} />
|
|
560
|
+
</>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function Child({ config }: { config: { pageSize: number } }) {
|
|
565
|
+
console.log("render child");
|
|
566
|
+
return <div>{config.pageSize}</div>;
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**What happens when `count` changes:**
|
|
571
|
+
1. `config` is created anew
|
|
572
|
+
2. New reference is created
|
|
573
|
+
3. **All children are guaranteed to re-render**
|
|
574
|
+
|
|
575
|
+
Even though the children don't use `count` at all, they re-render because `config` is a new object.
|
|
576
|
+
|
|
577
|
+
**Why this happens:**
|
|
578
|
+
- Quick implementation to ship faster
|
|
579
|
+
- Passing parent state/objects down instead of proper component boundaries
|
|
580
|
+
- Callbacks passed to children to modify parent state
|
|
581
|
+
- Seems convenient but breaks component isolation
|
|
582
|
+
- Hard to spot performance issues until they accumulate
|
|
583
|
+
|
|
584
|
+
### How dynstruct Solves This
|
|
585
|
+
|
|
586
|
+
**Declarative and Explicit:**
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// React implementation
|
|
590
|
+
type TodoListStruct = ComponentStruct<AppMsgStruct, {
|
|
591
|
+
props: {
|
|
592
|
+
filter: string;
|
|
593
|
+
todos: Todo[];
|
|
594
|
+
};
|
|
595
|
+
children: {
|
|
596
|
+
filterInput: InputStruct;
|
|
597
|
+
todoItems: Record<string, TodoItemStruct>;
|
|
598
|
+
};
|
|
599
|
+
}>;
|
|
600
|
+
|
|
601
|
+
const useTodoList = (params: ComponentParams<TodoListStruct>) => {
|
|
602
|
+
let c: Component<TodoListStruct>;
|
|
603
|
+
let m: ComponentModel<TodoListStruct>;
|
|
604
|
+
|
|
605
|
+
const def: ComponentDef<TodoListStruct> = {
|
|
606
|
+
props: {
|
|
607
|
+
filter: '',
|
|
608
|
+
todos: []
|
|
609
|
+
},
|
|
610
|
+
// Events are explicit and declarative
|
|
611
|
+
events: {
|
|
612
|
+
onChangeFilter: (old, newFilter) => {
|
|
613
|
+
// No runInAction needed!
|
|
614
|
+
// Batching happens automatically
|
|
615
|
+
console.log('Filter changed:', newFilter);
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
// Children defined once, stable references
|
|
619
|
+
children: {
|
|
620
|
+
filterInput: useInput({
|
|
621
|
+
value: bind(() => m.filter, v => { m.filter = v; })
|
|
622
|
+
}),
|
|
623
|
+
todoItems: computed(() =>
|
|
624
|
+
Object.fromEntries(
|
|
625
|
+
m.todos.map(todo => [
|
|
626
|
+
todo.id,
|
|
627
|
+
useTodoItem({
|
|
628
|
+
text: todo.text,
|
|
629
|
+
completed: todo.completed,
|
|
630
|
+
onToggle: () => {
|
|
631
|
+
// Direct mutation, no runInAction!
|
|
632
|
+
todo.completed = !todo.completed;
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
])
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
},
|
|
639
|
+
view: (_, c) => (
|
|
640
|
+
<div>
|
|
641
|
+
{/* Clean JSX - no inline handlers or objects */}
|
|
642
|
+
<c.children.filterInput.View />
|
|
643
|
+
{Object.values(c.children.todoItems).map(item => (
|
|
644
|
+
<item.View key={item.id} />
|
|
645
|
+
))}
|
|
646
|
+
</div>
|
|
647
|
+
)
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
c = useComponent(def, params);
|
|
651
|
+
m = c.model;
|
|
652
|
+
return c;
|
|
653
|
+
};
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Key Benefits:**
|
|
657
|
+
|
|
658
|
+
1. **📋 Explicit Structure** - All dependencies visible in type system
|
|
659
|
+
2. **🧹 No Inline Functions/Objects** - Stable references, no re-render issues
|
|
660
|
+
3. **⚡ No runInAction** - Mutations work directly, batching automatic
|
|
661
|
+
4. **🎯 Declarative Events** - Clear, debuggable event flow
|
|
662
|
+
5. **🔍 Easy Debugging** - No hidden reactive dependencies
|
|
663
|
+
6. **💡 Simple Mental Model** - No need to learn RxJS, no complex computed chains
|
|
664
|
+
7. **⚙️ Automatic Optimization** - Batching and re-render prevention built-in
|
|
665
|
+
8. **📦 Minimal Overhead** - Performance optimizations with clear benefits
|
|
666
|
+
|
|
667
|
+
**Important Note:**
|
|
668
|
+
|
|
669
|
+
We cannot claim that using dynstruct is **always more optimal** in terms of performance, or that it **completely eliminates** the possibility of shooting yourself in the foot. Where fine-grained optimization is truly necessary, it can be done **selectively** through other approaches - **using standard React components is not prohibited!**
|
|
670
|
+
|
|
671
|
+
However, the dynstruct approach creates conditions where **dividing the application into isolated zones of responsibility becomes both necessary and convenient**. At the same time, **deviating from the rules and stepping on rakes becomes both unnecessary and inconvenient!**
|
|
672
|
+
|
|
673
|
+
Using this component model **encourages building applications from many small, well-designed architectural blocks** and making **numerous small but correct architectural decisions**. This is useful **not only in the long term** - development becomes **faster when all rules are clear and understandable**, and **technological boundaries and constraints are well-defined**.
|
|
674
|
+
|
|
675
|
+
### Why Explicit Structure Matters
|
|
676
|
+
|
|
677
|
+
The explicit separation of **props**, **actions**, and **events** in dynstruct makes code more manageable and maintainable:
|
|
678
|
+
|
|
679
|
+
**🎯 Props as Reactive Foundation:**
|
|
680
|
+
- Clear declaration: "these properties are reactive"
|
|
681
|
+
- No confusion about what triggers re-renders
|
|
682
|
+
- Type-safe from the start
|
|
683
|
+
|
|
684
|
+
**⚙️ Actions as Methods:**
|
|
685
|
+
- Clean separation: actions modify properties
|
|
686
|
+
- Easy to find where state changes happen
|
|
687
|
+
- Predictable data flow
|
|
688
|
+
|
|
689
|
+
**📡 Events as Simple Handlers:**
|
|
690
|
+
- Familiar concept: "something happened, react to it"
|
|
691
|
+
- Both property changes AND lifecycle events
|
|
692
|
+
- No complex reactive chains to debug
|
|
693
|
+
|
|
694
|
+
**Benefits in Practice:**
|
|
695
|
+
|
|
696
|
+
✅ **Less Mental Overhead:**
|
|
697
|
+
- Don't think: "Should I use `useRef`? `useState`? Take from props?"
|
|
698
|
+
- Don't think: "Do I need Redux with slices, reducers, enhancers?"
|
|
699
|
+
- Just declare props in structure - they're reactive automatically
|
|
700
|
+
|
|
701
|
+
✅ **No Optimization Anxiety:**
|
|
702
|
+
- Don't think: "Do I need `useCallback` here?"
|
|
703
|
+
- Don't think: "Should I wrap this in `useMemo`?"
|
|
704
|
+
- Write straightforward code - framework handles optimization
|
|
705
|
+
|
|
706
|
+
✅ **Better Dependency Control:**
|
|
707
|
+
- All dependencies visible in component structure
|
|
708
|
+
- Clear data flow: props → actions → events → view
|
|
709
|
+
- Easy to trace what affects what
|
|
710
|
+
|
|
711
|
+
✅ **Easier to Maintain:**
|
|
712
|
+
- New developers understand the pattern immediately
|
|
713
|
+
- Changes are localized and predictable
|
|
714
|
+
- Refactoring is safer with explicit types
|
|
715
|
+
|
|
716
|
+
#### The Problem with Too Many Degrees of Freedom
|
|
717
|
+
|
|
718
|
+
Traditional React development offers **too many choices** for managing state and logic:
|
|
719
|
+
|
|
720
|
+
- Should I use `useState`? `useRef`? `useReducer`?
|
|
721
|
+
- Do I need Redux? MobX? Zustand? Jotai?
|
|
722
|
+
- Should state live in the component? In a context? In a global store?
|
|
723
|
+
- How should I handle derived state? `useMemo`? Computed values?
|
|
724
|
+
- What about side effects? `useEffect`? Custom hooks?
|
|
725
|
+
|
|
726
|
+
**The Result:** Each developer writes differently based on their:
|
|
727
|
+
- **Experience level** - beginners vs. experts make different choices
|
|
728
|
+
- **Habits** - "I always use Redux because that's what I learned"
|
|
729
|
+
- **Patterns from previous projects** - "We did it this way at my last job"
|
|
730
|
+
- **Stereotypes and misconceptions** - "Redux is better for large apps"
|
|
731
|
+
- **Personal taste** - "I prefer this pattern because it looks cleaner to me"
|
|
732
|
+
- **Laziness** - "This is faster to write, even if it's not optimal"
|
|
733
|
+
|
|
734
|
+
When your component architecture is built on **many different principles** and becomes **sophisticated**, understanding where a problem is hiding becomes extremely difficult. Different components use different approaches, making the codebase inconsistent and hard to reason about.
|
|
735
|
+
|
|
736
|
+
**When Problems Surface:**
|
|
737
|
+
- ❌ **Hard to detect** - Inconsistent patterns mask the root cause
|
|
738
|
+
- ❌ **Hard to debug** - Need to understand multiple different approaches
|
|
739
|
+
- ❌ **Hard to fix** - Often requires refactoring neighboring components
|
|
740
|
+
- ❌ **Hard to prevent** - No clear "right way" to implement features
|
|
741
|
+
|
|
742
|
+
**dynstruct's Solution: Consistency Through Constraints**
|
|
743
|
+
|
|
744
|
+
By providing **one clear way** to structure components:
|
|
745
|
+
- ✅ All components follow the same pattern
|
|
746
|
+
- ✅ Problems are easier to spot (deviations stand out)
|
|
747
|
+
- ✅ Fixes are localized (explicit dependencies)
|
|
748
|
+
- ✅ New developers onboard faster (consistent approach)
|
|
749
|
+
- ✅ Code reviews focus on logic, not architecture debates
|
|
750
|
+
|
|
751
|
+
The framework constrains your choices in a **productive way** - you have fewer decisions to make, but those constraints guide you toward maintainable, scalable code.
|
|
752
|
+
|
|
753
|
+
### Performance Characteristics
|
|
754
|
+
|
|
755
|
+
- ✅ **Stable references** - `.View` components created once
|
|
756
|
+
- ✅ **Automatic batching** - Multiple property updates batched automatically
|
|
757
|
+
- ✅ **Precise reactivity** - Only properties used in view trigger re-renders
|
|
758
|
+
- ✅ **No accidental dependencies** - Can't accidentally subscribe to wrong properties
|
|
759
|
+
- ✅ **Clear data flow** - Props → Events → Model changes → View updates
|
|
760
|
+
|
|
761
|
+
This separation means you can refactor logic, add validation, or change behavior without touching your JSX markup, and without worrying about performance pitfalls.
|
|
762
|
+
|
|
763
|
+
## Core Concepts
|
|
764
|
+
|
|
765
|
+
### Component Structure
|
|
766
|
+
|
|
767
|
+
The first step in the dynstruct architectural pattern is defining the **component structure**. The base generic class `ComponentStruct` acts as a structural constructor — a scaffold that provides constraints, hints, and full IntelliSense to the developer when forming the base type contract. All derived component model APIs are built on top of this contract through TypeScript's advanced type system.
|
|
768
|
+
|
|
769
|
+
**Crucially, component structures are pure type declarations** — they require no implementations (hook-constructors), only type information. This means you can define the entire application's component hierarchy at the type level before writing a single line of runtime code.
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
type Struct = ComponentStruct<
|
|
773
|
+
AppMsgStruct,
|
|
774
|
+
// The message bus structure that will serve as the basis for the
|
|
775
|
+
// component's msgBroker operation. This type maps to Struct["msg"].
|
|
776
|
+
{
|
|
777
|
+
props: {
|
|
778
|
+
// Names and types of component properties that will be reactive
|
|
779
|
+
// (including nested values) after the component is created.
|
|
780
|
+
counter: number;
|
|
781
|
+
message: string;
|
|
782
|
+
items: Item[];
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
actions: {
|
|
786
|
+
// Method signatures that perform operations on properties.
|
|
787
|
+
// Action calls are optimized for batching reactive property
|
|
788
|
+
// change application.
|
|
789
|
+
increment: () => void;
|
|
790
|
+
updateMessage: (text: string) => void;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
children: {
|
|
794
|
+
// Names and types of child components.
|
|
795
|
+
// Types are base structures (similar to this one) of other components.
|
|
796
|
+
// No implementations (hook-constructors) are required to form the
|
|
797
|
+
// structure — only type data.
|
|
798
|
+
header: HeaderStruct;
|
|
799
|
+
footer: FooterStruct;
|
|
800
|
+
todoList: TodoListStruct;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
msgScope: {
|
|
804
|
+
// Message bus channel names this component works with.
|
|
805
|
+
// Divided into sections: subscribe, publish, provide.
|
|
806
|
+
// See @actdim/msgmesh documentation for details.
|
|
807
|
+
//
|
|
808
|
+
// msgScope narrows the bus working area (it is normal to use a
|
|
809
|
+
// global app-wide bus) to this component's zone of responsibility.
|
|
810
|
+
// This not only makes working with the bus more convenient
|
|
811
|
+
// (the namespace is not polluted by other channels), but also
|
|
812
|
+
// lets you immediately see the component's message scope.
|
|
813
|
+
|
|
814
|
+
// Channels this component subscribes to (consumes messages from)
|
|
815
|
+
subscribe: AppMsgChannels<'USER-UPDATED' | 'DATA-LOADED'>;
|
|
816
|
+
|
|
817
|
+
// Channels this component publishes messages to
|
|
818
|
+
publish: AppMsgChannels<'FORM-SUBMITTED'>;
|
|
819
|
+
|
|
820
|
+
// Channels for which this component is a response-message
|
|
821
|
+
// provider ("out" groups) for request-messages ("in" groups)
|
|
822
|
+
provide: AppMsgChannels<'GET-USER-DATA' | 'VALIDATE-INPUT'>;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// List of effect names that will be available in this component.
|
|
826
|
+
// Effect implementations are defined in ComponentDef (see below).
|
|
827
|
+
effects: ['loadData', 'syncState'];
|
|
828
|
+
}
|
|
829
|
+
>;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
| Field | Description |
|
|
833
|
+
|---|---|
|
|
834
|
+
| `props` | Reactive property names and types. All declared properties (including nested values) become reactive after component creation. |
|
|
835
|
+
| `actions` | Method signatures that operate on props. Action calls are optimized for batching reactive property change application. |
|
|
836
|
+
| `children` | Names and types of child components. Uses base structures of other components — **no implementations required, only type data**. |
|
|
837
|
+
| `msgScope` | Message bus channels this component works with. Sections: `subscribe` (incoming message subscriptions), `publish` (outgoing message channels), `provide` (response provider for request-messages). Narrows the global bus scope to this component's responsibility zone. See [@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh) documentation. |
|
|
838
|
+
| `effects` | List of effect names available in this component. Implementations are defined in `ComponentDef`. |
|
|
839
|
+
|
|
840
|
+
### Component Definition
|
|
841
|
+
|
|
842
|
+
The component implementation is created inside a **hook-constructor** function (`use<ComponentName>`) using the `ComponentDef<Struct>` type. This is where you provide the runtime implementation for the contract declared in the structure:
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
const useMyComponent = (params: ComponentParams<Struct>) => {
|
|
846
|
+
let c: Component<Struct>;
|
|
847
|
+
let m: ComponentModel<Struct>;
|
|
848
|
+
|
|
849
|
+
const def: ComponentDef<Struct> = {
|
|
850
|
+
// Component type identifier used when registering in the component tree.
|
|
851
|
+
// Also used to form the component instance ID, which can be used
|
|
852
|
+
// (manually) as an HTML id in the component's markup.
|
|
853
|
+
regType: 'MyComponent',
|
|
854
|
+
|
|
855
|
+
props: {
|
|
856
|
+
// Initial values for properties (types match those declared
|
|
857
|
+
// in the component structure).
|
|
858
|
+
counter: params.counter ?? 0,
|
|
859
|
+
message: params.message ?? 'Hello',
|
|
860
|
+
items: [],
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
actions: {
|
|
864
|
+
// Method implementations (signatures match those declared
|
|
865
|
+
// in the component structure). Actions perform operations on
|
|
866
|
+
// properties; their calls are optimized for batching reactive
|
|
867
|
+
// property change application.
|
|
868
|
+
increment: () => { m.counter++; },
|
|
869
|
+
updateMessage: (text) => { m.message = text; },
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
effects: {
|
|
873
|
+
// Effect implementations. Effects are methods similar to actions
|
|
874
|
+
// (or they simply call actions), but they run automatically as
|
|
875
|
+
// soon as any property accessed within the effect implementation
|
|
876
|
+
// changes.
|
|
877
|
+
//
|
|
878
|
+
// Effects are accessed on the component instance by name via
|
|
879
|
+
// the `effects` property (e.g. c.effects.loadData).
|
|
880
|
+
//
|
|
881
|
+
// An effect runs immediately when the component is created and
|
|
882
|
+
// can later be manually paused, resumed, or stopped entirely.
|
|
883
|
+
loadData: (component) => {
|
|
884
|
+
console.log('Items count:', m.items.length);
|
|
885
|
+
// Return an optional cleanup function
|
|
886
|
+
return () => { /* cleanup */ };
|
|
887
|
+
},
|
|
888
|
+
syncState: (component) => {
|
|
889
|
+
console.log('Counter is', m.counter);
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
children: {
|
|
894
|
+
// Child component instances created via their hook-constructors
|
|
895
|
+
// (use*). When creating children you can initialize their
|
|
896
|
+
// properties, including bindings, and assign additional (external)
|
|
897
|
+
// event handlers.
|
|
898
|
+
header: useHeader({ title: bind(() => m.message) }),
|
|
899
|
+
footer: useFooter({ year: 2025 }),
|
|
900
|
+
todoList: useTodoList({
|
|
901
|
+
items: bind(
|
|
902
|
+
() => m.items,
|
|
903
|
+
v => { m.items = v; }
|
|
904
|
+
),
|
|
905
|
+
}),
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
events: {
|
|
909
|
+
// Component event handlers. The type system offers a choice of
|
|
910
|
+
// all supported events. See the Component Events section below
|
|
911
|
+
// for the full list.
|
|
912
|
+
onInit: (component) => { console.log('Initialized'); },
|
|
913
|
+
onChangeCounter: (value) => {
|
|
914
|
+
if (value > 100) m.message = 'Counter is high!';
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
msgBroker: {
|
|
919
|
+
// Message bus handlers declared in the component structure.
|
|
920
|
+
// Defined by channels and groups in sections:
|
|
921
|
+
provide: {
|
|
922
|
+
// Response-message providers ("out" groups)
|
|
923
|
+
// for request-messages ("in" groups).
|
|
924
|
+
'GET-USER-DATA': {
|
|
925
|
+
in: {
|
|
926
|
+
callback: (msgIn, headers, component) => {
|
|
927
|
+
return { userId: '1', name: 'Alice', email: 'a@b.c' };
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
subscribe: {
|
|
933
|
+
// Handlers for incoming messages.
|
|
934
|
+
'USER-UPDATED': {
|
|
935
|
+
in: {
|
|
936
|
+
callback: (msg, component) => {
|
|
937
|
+
console.log('User updated:', msg.payload);
|
|
938
|
+
},
|
|
939
|
+
componentFilter: ComponentMsgFilter.FromDescendants,
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
// Message bus instance. If not specified, the bus from the
|
|
946
|
+
// available component model context will be used.
|
|
947
|
+
// The bus must be compatible with the message structure
|
|
948
|
+
// declared in the component structure.
|
|
949
|
+
msgBus: undefined,
|
|
950
|
+
|
|
951
|
+
// Component render function that produces the view (JSX).
|
|
952
|
+
// Uses automatic JSX components created for child components
|
|
953
|
+
// (accessed via component.children.*.View).
|
|
954
|
+
// This function is intended to be compact since all wiring
|
|
955
|
+
// and initialization code is distributed across other
|
|
956
|
+
// definition areas. Inline capability exists but is mainly
|
|
957
|
+
// for embedding dynstruct components into regular ones.
|
|
958
|
+
view: (_, c) => (
|
|
959
|
+
<div>
|
|
960
|
+
<h3>{m.message}</h3>
|
|
961
|
+
<p>Counter: {m.counter}</p>
|
|
962
|
+
<c.children.header.View />
|
|
963
|
+
<c.children.todoList.View />
|
|
964
|
+
<c.children.footer.View />
|
|
965
|
+
</div>
|
|
966
|
+
),
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
c = useComponent(def, params);
|
|
970
|
+
m = c.model;
|
|
971
|
+
return c;
|
|
972
|
+
};
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
| Field | Description |
|
|
976
|
+
|---|---|
|
|
977
|
+
| `regType` | Component type identifier used when registering in the component tree. Also used to form the instance ID (can be used as HTML `id`). |
|
|
978
|
+
| `props` | Initial property values (types match the component structure). |
|
|
979
|
+
| `actions` | Method implementations (signatures match the structure). Optimized for batching reactive property changes. |
|
|
980
|
+
| `effects` | Effect implementations — methods that run automatically when any property accessed within them changes. An effect runs on component creation and can be paused, resumed, or stopped via `c.effects.<name>`. Returns an optional cleanup function. |
|
|
981
|
+
| `children` | Child component instances created via hook-constructors (`use*`). Properties can be initialized with values or bindings; external event handlers can be assigned. |
|
|
982
|
+
| `events` | Component event handlers (lifecycle, property changes). See [Component Events](#component-events). |
|
|
983
|
+
| `msgBroker` | Message bus handlers for channels declared in the structure. Contains `provide` (response providers) and `subscribe` (message handlers) sections. |
|
|
984
|
+
| `msgBus` | Explicit message bus instance. If omitted, the bus from the component model context is used. Must be compatible with the declared message structure. |
|
|
985
|
+
| `view` | Render function producing the component's JSX view. Child components are rendered via `c.children.<name>.View`. Intended to be compact — logic is distributed across other definition areas. |
|
|
986
|
+
|
|
987
|
+
### Reactive Properties
|
|
988
|
+
|
|
989
|
+
Component properties are **automatically reactive** after component creation with `useComponent`. Any changes to properties will trigger UI updates:
|
|
990
|
+
|
|
991
|
+
```typescript
|
|
992
|
+
const def: ComponentDef<Struct> = {
|
|
993
|
+
props: {
|
|
994
|
+
counter: 0,
|
|
995
|
+
message: 'Hello'
|
|
996
|
+
},
|
|
997
|
+
actions: {
|
|
998
|
+
increment: () => {
|
|
999
|
+
m.counter++; // Automatically triggers re-render
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
const c = useComponent(def, params);
|
|
1005
|
+
const m = c.model; // m.counter and m.message are reactive
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### Bindings to External State
|
|
1009
|
+
|
|
1010
|
+
Use **bindings** to connect component properties to external state or parent properties:
|
|
1011
|
+
|
|
1012
|
+
```typescript
|
|
1013
|
+
import { bind } from '@actdim/dynstruct/componentModel/core';
|
|
1014
|
+
|
|
1015
|
+
// Example 1: Binding to external state
|
|
1016
|
+
const appState = { userName: 'John' };
|
|
1017
|
+
|
|
1018
|
+
const binding = bind(
|
|
1019
|
+
() => appState.userName, // getter
|
|
1020
|
+
(v) => { appState.userName = v; } // setter
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
// Example 2: Binding to parent component's property (typical pattern)
|
|
1024
|
+
children: {
|
|
1025
|
+
messageInput: useInput({
|
|
1026
|
+
value: bind(
|
|
1027
|
+
() => m.message, // getter from parent model
|
|
1028
|
+
v => { m.message = v; } // setter to parent model
|
|
1029
|
+
)
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### Message Bus Communication
|
|
1035
|
+
|
|
1036
|
+
dynstruct integrates with **[@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh)**, a powerful type-safe message bus library that enables decoupled component communication.
|
|
1037
|
+
|
|
1038
|
+
#### Key Benefits
|
|
1039
|
+
|
|
1040
|
+
✅ **Type-Safe Channels** - No magic strings, full IntelliSense for channel names
|
|
1041
|
+
✅ **Local Message Namespaces** - Component structure declares only relevant channels
|
|
1042
|
+
✅ **Clear Component Responsibilities** - Message scope shows what component consumes/provides
|
|
1043
|
+
✅ **Component Independence** - Components communicate without direct references
|
|
1044
|
+
✅ **Testability** - Message bus can be easily mocked
|
|
1045
|
+
✅ **Flexible Routing** - Connect any components, not just parent-child
|
|
1046
|
+
|
|
1047
|
+
#### Step 1: Define Global Message Channels
|
|
1048
|
+
|
|
1049
|
+
First, declare message channels at the application (or domain) level with full typing:
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
import { MsgStructFactory, MsgBus } from '@actdim/msgmesh/contracts';
|
|
1053
|
+
import { createMsgBus } from '@actdim/msgmesh/core';
|
|
1054
|
+
import { BaseAppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1055
|
+
|
|
1056
|
+
// Define your application's message structure
|
|
1057
|
+
export type AppMsgStruct = BaseAppMsgStruct<AppRoutes> &
|
|
1058
|
+
MsgStructFactory<{
|
|
1059
|
+
// Event message (fire-and-forget)
|
|
1060
|
+
'USER-CLICKED': {
|
|
1061
|
+
in: { buttonId: string; timestamp: number };
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// Request/response message
|
|
1065
|
+
'GET-USER-DATA': {
|
|
1066
|
+
in: { userId: string };
|
|
1067
|
+
out: { userId: string; name: string; email: string };
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// Event from child components
|
|
1071
|
+
'FORM-SUBMITTED': {
|
|
1072
|
+
in: { formData: Record<string, any> };
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// Service message
|
|
1076
|
+
'VALIDATE-EMAIL': {
|
|
1077
|
+
in: { email: string };
|
|
1078
|
+
out: { valid: boolean; error?: string };
|
|
1079
|
+
};
|
|
1080
|
+
}>;
|
|
1081
|
+
|
|
1082
|
+
// Create typed message bus
|
|
1083
|
+
export type AppMsgBus = MsgBus<AppMsgStruct, ComponentMsgHeaders>;
|
|
1084
|
+
|
|
1085
|
+
export function createAppMsgBus() {
|
|
1086
|
+
return createMsgBus<AppMsgStruct, ComponentMsgHeaders>({});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Helper for selecting channels in component structures
|
|
1090
|
+
export type AppMsgChannels<TChannel extends keyof AppMsgStruct | Array<keyof AppMsgStruct>> =
|
|
1091
|
+
KeysOf<AppMsgStruct, TChannel>;
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
#### Step 2: Declare Component's Message Scope
|
|
1095
|
+
|
|
1096
|
+
In **ComponentStruct**, explicitly declare which channels this component works with:
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
import { ComponentStruct } from '@actdim/dynstruct/componentModel/contracts';
|
|
1100
|
+
import { AppMsgStruct, AppMsgChannels } from './appDomain';
|
|
1101
|
+
|
|
1102
|
+
type UserPanelStruct = ComponentStruct<
|
|
1103
|
+
AppMsgStruct,
|
|
1104
|
+
{
|
|
1105
|
+
props: {
|
|
1106
|
+
userId: string;
|
|
1107
|
+
userData: UserData | null;
|
|
1108
|
+
};
|
|
1109
|
+
children: {
|
|
1110
|
+
submitButton: ButtonStruct;
|
|
1111
|
+
emailInput: InputStruct;
|
|
1112
|
+
};
|
|
1113
|
+
// Message scope - creates LOCAL namespace for this component
|
|
1114
|
+
msgScope: {
|
|
1115
|
+
// Channels this component SUBSCRIBES to (consumes)
|
|
1116
|
+
subscribe: AppMsgChannels<'USER-CLICKED' | 'FORM-SUBMITTED'>;
|
|
1117
|
+
|
|
1118
|
+
// Channels this component PROVIDES (request/response handlers)
|
|
1119
|
+
provide: AppMsgChannels<'GET-USER-DATA' | 'VALIDATE-EMAIL'>;
|
|
1120
|
+
|
|
1121
|
+
// Channels this component PUBLISHES to (sends)
|
|
1122
|
+
publish: AppMsgChannels<'USER-UPDATED'>;
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
>;
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**What This Achieves:**
|
|
1129
|
+
|
|
1130
|
+
🎯 **Local Namespace** - Component only sees relevant channels, not the entire global list
|
|
1131
|
+
📋 **Clear Responsibilities** - Message scope documents component's communication surface
|
|
1132
|
+
🔒 **Type Safety** - TypeScript ensures only declared channels can be used in msgBroker
|
|
1133
|
+
👀 **Better Project Visibility** - Easy to understand component's external dependencies
|
|
1134
|
+
🔗 **Communication Map** - Shows how components connect, alongside children references
|
|
1135
|
+
|
|
1136
|
+
#### Step 3: Implement Message Handlers
|
|
1137
|
+
|
|
1138
|
+
In **ComponentDef**, implement handlers for declared channels in `msgBroker`:
|
|
1139
|
+
|
|
1140
|
+
```typescript
|
|
1141
|
+
import { ComponentDef, ComponentMsgFilter } from '@actdim/dynstruct/componentModel/contracts';
|
|
1142
|
+
|
|
1143
|
+
const useUserPanel = (params: ComponentParams<UserPanelStruct>) => {
|
|
1144
|
+
let c: Component<UserPanelStruct>;
|
|
1145
|
+
let m: ComponentModel<UserPanelStruct>;
|
|
1146
|
+
|
|
1147
|
+
const def: ComponentDef<UserPanelStruct> = {
|
|
1148
|
+
props: {
|
|
1149
|
+
userId: params.userId ?? '',
|
|
1150
|
+
userData: null
|
|
1151
|
+
},
|
|
1152
|
+
|
|
1153
|
+
msgBroker: {
|
|
1154
|
+
// SUBSCRIBE handlers - react to events from other components
|
|
1155
|
+
subscribe: {
|
|
1156
|
+
'USER-CLICKED': {
|
|
1157
|
+
in: {
|
|
1158
|
+
callback: (msg, component) => {
|
|
1159
|
+
console.log('User clicked button:', msg.payload.buttonId);
|
|
1160
|
+
// Update component state
|
|
1161
|
+
// No runInAction needed!
|
|
1162
|
+
},
|
|
1163
|
+
// Filter messages by source
|
|
1164
|
+
componentFilter: ComponentMsgFilter.FromDescendants
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
|
|
1168
|
+
'FORM-SUBMITTED': {
|
|
1169
|
+
in: {
|
|
1170
|
+
callback: (msg, component) => {
|
|
1171
|
+
const formData = msg.payload.formData;
|
|
1172
|
+
// Handle form submission
|
|
1173
|
+
m.userData = { ...m.userData, ...formData };
|
|
1174
|
+
},
|
|
1175
|
+
componentFilter: ComponentMsgFilter.FromDescendants
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
|
|
1180
|
+
// PROVIDE handlers - respond to requests from other components
|
|
1181
|
+
provide: {
|
|
1182
|
+
'GET-USER-DATA': {
|
|
1183
|
+
in: {
|
|
1184
|
+
callback: (msgIn, headers, component) => {
|
|
1185
|
+
// Return response data
|
|
1186
|
+
return {
|
|
1187
|
+
userId: m.userId,
|
|
1188
|
+
name: m.userData?.name ?? '',
|
|
1189
|
+
email: m.userData?.email ?? ''
|
|
1190
|
+
};
|
|
1191
|
+
},
|
|
1192
|
+
componentFilter: ComponentMsgFilter.FromDescendants
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
|
|
1196
|
+
'VALIDATE-EMAIL': {
|
|
1197
|
+
in: {
|
|
1198
|
+
callback: (msgIn, headers, component) => {
|
|
1199
|
+
const email = msgIn.payload.email;
|
|
1200
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1201
|
+
return {
|
|
1202
|
+
valid: emailRegex.test(email),
|
|
1203
|
+
error: emailRegex.test(email) ? undefined : 'Invalid email format'
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
|
|
1211
|
+
children: {
|
|
1212
|
+
submitButton: useButton({
|
|
1213
|
+
label: 'Submit',
|
|
1214
|
+
onClick: () => {
|
|
1215
|
+
// SEND event (fire-and-forget)
|
|
1216
|
+
c.msgBus.send({
|
|
1217
|
+
channel: 'FORM-SUBMITTED',
|
|
1218
|
+
payload: { formData: { name: 'Alice' } }
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}),
|
|
1222
|
+
emailInput: useInput({
|
|
1223
|
+
value: bind(() => m.userData?.email ?? '', v => {
|
|
1224
|
+
m.userData = { ...m.userData, email: v };
|
|
1225
|
+
})
|
|
1226
|
+
})
|
|
1227
|
+
},
|
|
1228
|
+
|
|
1229
|
+
view: (_, c) => (
|
|
1230
|
+
<div>
|
|
1231
|
+
<c.children.emailInput.View />
|
|
1232
|
+
<c.children.submitButton.View />
|
|
1233
|
+
</div>
|
|
1234
|
+
)
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
c = useComponent(def, params);
|
|
1238
|
+
m = c.model;
|
|
1239
|
+
return c;
|
|
1240
|
+
};
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
#### Step 4: Send Messages and Make Requests
|
|
1244
|
+
|
|
1245
|
+
Components use their `msgBus` to send events or make requests:
|
|
1246
|
+
|
|
1247
|
+
```typescript
|
|
1248
|
+
// Send event (fire-and-forget)
|
|
1249
|
+
c.msgBus.send({
|
|
1250
|
+
channel: 'USER-CLICKED',
|
|
1251
|
+
payload: { buttonId: 'btn-1', timestamp: Date.now() }
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
// Request/response pattern (async)
|
|
1255
|
+
const response = await c.msgBus.request({
|
|
1256
|
+
channel: 'GET-USER-DATA',
|
|
1257
|
+
payload: { userId: '123' }
|
|
1258
|
+
});
|
|
1259
|
+
console.log('User data:', response.payload);
|
|
1260
|
+
|
|
1261
|
+
// Request with timeout
|
|
1262
|
+
const validationResult = await c.msgBus.request(
|
|
1263
|
+
{
|
|
1264
|
+
channel: 'VALIDATE-EMAIL',
|
|
1265
|
+
payload: { email: 'test@example.com' }
|
|
1266
|
+
},
|
|
1267
|
+
{ timeout: 5000 }
|
|
1268
|
+
);
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
#### Message Filtering
|
|
1272
|
+
|
|
1273
|
+
Use **ComponentMsgFilter** to control which components can send messages to your handlers:
|
|
1274
|
+
|
|
1275
|
+
```typescript
|
|
1276
|
+
import { ComponentMsgFilter } from '@actdim/dynstruct/componentModel/contracts';
|
|
1277
|
+
|
|
1278
|
+
msgBroker: {
|
|
1279
|
+
subscribe: {
|
|
1280
|
+
'USER-CLICKED': {
|
|
1281
|
+
in: {
|
|
1282
|
+
callback: (msg) => { /* ... */ },
|
|
1283
|
+
componentFilter: ComponentMsgFilter.FromDescendants // Only from children
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
'ADMIN-ACTION': {
|
|
1287
|
+
in: {
|
|
1288
|
+
callback: (msg) => { /* ... */ },
|
|
1289
|
+
componentFilter: ComponentMsgFilter.FromAncestors // Only from parents
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
'GLOBAL-EVENT': {
|
|
1293
|
+
in: {
|
|
1294
|
+
callback: (msg) => { /* ... */ },
|
|
1295
|
+
componentFilter: ComponentMsgFilter.FromBus // From anywhere
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**Available Filters:**
|
|
1303
|
+
- `FromDescendants` - Only messages from child components
|
|
1304
|
+
- `FromAncestors` - Only messages from parent/ancestor components
|
|
1305
|
+
- `FromSelf` - Only messages from this component
|
|
1306
|
+
- `FromBus` - Messages from anywhere in the application
|
|
1307
|
+
|
|
1308
|
+
#### Real-World Example
|
|
1309
|
+
|
|
1310
|
+
See [TestContainer.tsx](src/_stories/componentModel/TestContainer.tsx) for a complete example:
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
// Structure declares message scope
|
|
1314
|
+
type Struct = ComponentStruct<
|
|
1315
|
+
AppMsgStruct,
|
|
1316
|
+
{
|
|
1317
|
+
props: { text: string };
|
|
1318
|
+
children: {
|
|
1319
|
+
child1: TestChildStruct;
|
|
1320
|
+
child2: TestChildStruct;
|
|
1321
|
+
};
|
|
1322
|
+
msgScope: {
|
|
1323
|
+
subscribe: AppMsgChannels<'TEST-EVENT'>;
|
|
1324
|
+
provide: AppMsgChannels<'LOCAL-EVENT'>;
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
>;
|
|
1328
|
+
|
|
1329
|
+
const def: ComponentDef<Struct> = {
|
|
1330
|
+
props: { text: '' },
|
|
1331
|
+
|
|
1332
|
+
msgBroker: {
|
|
1333
|
+
subscribe: {
|
|
1334
|
+
'TEST-EVENT': {
|
|
1335
|
+
in: {
|
|
1336
|
+
callback: (msg, c) => {
|
|
1337
|
+
m.text = msg.payload;
|
|
1338
|
+
},
|
|
1339
|
+
componentFilter: ComponentMsgFilter.FromDescendants
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
provide: {
|
|
1344
|
+
'LOCAL-EVENT': {
|
|
1345
|
+
in: {
|
|
1346
|
+
callback: (msgIn, headers, c) => {
|
|
1347
|
+
return `Hi ${msgIn.payload} from parent ${c.id}!`;
|
|
1348
|
+
},
|
|
1349
|
+
componentFilter: ComponentMsgFilter.FromDescendants
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
#### Testing and Mocking
|
|
1358
|
+
|
|
1359
|
+
The message bus can be easily mocked for testing:
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
import { createMsgBus } from '@actdim/msgmesh/core';
|
|
1363
|
+
|
|
1364
|
+
// Create mock bus for testing
|
|
1365
|
+
const mockMsgBus = createMsgBus<AppMsgStruct, ComponentMsgHeaders>({});
|
|
1366
|
+
|
|
1367
|
+
// Spy on messages
|
|
1368
|
+
const sendSpy = jest.spyOn(mockMsgBus, 'send');
|
|
1369
|
+
|
|
1370
|
+
// Test component
|
|
1371
|
+
const component = useComponent(def, { msgBus: mockMsgBus });
|
|
1372
|
+
|
|
1373
|
+
// Verify message was sent
|
|
1374
|
+
expect(sendSpy).toHaveBeenCalledWith({
|
|
1375
|
+
channel: 'USER-CLICKED',
|
|
1376
|
+
payload: expect.any(Object)
|
|
1377
|
+
});
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
#### Why This Approach is Powerful
|
|
1381
|
+
|
|
1382
|
+
**1. Type Safety Without Magic Strings**
|
|
1383
|
+
- All channels defined in one place with full typing
|
|
1384
|
+
- IntelliSense shows available channels
|
|
1385
|
+
- Compile-time errors for typos
|
|
1386
|
+
|
|
1387
|
+
**2. Clear Component Boundaries**
|
|
1388
|
+
- `msgScope` documents component's external communication
|
|
1389
|
+
- Easy to see what component consumes/provides
|
|
1390
|
+
- Reduces cognitive load when reading code
|
|
1391
|
+
|
|
1392
|
+
**3. Loose Coupling**
|
|
1393
|
+
- Components communicate without direct references
|
|
1394
|
+
- Easy to add/remove components
|
|
1395
|
+
- Services can be swapped without changing component code
|
|
1396
|
+
|
|
1397
|
+
**4. Better Project Visibility**
|
|
1398
|
+
- Structure shows children dependencies (direct composition)
|
|
1399
|
+
- Structure shows message dependencies (loose coupling)
|
|
1400
|
+
- Complete picture of component's responsibilities
|
|
1401
|
+
|
|
1402
|
+
**5. Testability**
|
|
1403
|
+
- Message bus can be mocked
|
|
1404
|
+
- Test components in isolation
|
|
1405
|
+
- Verify message contracts
|
|
1406
|
+
|
|
1407
|
+
**6. Flexibility**
|
|
1408
|
+
- Connect any components (not just parent-child)
|
|
1409
|
+
- Route messages through component hierarchy
|
|
1410
|
+
- Filter by source with ComponentMsgFilter
|
|
1411
|
+
- Support both events and request/response patterns
|
|
1412
|
+
|
|
1413
|
+
### Parent-Child Relationships
|
|
1414
|
+
|
|
1415
|
+
Components can access their hierarchy:
|
|
1416
|
+
|
|
1417
|
+
```typescript
|
|
1418
|
+
// Define parent with children
|
|
1419
|
+
const parentDef: ComponentDef<ParentStruct> = {
|
|
1420
|
+
children: {
|
|
1421
|
+
child1: useChildComponent({ /* params */ }),
|
|
1422
|
+
child2: useChildComponent({ /* params */ })
|
|
1423
|
+
},
|
|
1424
|
+
view: (_, c) => (
|
|
1425
|
+
<div>
|
|
1426
|
+
<c.children.child1.View />
|
|
1427
|
+
<c.children.child2.View />
|
|
1428
|
+
</div>
|
|
1429
|
+
)
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
// Access from child component
|
|
1433
|
+
const parentId = component.getParent();
|
|
1434
|
+
const ancestors = component.getChainUp();
|
|
1435
|
+
const descendants = component.getChainDown();
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
### Component Events
|
|
1439
|
+
|
|
1440
|
+
The component model provides **automatic type-safe event handlers** for the component lifecycle and property changes. IntelliSense automatically suggests all available events based on the component structure.
|
|
1441
|
+
|
|
1442
|
+
The full set of supported events is defined by the `ComponentEvents<TStruct>` type and is divided into three groups: **lifecycle events**, **global property change events**, and **property-specific events**.
|
|
1443
|
+
|
|
1444
|
+
#### Lifecycle Events
|
|
1445
|
+
|
|
1446
|
+
| Event | Phase | Description |
|
|
1447
|
+
|---|---|---|
|
|
1448
|
+
| `onInit` | preMount | Initialization event. Called after props and children are set up, but before the HTML representation is inserted into the DOM. |
|
|
1449
|
+
| `onLayoutReady` | mount | The HTML representation is ready and inserted into the DOM tree, but the frame has not been painted yet. |
|
|
1450
|
+
| `onReady` | postMount | The HTML representation has already been rendered and is visible to the user. The component is fully ready for interaction. |
|
|
1451
|
+
| `onLayoutDestroy` | preUnmount | The component's HTML representation is about to be removed from the DOM. |
|
|
1452
|
+
| `onDestroy` | unmount | The component is destroyed. All resources should be released. |
|
|
1453
|
+
| `onError` | — | An error occurred during component operation. Receives the error object and optional info. |
|
|
1454
|
+
|
|
1455
|
+
```typescript
|
|
1456
|
+
const def: ComponentDef<Struct> = {
|
|
1457
|
+
events: {
|
|
1458
|
+
// Initialization (preMount)
|
|
1459
|
+
onInit: (component) => {
|
|
1460
|
+
console.log('Component initialized:', component.id);
|
|
1461
|
+
},
|
|
1462
|
+
|
|
1463
|
+
// HTML inserted into DOM, frame not yet painted (mount)
|
|
1464
|
+
onLayoutReady: (component) => {
|
|
1465
|
+
console.log('Component layout ready');
|
|
1466
|
+
},
|
|
1467
|
+
|
|
1468
|
+
// HTML rendered and visible (postMount)
|
|
1469
|
+
onReady: (component) => {
|
|
1470
|
+
console.log('Component is ready for interaction');
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1473
|
+
// HTML representation about to be removed from DOM
|
|
1474
|
+
onLayoutDestroy: (component) => {
|
|
1475
|
+
console.log('Layout will be destroyed');
|
|
1476
|
+
},
|
|
1477
|
+
|
|
1478
|
+
// Component destroyed
|
|
1479
|
+
onDestroy: (component) => {
|
|
1480
|
+
console.log('Component destroyed');
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
// Error during component operation
|
|
1484
|
+
onError: (component, error) => {
|
|
1485
|
+
console.error('Component error:', error);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
#### Global Property Change Events
|
|
1492
|
+
|
|
1493
|
+
These events fire when **any** reactive property changes. Useful for cross-cutting concerns like logging, validation, or synchronization.
|
|
1494
|
+
|
|
1495
|
+
| Event | Description |
|
|
1496
|
+
|---|---|
|
|
1497
|
+
| `onPropChanging` | Fires before any reactive property changes. Return `false` to cancel the change. |
|
|
1498
|
+
| `onPropChange` | Fires after any reactive property has changed. |
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
const def: ComponentDef<Struct> = {
|
|
1502
|
+
events: {
|
|
1503
|
+
// Before ANY property changes — return false to cancel
|
|
1504
|
+
onPropChanging: (propName, oldValue, newValue) => {
|
|
1505
|
+
console.log(`Property ${propName} changing:`, oldValue, '->', newValue);
|
|
1506
|
+
return newValue !== null; // cancel if null
|
|
1507
|
+
},
|
|
1508
|
+
|
|
1509
|
+
// After ANY property has changed
|
|
1510
|
+
onPropChange: (propName, value) => {
|
|
1511
|
+
console.log(`Property ${propName} changed to:`, value);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
#### Property-Specific Events (Automatically Typed)
|
|
1518
|
+
|
|
1519
|
+
For each property declared in `props`, the type system **automatically generates** typed event handler slots. IntelliSense provides suggestions for all properties.
|
|
1520
|
+
|
|
1521
|
+
| Event pattern | Description |
|
|
1522
|
+
|---|---|
|
|
1523
|
+
| `onGet<PropName>` | Getter interceptor — called when the property is read. Returns the value. |
|
|
1524
|
+
| `onChanging<PropName>` | Fires before a specific property changes. Return `false` to cancel the change. |
|
|
1525
|
+
| `onChange<PropName>` | Fires after a specific property has changed. |
|
|
1526
|
+
|
|
1527
|
+
```typescript
|
|
1528
|
+
type MyStruct = ComponentStruct<AppMsgStruct, {
|
|
1529
|
+
props: {
|
|
1530
|
+
counter: number;
|
|
1531
|
+
text: string;
|
|
1532
|
+
isActive: boolean;
|
|
1533
|
+
};
|
|
1534
|
+
}>;
|
|
1535
|
+
|
|
1536
|
+
const def: ComponentDef<MyStruct> = {
|
|
1537
|
+
props: {
|
|
1538
|
+
counter: 0,
|
|
1539
|
+
text: '',
|
|
1540
|
+
isActive: false
|
|
1541
|
+
},
|
|
1542
|
+
events: {
|
|
1543
|
+
// IntelliSense automatically suggests these based on props!
|
|
1544
|
+
|
|
1545
|
+
// Getter interceptor — called when property is read
|
|
1546
|
+
onGetCounter: () => {
|
|
1547
|
+
console.log('Counter was read');
|
|
1548
|
+
return m.counter;
|
|
1549
|
+
},
|
|
1550
|
+
|
|
1551
|
+
// Before a specific property changes — return false to cancel
|
|
1552
|
+
onChangingText: (oldValue, newValue) => {
|
|
1553
|
+
console.log('Text changing:', oldValue, '->', newValue);
|
|
1554
|
+
return newValue.trim(); // sanitize input
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
// After a specific property has changed
|
|
1558
|
+
onChangeText: (value) => {
|
|
1559
|
+
console.log('Text changed to:', value);
|
|
1560
|
+
c.children.child1.model.value = value;
|
|
1561
|
+
},
|
|
1562
|
+
|
|
1563
|
+
onChangeIsActive: (value) => {
|
|
1564
|
+
if (value) {
|
|
1565
|
+
console.log('Component activated!');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
#### Real-World Example
|
|
1573
|
+
|
|
1574
|
+
```typescript
|
|
1575
|
+
type FormStruct = ComponentStruct<AppMsgStruct, {
|
|
1576
|
+
props: {
|
|
1577
|
+
email: string;
|
|
1578
|
+
password: string;
|
|
1579
|
+
isValid: boolean;
|
|
1580
|
+
};
|
|
1581
|
+
children: {
|
|
1582
|
+
emailInput: InputStruct;
|
|
1583
|
+
passwordInput: InputStruct;
|
|
1584
|
+
};
|
|
1585
|
+
}>;
|
|
1586
|
+
|
|
1587
|
+
const useForm = (params: ComponentParams<FormStruct>) => {
|
|
1588
|
+
let c: Component<FormStruct>;
|
|
1589
|
+
let m: ComponentModel<FormStruct>;
|
|
1590
|
+
|
|
1591
|
+
const def: ComponentDef<FormStruct> = {
|
|
1592
|
+
props: {
|
|
1593
|
+
email: '',
|
|
1594
|
+
password: '',
|
|
1595
|
+
isValid: false
|
|
1596
|
+
},
|
|
1597
|
+
events: {
|
|
1598
|
+
// Validate email when it changes
|
|
1599
|
+
onChangeEmail: (oldValue, newValue) => {
|
|
1600
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1601
|
+
m.isValid = emailRegex.test(newValue) && m.password.length >= 6;
|
|
1602
|
+
},
|
|
1603
|
+
|
|
1604
|
+
// Validate password when it changes
|
|
1605
|
+
onChangePassword: (oldValue, newValue) => {
|
|
1606
|
+
m.isValid = m.email.includes('@') && newValue.length >= 6;
|
|
1607
|
+
},
|
|
1608
|
+
|
|
1609
|
+
// Sanitize input before setting
|
|
1610
|
+
onChangingEmail: (oldValue, newValue) => {
|
|
1611
|
+
return newValue.toLowerCase().trim();
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
children: {
|
|
1615
|
+
emailInput: useInput({
|
|
1616
|
+
value: bind(() => m.email, v => { m.email = v; })
|
|
1617
|
+
}),
|
|
1618
|
+
passwordInput: useInput({
|
|
1619
|
+
value: bind(() => m.password, v => { m.password = v; })
|
|
1620
|
+
})
|
|
1621
|
+
},
|
|
1622
|
+
view: (_, c) => (
|
|
1623
|
+
<div>
|
|
1624
|
+
<c.children.emailInput.View />
|
|
1625
|
+
<c.children.passwordInput.View />
|
|
1626
|
+
<button disabled={!m.isValid}>Submit</button>
|
|
1627
|
+
</div>
|
|
1628
|
+
)
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
c = useComponent(def, params);
|
|
1632
|
+
m = c.model;
|
|
1633
|
+
return c;
|
|
1634
|
+
};
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
**Key Benefits:**
|
|
1638
|
+
- ✅ **Full TypeScript IntelliSense** - event names are auto-generated from props
|
|
1639
|
+
- ✅ **Type-safe parameters** - correct types for old/new values
|
|
1640
|
+
- ✅ **Validation and sanitization** - intercept changes before they happen
|
|
1641
|
+
- ✅ **Synchronization** - keep parent and child components in sync
|
|
1642
|
+
- ✅ **Lifecycle hooks** - respond to component lifecycle stages
|
|
1643
|
+
|
|
1644
|
+
### Effects
|
|
1645
|
+
|
|
1646
|
+
Effects are **auto-tracking reactive functions**. An effect runs immediately when the component is created, and then **re-runs automatically** whenever any reactive property accessed inside it changes. Effect names must first be declared in the component structure, then implemented in `ComponentDef`.
|
|
1647
|
+
|
|
1648
|
+
Each effect is accessible on the component instance via `c.effects.<name>` and exposes an `EffectController` with three methods:
|
|
1649
|
+
|
|
1650
|
+
| Method | Description |
|
|
1651
|
+
|---|---|
|
|
1652
|
+
| `pause()` | Suspends the effect. Property changes are ignored until resumed. |
|
|
1653
|
+
| `resume()` | Resumes a paused effect and immediately re-evaluates it. |
|
|
1654
|
+
| `stop()` | Stops the effect entirely. It will not run again. |
|
|
1655
|
+
|
|
1656
|
+
An effect can optionally return a **cleanup function** that is called when the effect is stopped or the component is destroyed.
|
|
1657
|
+
|
|
1658
|
+
**Example** — computed `fullName` that auto-updates when `firstName` or `lastName` changes, with pause/resume control:
|
|
1659
|
+
|
|
1660
|
+
```typescript
|
|
1661
|
+
type Struct = ComponentStruct<AppMsgStruct, {
|
|
1662
|
+
props: {
|
|
1663
|
+
fullName: string;
|
|
1664
|
+
firstName: string;
|
|
1665
|
+
lastName: string;
|
|
1666
|
+
trackingEnabled: boolean;
|
|
1667
|
+
};
|
|
1668
|
+
children: {
|
|
1669
|
+
firstNameEdit: SimpleEditStruct;
|
|
1670
|
+
lastNameEdit: SimpleEditStruct;
|
|
1671
|
+
};
|
|
1672
|
+
// Declare effect names in the structure
|
|
1673
|
+
effects: 'trackNameChanges';
|
|
1674
|
+
}>;
|
|
1675
|
+
|
|
1676
|
+
const useEffectDemo = (params: ComponentParams<Struct>) => {
|
|
1677
|
+
let c: Component<Struct>;
|
|
1678
|
+
let m: ComponentModel<Struct>;
|
|
1679
|
+
|
|
1680
|
+
const def: ComponentDef<Struct> = {
|
|
1681
|
+
props: {
|
|
1682
|
+
fullName: undefined,
|
|
1683
|
+
firstName: 'John',
|
|
1684
|
+
lastName: 'Smith',
|
|
1685
|
+
trackingEnabled: true,
|
|
1686
|
+
},
|
|
1687
|
+
events: {
|
|
1688
|
+
// Toggle effect pause/resume via a property change event
|
|
1689
|
+
onChangeTrackingEnabled: (v) => {
|
|
1690
|
+
if (v) {
|
|
1691
|
+
c.effects.trackNameChanges.resume();
|
|
1692
|
+
} else {
|
|
1693
|
+
c.effects.trackNameChanges.pause();
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
effects: {
|
|
1698
|
+
// Runs immediately on creation, then re-runs whenever
|
|
1699
|
+
// m.firstName or m.lastName changes
|
|
1700
|
+
trackNameChanges: (c) => {
|
|
1701
|
+
m.fullName = `${m.firstName} ${m.lastName}`;
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
children: {
|
|
1705
|
+
firstNameEdit: useSimpleEdit({
|
|
1706
|
+
value: bindProp(() => m, 'firstName'),
|
|
1707
|
+
}),
|
|
1708
|
+
lastNameEdit: useSimpleEdit({
|
|
1709
|
+
value: bindProp(() => m, 'lastName'),
|
|
1710
|
+
}),
|
|
1711
|
+
},
|
|
1712
|
+
view: (_, c) => (
|
|
1713
|
+
<div id={c.id}>
|
|
1714
|
+
<div>First Name: <c.children.firstNameEdit.View /></div>
|
|
1715
|
+
<div>Last Name: <c.children.lastNameEdit.View /></div>
|
|
1716
|
+
<div>Full Name: {m.fullName}</div>
|
|
1717
|
+
{m.trackingEnabled
|
|
1718
|
+
? <button onClick={() => { m.trackingEnabled = false; }}>Pause</button>
|
|
1719
|
+
: <button onClick={() => { m.trackingEnabled = true; }}>Resume</button>
|
|
1720
|
+
}
|
|
1721
|
+
</div>
|
|
1722
|
+
),
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
c = useComponent(def, params);
|
|
1726
|
+
m = c.model;
|
|
1727
|
+
return c;
|
|
1728
|
+
};
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
In this example the `trackNameChanges` effect accesses `m.firstName` and `m.lastName`, so it re-runs whenever either changes. Clicking **Pause** calls `c.effects.trackNameChanges.pause()`, which suspends the auto-tracking — edits to the name fields no longer update `fullName` until **Resume** is clicked.
|
|
1732
|
+
|
|
1733
|
+
## Examples (React)
|
|
1734
|
+
|
|
1735
|
+
> **Note:** All examples below are for the **React** implementation.
|
|
1736
|
+
|
|
1737
|
+
### Example 1: Simple Counter Component
|
|
1738
|
+
|
|
1739
|
+
```typescript
|
|
1740
|
+
// React implementation
|
|
1741
|
+
import { ComponentStruct, ComponentDef, ComponentParams } from '@actdim/dynstruct/componentModel/contracts';
|
|
1742
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1743
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1744
|
+
|
|
1745
|
+
type CounterStruct = ComponentStruct<AppMsgStruct, {
|
|
1746
|
+
props: { count: number };
|
|
1747
|
+
actions: { increment: () => void; decrement: () => void };
|
|
1748
|
+
}>;
|
|
1749
|
+
|
|
1750
|
+
const useCounter = (params: ComponentParams<CounterStruct>) => {
|
|
1751
|
+
const def: ComponentDef<CounterStruct> = {
|
|
1752
|
+
props: { count: params.count ?? 0 },
|
|
1753
|
+
actions: {
|
|
1754
|
+
increment: () => { c.model.count++; },
|
|
1755
|
+
decrement: () => { c.model.count--; }
|
|
1756
|
+
},
|
|
1757
|
+
view: (_, c) => (
|
|
1758
|
+
<div>
|
|
1759
|
+
<h2>Counter: {c.model.count}</h2>
|
|
1760
|
+
<button onClick={c.actions.increment}>+</button>
|
|
1761
|
+
<button onClick={c.actions.decrement}>-</button>
|
|
1762
|
+
</div>
|
|
1763
|
+
)
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const c = useComponent(def, params);
|
|
1767
|
+
return c;
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
export const Counter = toReact(useCounter);
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
### Example 2: Component with Children
|
|
1774
|
+
|
|
1775
|
+
```typescript
|
|
1776
|
+
// React implementation
|
|
1777
|
+
import { ComponentStruct, ComponentDef, ComponentParams } from '@actdim/dynstruct/componentModel/contracts';
|
|
1778
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1779
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1780
|
+
|
|
1781
|
+
// Child component
|
|
1782
|
+
type ButtonStruct = ComponentStruct<AppMsgStruct, {
|
|
1783
|
+
props: { label: string; onClick: () => void };
|
|
1784
|
+
}>;
|
|
1785
|
+
|
|
1786
|
+
const useButton = (params: ComponentParams<ButtonStruct>) => {
|
|
1787
|
+
const def: ComponentDef<ButtonStruct> = {
|
|
1788
|
+
props: {
|
|
1789
|
+
label: params.label ?? 'Click',
|
|
1790
|
+
onClick: params.onClick ?? (() => {})
|
|
1791
|
+
},
|
|
1792
|
+
view: (_, c) => (
|
|
1793
|
+
<button onClick={c.model.onClick}>{c.model.label}</button>
|
|
1794
|
+
)
|
|
1795
|
+
};
|
|
1796
|
+
return useComponent(def, params);
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
// Parent component with children
|
|
1800
|
+
type PanelStruct = ComponentStruct<AppMsgStruct, {
|
|
1801
|
+
props: { title: string; clickCount: number };
|
|
1802
|
+
children: {
|
|
1803
|
+
okButton: ButtonStruct;
|
|
1804
|
+
cancelButton: ButtonStruct;
|
|
1805
|
+
};
|
|
1806
|
+
}>;
|
|
1807
|
+
|
|
1808
|
+
const usePanel = (params: ComponentParams<PanelStruct>) => {
|
|
1809
|
+
let c: Component<PanelStruct>;
|
|
1810
|
+
let m: ComponentModel<PanelStruct>;
|
|
1811
|
+
|
|
1812
|
+
const def: ComponentDef<PanelStruct> = {
|
|
1813
|
+
props: {
|
|
1814
|
+
title: params.title ?? 'Panel',
|
|
1815
|
+
clickCount: 0
|
|
1816
|
+
},
|
|
1817
|
+
children: {
|
|
1818
|
+
okButton: useButton({
|
|
1819
|
+
label: 'OK',
|
|
1820
|
+
onClick: () => {
|
|
1821
|
+
m.clickCount++; // Reactive property update
|
|
1822
|
+
console.log('OK clicked');
|
|
1823
|
+
}
|
|
1824
|
+
}),
|
|
1825
|
+
cancelButton: useButton({
|
|
1826
|
+
label: 'Cancel',
|
|
1827
|
+
onClick: () => console.log('Cancel clicked')
|
|
1828
|
+
})
|
|
1829
|
+
},
|
|
1830
|
+
view: (_, c) => (
|
|
1831
|
+
<div className="panel">
|
|
1832
|
+
<h3>{m.title}</h3>
|
|
1833
|
+
<p>Clicks: {m.clickCount}</p>
|
|
1834
|
+
<div className="buttons">
|
|
1835
|
+
<c.children.okButton.View />
|
|
1836
|
+
<c.children.cancelButton.View />
|
|
1837
|
+
</div>
|
|
1838
|
+
</div>
|
|
1839
|
+
)
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
c = useComponent(def, params);
|
|
1843
|
+
m = c.model;
|
|
1844
|
+
return c;
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
export const Panel = toReact(usePanel);
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
### Example 3: Message Bus Producer/Consumer
|
|
1851
|
+
|
|
1852
|
+
```typescript
|
|
1853
|
+
// React implementation
|
|
1854
|
+
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
1855
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1856
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1857
|
+
|
|
1858
|
+
// Producer Component
|
|
1859
|
+
type ProducerStruct = ComponentStruct<AppMsgStruct, {
|
|
1860
|
+
msgScope: {
|
|
1861
|
+
provide: {
|
|
1862
|
+
'EVENT-FIRED': { timestamp: number; data: string };
|
|
1863
|
+
};
|
|
1864
|
+
};
|
|
1865
|
+
}>;
|
|
1866
|
+
|
|
1867
|
+
const useProducer = (params: ComponentParams<ProducerStruct>) => {
|
|
1868
|
+
const def: ComponentDef<ProducerStruct> = {
|
|
1869
|
+
// msgBroker is part of ComponentDef
|
|
1870
|
+
msgBroker: {
|
|
1871
|
+
provide: {
|
|
1872
|
+
'EVENT-FIRED': {
|
|
1873
|
+
callback: () => ({
|
|
1874
|
+
timestamp: Date.now(),
|
|
1875
|
+
data: 'Event fired from producer'
|
|
1876
|
+
})
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
view: (_, c) => (
|
|
1881
|
+
<button onClick={() => {
|
|
1882
|
+
// Use component's msgBus to send
|
|
1883
|
+
c.msgBus.send({
|
|
1884
|
+
channel: 'EVENT-FIRED',
|
|
1885
|
+
payload: {}
|
|
1886
|
+
});
|
|
1887
|
+
}}>
|
|
1888
|
+
Fire Event
|
|
1889
|
+
</button>
|
|
1890
|
+
)
|
|
1891
|
+
};
|
|
1892
|
+
return useComponent(def, params);
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
export const Producer = toReact(useProducer);
|
|
1896
|
+
|
|
1897
|
+
// Consumer Component
|
|
1898
|
+
type ConsumerStruct = ComponentStruct<AppMsgStruct, {
|
|
1899
|
+
props: { lastEvent: string };
|
|
1900
|
+
msgScope: {
|
|
1901
|
+
subscribe: {
|
|
1902
|
+
'EVENT-FIRED': { timestamp: number; data: string };
|
|
1903
|
+
};
|
|
1904
|
+
};
|
|
1905
|
+
}>;
|
|
1906
|
+
|
|
1907
|
+
const useConsumer = (params: ComponentParams<ConsumerStruct>) => {
|
|
1908
|
+
let c: Component<ConsumerStruct>;
|
|
1909
|
+
let m: ComponentModel<ConsumerStruct>;
|
|
1910
|
+
|
|
1911
|
+
const def: ComponentDef<ConsumerStruct> = {
|
|
1912
|
+
props: { lastEvent: 'No events yet' },
|
|
1913
|
+
// msgBroker subscribes to messages
|
|
1914
|
+
msgBroker: {
|
|
1915
|
+
subscribe: {
|
|
1916
|
+
'EVENT-FIRED': {
|
|
1917
|
+
callback: (msg) => {
|
|
1918
|
+
// Update reactive property
|
|
1919
|
+
m.lastEvent = `${msg.payload.data} at ${new Date(msg.payload.timestamp).toLocaleTimeString()}`;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
view: (_, c) => (
|
|
1925
|
+
<div>
|
|
1926
|
+
<p>Last Event: {m.lastEvent}</p>
|
|
1927
|
+
</div>
|
|
1928
|
+
)
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
c = useComponent(def, params);
|
|
1932
|
+
m = c.model; // Properties are now reactive
|
|
1933
|
+
return c;
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1936
|
+
export const Consumer = toReact(useConsumer);
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
### Example 4: Service Integration (API Calls)
|
|
1940
|
+
|
|
1941
|
+
```typescript
|
|
1942
|
+
// React implementation
|
|
1943
|
+
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
1944
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1945
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1946
|
+
import { ClientBase } from '@actdim/dynstruct/net/client';
|
|
1947
|
+
import { MsgProviderAdapter } from '@actdim/dynstruct/componentModel/adapters';
|
|
1948
|
+
import { ServiceProvider } from '@actdim/dynstruct/services/ServiceProvider';
|
|
1949
|
+
|
|
1950
|
+
// Define API client
|
|
1951
|
+
class UserApiClient extends ClientBase {
|
|
1952
|
+
constructor() {
|
|
1953
|
+
super({ baseUrl: 'https://api.example.com' });
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
async getUsers() {
|
|
1957
|
+
return this.get<User[]>('/users');
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async createUser(data: CreateUserDto) {
|
|
1961
|
+
return this.post<User>('/users', data);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
async deleteUser(id: string) {
|
|
1965
|
+
return this.delete(`/users/${id}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Create adapter
|
|
1970
|
+
const userApiAdapter: MsgProviderAdapter<UserApiClient> = {
|
|
1971
|
+
service: new UserApiClient(),
|
|
1972
|
+
channelSelector: (service, method) => `API.USERS.${method.toUpperCase()}`
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// Register in app provider
|
|
1976
|
+
export const ApiServiceProvider = () =>
|
|
1977
|
+
ServiceProvider({ adapters: [userApiAdapter] });
|
|
1978
|
+
|
|
1979
|
+
// Use in component
|
|
1980
|
+
type AppStruct = ComponentStruct<AppMsgStruct, {
|
|
1981
|
+
props: { users: User[]; loading: boolean };
|
|
1982
|
+
}>;
|
|
1983
|
+
|
|
1984
|
+
const useApp = (params: ComponentParams<AppStruct>) => {
|
|
1985
|
+
let c: Component<AppStruct>;
|
|
1986
|
+
let m: ComponentModel<AppStruct>;
|
|
1987
|
+
|
|
1988
|
+
const def: ComponentDef<AppStruct> = {
|
|
1989
|
+
props: {
|
|
1990
|
+
users: [],
|
|
1991
|
+
loading: false
|
|
1992
|
+
},
|
|
1993
|
+
effects: {
|
|
1994
|
+
'loadUsers': async (component) => {
|
|
1995
|
+
m.loading = true; // Reactive update
|
|
1996
|
+
const response = await component.msgBus.request({
|
|
1997
|
+
channel: 'API.USERS.GETUSERS',
|
|
1998
|
+
payload: {}
|
|
1999
|
+
});
|
|
2000
|
+
m.users = response.payload; // Reactive update
|
|
2001
|
+
m.loading = false;
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
view: (_, c) => (
|
|
2005
|
+
<div>
|
|
2006
|
+
<h2>Users</h2>
|
|
2007
|
+
{m.loading ? (
|
|
2008
|
+
<p>Loading...</p>
|
|
2009
|
+
) : (
|
|
2010
|
+
<ul>
|
|
2011
|
+
{m.users.map(user => (
|
|
2012
|
+
<li key={user.id}>{user.name}</li>
|
|
2013
|
+
))}
|
|
2014
|
+
</ul>
|
|
2015
|
+
)}
|
|
2016
|
+
</div>
|
|
2017
|
+
)
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
c = useComponent(def, params);
|
|
2021
|
+
m = c.model; // Properties are reactive after useComponent
|
|
2022
|
+
return c;
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
export const App = toReact(useApp);
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
### Example 5: Navigation
|
|
2029
|
+
|
|
2030
|
+
```typescript
|
|
2031
|
+
// React implementation
|
|
2032
|
+
import { ComponentStruct, ComponentDef, ComponentParams, Component } from '@actdim/dynstruct/componentModel/contracts';
|
|
2033
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
2034
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
2035
|
+
|
|
2036
|
+
type PageStruct = ComponentStruct<AppMsgStruct, {
|
|
2037
|
+
actions: { navigateToHome: () => void; navigateToProfile: (userId: string) => void };
|
|
2038
|
+
}>;
|
|
2039
|
+
|
|
2040
|
+
const usePage = (params: ComponentParams<PageStruct>) => {
|
|
2041
|
+
let c: Component<PageStruct>;
|
|
2042
|
+
|
|
2043
|
+
const def: ComponentDef<PageStruct> = {
|
|
2044
|
+
actions: {
|
|
2045
|
+
navigateToHome: () => {
|
|
2046
|
+
c.msgBus.send({
|
|
2047
|
+
channel: '$NAV_GOTO',
|
|
2048
|
+
payload: { path: '/' }
|
|
2049
|
+
});
|
|
2050
|
+
},
|
|
2051
|
+
navigateToProfile: (userId: string) => {
|
|
2052
|
+
c.msgBus.send({
|
|
2053
|
+
channel: '$NAV_GOTO',
|
|
2054
|
+
payload: { path: `/profile/${userId}` }
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
view: (_, c) => (
|
|
2059
|
+
<div>
|
|
2060
|
+
<button onClick={c.actions.navigateToHome}>Home</button>
|
|
2061
|
+
<button onClick={() => c.actions.navigateToProfile('123')}>
|
|
2062
|
+
View Profile
|
|
2063
|
+
</button>
|
|
2064
|
+
</div>
|
|
2065
|
+
)
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
c = useComponent(def, params);
|
|
2069
|
+
return c;
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
export const Page = toReact(usePage);
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
### Example 6: Authentication & Security
|
|
2076
|
+
|
|
2077
|
+
```typescript
|
|
2078
|
+
// React implementation
|
|
2079
|
+
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
2080
|
+
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
2081
|
+
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
2082
|
+
import { SecurityProvider } from '@actdim/dynstruct/appDomain/security/securityProvider';
|
|
2083
|
+
import { ComponentContextProvider } from '@actdim/dynstruct/componentModel/componentContext';
|
|
2084
|
+
|
|
2085
|
+
// In your app root
|
|
2086
|
+
<ComponentContextProvider>
|
|
2087
|
+
<SecurityProvider>
|
|
2088
|
+
<ApiServiceProvider>
|
|
2089
|
+
<App />
|
|
2090
|
+
</ApiServiceProvider>
|
|
2091
|
+
</SecurityProvider>
|
|
2092
|
+
</ComponentContextProvider>
|
|
2093
|
+
|
|
2094
|
+
// Use in component
|
|
2095
|
+
type SecurePageStruct = ComponentStruct<AppMsgStruct, {
|
|
2096
|
+
props: { isAuthenticated: boolean };
|
|
2097
|
+
actions: { signIn: (credentials: Credentials) => void; signOut: () => void };
|
|
2098
|
+
}>;
|
|
2099
|
+
|
|
2100
|
+
const useSecurePage = (params: ComponentParams<SecurePageStruct>) => {
|
|
2101
|
+
let c: Component<SecurePageStruct>;
|
|
2102
|
+
let m: ComponentModel<SecurePageStruct>;
|
|
2103
|
+
|
|
2104
|
+
const def: ComponentDef<SecurePageStruct> = {
|
|
2105
|
+
props: { isAuthenticated: false },
|
|
2106
|
+
actions: {
|
|
2107
|
+
signIn: async (credentials) => {
|
|
2108
|
+
await c.msgBus.request({
|
|
2109
|
+
channel: '$AUTH_SIGNIN',
|
|
2110
|
+
payload: credentials
|
|
2111
|
+
});
|
|
2112
|
+
m.isAuthenticated = true; // Reactive update
|
|
2113
|
+
},
|
|
2114
|
+
signOut: async () => {
|
|
2115
|
+
await c.msgBus.request({
|
|
2116
|
+
channel: '$AUTH_SIGNOUT',
|
|
2117
|
+
payload: {}
|
|
2118
|
+
});
|
|
2119
|
+
m.isAuthenticated = false; // Reactive update
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
view: (_, c) => (
|
|
2123
|
+
<div>
|
|
2124
|
+
{m.isAuthenticated ? (
|
|
2125
|
+
<button onClick={c.actions.signOut}>Sign Out</button>
|
|
2126
|
+
) : (
|
|
2127
|
+
<button onClick={() => c.actions.signIn({ username: 'user', password: 'pass' })}>
|
|
2128
|
+
Sign In
|
|
2129
|
+
</button>
|
|
2130
|
+
)}
|
|
2131
|
+
</div>
|
|
2132
|
+
)
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
c = useComponent(def, params);
|
|
2136
|
+
m = c.model; // Model properties are reactive
|
|
2137
|
+
return c;
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
export const SecurePage = toReact(useSecurePage);
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
## Architecture
|
|
2144
|
+
|
|
2145
|
+
### Message Channels
|
|
2146
|
+
|
|
2147
|
+
The framework provides standard message channels for common operations:
|
|
2148
|
+
|
|
2149
|
+
#### Navigation
|
|
2150
|
+
- `$NAV_GOTO` - Navigate to a path
|
|
2151
|
+
- `$NAV_CONTEXT_GET` - Get current navigation context
|
|
2152
|
+
- `$NAV_CONTEXT_CHANGED` - Navigation context changed event
|
|
2153
|
+
|
|
2154
|
+
#### Notifications
|
|
2155
|
+
- `$NOTICE` - Display user notification
|
|
2156
|
+
|
|
2157
|
+
#### Errors
|
|
2158
|
+
- `$ERROR` - Global error handler
|
|
2159
|
+
|
|
2160
|
+
#### HTTP
|
|
2161
|
+
- `$FETCH` - HTTP request
|
|
2162
|
+
|
|
2163
|
+
#### Storage
|
|
2164
|
+
- `$STORE_GET` - Get item from storage
|
|
2165
|
+
- `$STORE_SET` - Set item in storage
|
|
2166
|
+
- `$STORE_REMOVE` - Remove item from storage
|
|
2167
|
+
|
|
2168
|
+
#### Configuration
|
|
2169
|
+
- `$CONFIG_GET` - Get configuration value
|
|
2170
|
+
|
|
2171
|
+
#### Authentication
|
|
2172
|
+
- `$AUTH_SIGNIN` - Sign in user
|
|
2173
|
+
- `$AUTH_SIGNOUT` - Sign out user
|
|
2174
|
+
- `$AUTH_REFRESH` - Refresh authentication token
|
|
2175
|
+
- `$AUTH_ENSURE` - Ensure user is authenticated
|
|
2176
|
+
|
|
2177
|
+
#### Access Control
|
|
2178
|
+
- `$ACL_GET` - Get access control list
|
|
2179
|
+
|
|
2180
|
+
### Component Lifecycle
|
|
2181
|
+
|
|
2182
|
+
Components go through the following lifecycle stages:
|
|
2183
|
+
|
|
2184
|
+
1. **Construction** - Component instance is created
|
|
2185
|
+
2. **Init** - Props and children are initialized
|
|
2186
|
+
3. **Layout** - Component structure is established
|
|
2187
|
+
4. **Ready** - Effects are executed, component is ready for interaction
|
|
2188
|
+
5. **Destroy** - Cleanup functions are called, resources are released
|
|
2189
|
+
|
|
2190
|
+
### Message Routing
|
|
2191
|
+
|
|
2192
|
+
Messages can be filtered by source using `ComponentMsgFilter`:
|
|
2193
|
+
|
|
2194
|
+
- `FromAncestors` - Only receive messages from parent components
|
|
2195
|
+
- `FromDescendants` - Only receive messages from child components
|
|
2196
|
+
- `FromSelf` - Only messages from this component
|
|
2197
|
+
- `FromBus` - Messages from the global bus
|
|
2198
|
+
|
|
2199
|
+
```typescript
|
|
2200
|
+
msgBroker: {
|
|
2201
|
+
subscribe: {
|
|
2202
|
+
'MY-EVENT': {
|
|
2203
|
+
filter: { FromAncestors: true },
|
|
2204
|
+
callback: (msg) => {
|
|
2205
|
+
// Only triggered by parent components
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
```
|
|
2211
|
+
|
|
2212
|
+
## API Reference
|
|
2213
|
+
|
|
2214
|
+
### Core Modules
|
|
2215
|
+
|
|
2216
|
+
**Framework-Agnostic:**
|
|
2217
|
+
- **componentModel/contracts** - Type definitions for components
|
|
2218
|
+
- **componentModel/core** - Core utilities (binding, proxy, effects)
|
|
2219
|
+
- **componentModel/componentContext** - Component registry and hierarchy
|
|
2220
|
+
- **appDomain/appContracts** - Application message structures
|
|
2221
|
+
- **appDomain/navigation** - Navigation utilities
|
|
2222
|
+
- **appDomain/security/securityProvider** - Security and authentication
|
|
2223
|
+
- **net/client** - HTTP client base class
|
|
2224
|
+
- **services/ServiceProvider** - Service provider factory
|
|
2225
|
+
|
|
2226
|
+
**Framework-Specific:**
|
|
2227
|
+
- **componentModel/react** - React integration hooks (current)
|
|
2228
|
+
- **componentModel/solid** - SolidJS integration hooks (planned)
|
|
2229
|
+
- **componentModel/vue** - Vue.js integration (planned)
|
|
2230
|
+
|
|
2231
|
+
### Key Functions
|
|
2232
|
+
|
|
2233
|
+
#### `useComponent(def, params)`
|
|
2234
|
+
Creates a component instance from a definition and parameters.
|
|
2235
|
+
|
|
2236
|
+
#### `toReact(useComponentFn)` (React)
|
|
2237
|
+
Converts a component hook into a React functional component.
|
|
2238
|
+
|
|
2239
|
+
**Framework Adapters:**
|
|
2240
|
+
- `toReact()` - React adapter (currently available)
|
|
2241
|
+
- `toSolid()` - SolidJS adapter (planned)
|
|
2242
|
+
- `toVue()` - Vue.js adapter (planned)
|
|
2243
|
+
|
|
2244
|
+
#### `bind(getter, setter, handlers?)`
|
|
2245
|
+
Creates a bidirectional binding for reactive properties.
|
|
2246
|
+
|
|
2247
|
+
#### `registerAdapters(msgBus, adapters)`
|
|
2248
|
+
Registers service adapters with the message bus.
|
|
2249
|
+
|
|
2250
|
+
#### `ServiceProvider({ adapters })`
|
|
2251
|
+
Creates a service provider component from adapters.
|
|
2252
|
+
|
|
2253
|
+
## Storybook Examples
|
|
2254
|
+
|
|
2255
|
+
This project includes comprehensive Storybook examples demonstrating all major features:
|
|
2256
|
+
|
|
2257
|
+
```bash
|
|
2258
|
+
npm run storybook
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
Available stories:
|
|
2262
|
+
- **SimpleComponent** - Basic reactive component with props and children
|
|
2263
|
+
- **ConnectionExample** - Message bus producer/consumer pattern
|
|
2264
|
+
- **ParentChildConnectionExample** - Parent-child component messaging
|
|
2265
|
+
- **ApiCallExample** - HTTP request integration with service adapters
|
|
2266
|
+
- **LocalMsgStructExample** - Local message structure with todo list
|
|
2267
|
+
- **StorageServiceExample** - Storage service provider usage
|
|
2268
|
+
|
|
2269
|
+
## Development
|
|
2270
|
+
|
|
2271
|
+
### Build
|
|
2272
|
+
|
|
2273
|
+
```bash
|
|
2274
|
+
npm run build
|
|
2275
|
+
```
|
|
2276
|
+
|
|
2277
|
+
### Run Tests
|
|
2278
|
+
|
|
2279
|
+
```bash
|
|
2280
|
+
npm test
|
|
2281
|
+
```
|
|
2282
|
+
|
|
2283
|
+
### Linting
|
|
2284
|
+
|
|
2285
|
+
```bash
|
|
2286
|
+
npm run lint
|
|
2287
|
+
```
|
|
2288
|
+
|
|
2289
|
+
### Format Code
|
|
2290
|
+
|
|
2291
|
+
```bash
|
|
2292
|
+
npm run format
|
|
2293
|
+
```
|
|
2294
|
+
|
|
2295
|
+
### Type Checking
|
|
2296
|
+
|
|
2297
|
+
```bash
|
|
2298
|
+
npm run typecheck
|
|
2299
|
+
```
|
|
2300
|
+
|
|
2301
|
+
## Package Management
|
|
2302
|
+
|
|
2303
|
+
Use dedupe for the following packages to avoid version conflicts:
|
|
2304
|
+
|
|
2305
|
+
- http-status
|
|
2306
|
+
- jwt-decode
|
|
2307
|
+
- mobx
|
|
2308
|
+
- mobx-react-lite
|
|
2309
|
+
- mobx-utils
|
|
2310
|
+
- path-to-regexp
|
|
2311
|
+
- react
|
|
2312
|
+
- react-dom
|
|
2313
|
+
- react-router
|
|
2314
|
+
- react-router-dom
|
|
2315
|
+
- @actdim/utico
|
|
2316
|
+
- @actdim/msgmesh
|
|
2317
|
+
- rxjs
|
|
2318
|
+
- uuid
|
|
2319
|
+
|
|
2320
|
+
## Contributing
|
|
2321
|
+
|
|
2322
|
+
This is a proprietary package. Please contact the author for contribution guidelines.
|
|
2323
|
+
|
|
2324
|
+
## License
|
|
2325
|
+
|
|
2326
|
+
Proprietary - See LICENSE file for details.
|
|
2327
|
+
|
|
2328
|
+
## Author
|
|
2329
|
+
|
|
2330
|
+
Pavel Borodaev
|
|
2331
|
+
|
|
2332
|
+
## Repository
|
|
2333
|
+
|
|
2334
|
+
https://github.com/actdim/dynstruct
|
|
2335
|
+
|
|
2336
|
+
## Issues
|
|
2337
|
+
|
|
2338
|
+
https://github.com/actdim/dynstruct/issues
|
|
2339
|
+
|
|
2340
|
+
## Keywords
|
|
2341
|
+
|
|
2342
|
+
typescript, components, react, component-model, architecture, modularity, structure, communication, message-bus, mobx, reactive, type-safe
|