@actdim/dynstruct 1.2.7 → 1.2.9
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 +278 -302
- package/dist/componentModel/DynamicContent.d.ts +3 -4
- package/dist/componentModel/DynamicContent.d.ts.map +1 -1
- package/dist/componentModel/contracts.d.ts +7 -3
- package/dist/componentModel/contracts.d.ts.map +1 -1
- package/dist/componentModel/contracts.es.js +17 -7
- package/dist/componentModel/contracts.es.js.map +1 -1
- package/dist/componentModel/core.d.ts +0 -1
- package/dist/componentModel/core.d.ts.map +1 -1
- package/dist/componentModel/core.es.js +55 -59
- package/dist/componentModel/core.es.js.map +1 -1
- package/dist/componentModel/react.d.ts.map +1 -1
- package/dist/componentModel/react.es.js +27 -26
- package/dist/componentModel/react.es.js.map +1 -1
- package/dist/services/ServiceProvider.d.ts +1 -1
- package/dist/services/ServiceProvider.d.ts.map +1 -1
- package/dist/services/ServiceProvider.es.js +1 -1
- package/dist/services/react/ServiceProvider.d.ts +1 -1
- package/dist/services/react/ServiceProvider.d.ts.map +1 -1
- package/dist/services/react/ServiceProvider.es.js +5 -5
- package/package.json +2 -2
- package/dist/componentModel/adapters.d.ts +0 -20
- package/dist/componentModel/adapters.d.ts.map +0 -1
- package/dist/componentModel/adapters.es.js +0 -33
- package/dist/componentModel/adapters.es.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,13 +2,37 @@
|
|
|
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
|
-
[](https://www.npmjs.com/package/@actdim/dynstruct)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@actdim/dynstruct)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Framework Support](#framework-support)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [How It Works](#how-it-works)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Getting Started (React)](#getting-started-react)
|
|
18
|
+
- [Key Advantages (React Examples)](#key-advantages-react-examples)
|
|
19
|
+
- [Core Concepts](#core-concepts)
|
|
20
|
+
- [More Examples (React)](#more-examples-react)
|
|
21
|
+
- [Architecture](#architecture)
|
|
22
|
+
- [API Reference](#api-reference)
|
|
23
|
+
- [Storybook Examples](#storybook-examples)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Package Management](#package-management)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
- [Author](#author)
|
|
29
|
+
- [Repository](#repository)
|
|
30
|
+
- [Issues](#issues)
|
|
31
|
+
- [Keywords](#keywords)
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
**@actdim/dynstruct** is a 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
36
|
|
|
13
37
|
- **Type-safe component model** with explicit dependency wiring
|
|
14
38
|
- **Decoupled messaging architecture** using a message bus for inter-component communication
|
|
@@ -49,7 +73,7 @@ The architectural core is framework-agnostic, allowing the same component struct
|
|
|
49
73
|
|
|
50
74
|
🎯 **Navigation & Routing** - Built-in navigation contracts with React Router integration
|
|
51
75
|
|
|
52
|
-
🔐 **Security Provider** - Authentication
|
|
76
|
+
🔐 **Security Provider** - Authentication and authorization support
|
|
53
77
|
|
|
54
78
|
## Quick Start
|
|
55
79
|
|
|
@@ -73,13 +97,14 @@ The core pattern in dynstruct is **structure-first composition** where parent co
|
|
|
73
97
|
npm install @actdim/dynstruct
|
|
74
98
|
```
|
|
75
99
|
|
|
76
|
-
### Peer Dependencies
|
|
77
|
-
|
|
78
|
-
This package requires the following peer dependencies:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
### Peer Dependencies
|
|
101
|
+
|
|
102
|
+
This package requires the following peer dependencies:
|
|
103
|
+
For message bus functionality, install [@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install react react-dom mobx mobx-react-lite mobx-utils \
|
|
107
|
+
@actdim/msgmesh @actdim/utico react-router react-router-dom \
|
|
83
108
|
rxjs uuid path-to-regexp jwt-decode http-status
|
|
84
109
|
```
|
|
85
110
|
|
|
@@ -92,7 +117,7 @@ pnpm add @actdim/dynstruct @actdim/msgmesh @actdim/utico \
|
|
|
92
117
|
jwt-decode http-status
|
|
93
118
|
```
|
|
94
119
|
|
|
95
|
-
##
|
|
120
|
+
## Getting Started (React)
|
|
96
121
|
|
|
97
122
|
> **Note:** All examples below are for the **React** implementation. SolidJS and Vue.js versions will have similar structure with framework-specific adapters.
|
|
98
123
|
|
|
@@ -389,7 +414,7 @@ function TodoList({ todos }) {
|
|
|
389
414
|
|
|
390
415
|
### MobX Reactivity Pitfalls
|
|
391
416
|
|
|
392
|
-
While MobX is
|
|
417
|
+
While MobX is capable, it has subtle issues that cause unexpected re-renders and are hard to debug:
|
|
393
418
|
|
|
394
419
|
#### Problem 1: Computed Returns New Object
|
|
395
420
|
|
|
@@ -731,7 +756,7 @@ Traditional React development offers **too many choices** for managing state and
|
|
|
731
756
|
- **Personal taste** - "I prefer this pattern because it looks cleaner to me"
|
|
732
757
|
- **Laziness** - "This is faster to write, even if it's not optimal"
|
|
733
758
|
|
|
734
|
-
When your component architecture is built on **many different principles** and becomes **
|
|
759
|
+
When your component architecture is built on **many different principles** and becomes **complex**, understanding where a problem is hiding becomes extremely difficult. Different components use different approaches, making the codebase inconsistent and hard to reason about.
|
|
735
760
|
|
|
736
761
|
**When Problems Surface:**
|
|
737
762
|
- ❌ **Hard to detect** - Inconsistent patterns mask the root cause
|
|
@@ -824,7 +849,7 @@ type Struct = ComponentStruct<
|
|
|
824
849
|
|
|
825
850
|
// List of effect names that will be available in this component.
|
|
826
851
|
// Effect implementations are defined in ComponentDef (see below).
|
|
827
|
-
effects: ['
|
|
852
|
+
effects: ['computeSummary', 'trackCounter'];
|
|
828
853
|
}
|
|
829
854
|
>;
|
|
830
855
|
```
|
|
@@ -870,23 +895,24 @@ const useMyComponent = (params: ComponentParams<Struct>) => {
|
|
|
870
895
|
},
|
|
871
896
|
|
|
872
897
|
effects: {
|
|
873
|
-
// Effect implementations. Effects are
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
// changes.
|
|
898
|
+
// Effect implementations. Effects are auto-tracking reactive
|
|
899
|
+
// functions that re-run automatically whenever any reactive
|
|
900
|
+
// property accessed inside them changes.
|
|
877
901
|
//
|
|
878
902
|
// Effects are accessed on the component instance by name via
|
|
879
|
-
// the `effects` property (e.g. c.effects.
|
|
903
|
+
// the `effects` property (e.g. c.effects.computeSummary).
|
|
880
904
|
//
|
|
881
905
|
// An effect runs immediately when the component is created and
|
|
882
906
|
// can later be manually paused, resumed, or stopped entirely.
|
|
883
|
-
|
|
884
|
-
|
|
907
|
+
computeSummary: (component) => {
|
|
908
|
+
// Re-runs whenever m.items changes
|
|
909
|
+
m.message = `Total items: ${m.items.length}`;
|
|
885
910
|
// Return an optional cleanup function
|
|
886
911
|
return () => { /* cleanup */ };
|
|
887
912
|
},
|
|
888
|
-
|
|
889
|
-
|
|
913
|
+
trackCounter: (component) => {
|
|
914
|
+
// Re-runs whenever m.counter changes
|
|
915
|
+
if (m.counter > 100) m.message = 'Counter is high!';
|
|
890
916
|
},
|
|
891
917
|
},
|
|
892
918
|
|
|
@@ -1033,7 +1059,7 @@ children: {
|
|
|
1033
1059
|
|
|
1034
1060
|
### Message Bus Communication
|
|
1035
1061
|
|
|
1036
|
-
dynstruct integrates with **[@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh)**, a
|
|
1062
|
+
dynstruct integrates with **[@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh)**, a type-safe message bus library that enables decoupled component communication.
|
|
1037
1063
|
|
|
1038
1064
|
#### Key Benefits
|
|
1039
1065
|
|
|
@@ -1435,6 +1461,130 @@ const ancestors = component.getChainUp();
|
|
|
1435
1461
|
const descendants = component.getChainDown();
|
|
1436
1462
|
```
|
|
1437
1463
|
|
|
1464
|
+
### Dynamic Content
|
|
1465
|
+
|
|
1466
|
+
Not all children need to be full dynstruct components. The `children` field supports three patterns for embedding dynamic content, ranging from lightweight React wrappers to parameterized component factories.
|
|
1467
|
+
|
|
1468
|
+
#### 1. React.FC Wrapper
|
|
1469
|
+
|
|
1470
|
+
The simplest approach: declare a child as `React.FC` in the structure and provide a plain function returning JSX in the definition. This is useful for small inline fragments that need access to the parent's reactive model but don't require their own component structure.
|
|
1471
|
+
|
|
1472
|
+
In the structure, the child type is `React.FC`. In the view, it is accessed with a **capitalized** name (because it's a function type): `<c.children.Summary />`.
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
type Struct = ComponentStruct<AppMsgStruct, {
|
|
1476
|
+
props: {
|
|
1477
|
+
counter: number;
|
|
1478
|
+
};
|
|
1479
|
+
children: {
|
|
1480
|
+
summary: React.FC; // standard React functional component
|
|
1481
|
+
};
|
|
1482
|
+
}>;
|
|
1483
|
+
|
|
1484
|
+
const def: ComponentDef<Struct> = {
|
|
1485
|
+
props: { counter: 0 },
|
|
1486
|
+
children: {
|
|
1487
|
+
// Plain function returning JSX — has access to the parent model
|
|
1488
|
+
summary: () => {
|
|
1489
|
+
return <div>Counter: {m.counter}</div>;
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
view: (_, c) => (
|
|
1493
|
+
<div>
|
|
1494
|
+
{/* Capitalized because it's a function type */}
|
|
1495
|
+
<c.children.Summary />
|
|
1496
|
+
</div>
|
|
1497
|
+
),
|
|
1498
|
+
};
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
#### 2. DynamicContent Component
|
|
1502
|
+
|
|
1503
|
+
When you need typed data and a render function inside a proper dynstruct component, use `DynamicContentStruct` / `useDynamicContent`. This gives you a component with a reactive `data` prop and a `render` callback, so the content re-renders when the data changes.
|
|
1504
|
+
|
|
1505
|
+
```typescript
|
|
1506
|
+
import { DynamicContentStruct, useDynamicContent } from '@actdim/dynstruct/componentModel/DynamicContent';
|
|
1507
|
+
|
|
1508
|
+
type Struct = ComponentStruct<AppMsgStruct, {
|
|
1509
|
+
props: {
|
|
1510
|
+
text: string;
|
|
1511
|
+
};
|
|
1512
|
+
children: {
|
|
1513
|
+
content: DynamicContentStruct<string, AppMsgStruct>;
|
|
1514
|
+
};
|
|
1515
|
+
}>;
|
|
1516
|
+
|
|
1517
|
+
const def: ComponentDef<Struct> = {
|
|
1518
|
+
props: { text: 'hello' },
|
|
1519
|
+
children: {
|
|
1520
|
+
content: useDynamicContent<string>({
|
|
1521
|
+
// Bind data to a parent property
|
|
1522
|
+
data: bindProp(() => m, 'text'),
|
|
1523
|
+
// Render function — can access the component's own model
|
|
1524
|
+
render: () => {
|
|
1525
|
+
return <>{c.children.content.model.data}</>;
|
|
1526
|
+
},
|
|
1527
|
+
}),
|
|
1528
|
+
},
|
|
1529
|
+
view: (_, c) => (
|
|
1530
|
+
<div>
|
|
1531
|
+
<c.children.content.View />
|
|
1532
|
+
</div>
|
|
1533
|
+
),
|
|
1534
|
+
};
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1537
|
+
`DynamicContentStruct` is generic: `DynamicContentStruct<TData, TMsgStruct>`. The `data` prop holds typed data (bound to a parent property or passed directly), and `render` produces the JSX.
|
|
1538
|
+
|
|
1539
|
+
#### 3. Factory Function (Parameterized Children)
|
|
1540
|
+
|
|
1541
|
+
When you need to create multiple instances of a child component dynamically (e.g. in a loop), declare the child as a factory function in the structure. The function accepts parameters and returns a component structure type.
|
|
1542
|
+
|
|
1543
|
+
In the view, factory children are also accessed with a **capitalized** name and can receive props (including a `key`):
|
|
1544
|
+
|
|
1545
|
+
```typescript
|
|
1546
|
+
type Struct = ComponentStruct<AppMsgStruct, {
|
|
1547
|
+
props: {
|
|
1548
|
+
counter: number;
|
|
1549
|
+
text: string;
|
|
1550
|
+
};
|
|
1551
|
+
children: {
|
|
1552
|
+
dynEdit: (props: { value?: string }) => SimpleEditStruct;
|
|
1553
|
+
};
|
|
1554
|
+
}>;
|
|
1555
|
+
|
|
1556
|
+
const def: ComponentDef<Struct> = {
|
|
1557
|
+
props: { counter: 0, text: 'bar' },
|
|
1558
|
+
children: {
|
|
1559
|
+
// Factory: called each time <c.children.DynEdit /> is rendered
|
|
1560
|
+
dynEdit: (params) => {
|
|
1561
|
+
return useSimpleEdit({
|
|
1562
|
+
value: bindProp(() => m, 'text'),
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
view: (_, c) => (
|
|
1567
|
+
<ul>
|
|
1568
|
+
{Array.from({ length: m.counter }).map((_, i) => (
|
|
1569
|
+
<li key={i}>
|
|
1570
|
+
<c.children.DynEdit key={i} />
|
|
1571
|
+
</li>
|
|
1572
|
+
))}
|
|
1573
|
+
</ul>
|
|
1574
|
+
),
|
|
1575
|
+
};
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
#### Summary
|
|
1579
|
+
|
|
1580
|
+
| Pattern | Structure type | Access in view | Use case |
|
|
1581
|
+
|---|---|---|---|
|
|
1582
|
+
| React.FC wrapper | `React.FC` | `<c.children.Name />` | Small inline fragments with access to parent model |
|
|
1583
|
+
| DynamicContent | `DynamicContentStruct<TData>` | `<c.children.name.View />` | Typed reactive data with custom render function |
|
|
1584
|
+
| Factory function | `(params) => ChildStruct` | `<c.children.Name key={...} />` | Multiple dynamic instances, parameterized creation |
|
|
1585
|
+
|
|
1586
|
+
> **Naming convention:** Children declared as function types (`React.FC`, factory functions) are accessed with a **capitalized** name in the view (`c.children.Summary`, `c.children.DynEdit`). Children declared as component structures use their original name (`c.children.content`).
|
|
1587
|
+
|
|
1438
1588
|
### Component Events
|
|
1439
1589
|
|
|
1440
1590
|
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.
|
|
@@ -1595,18 +1745,18 @@ const useForm = (params: ComponentParams<FormStruct>) => {
|
|
|
1595
1745
|
isValid: false
|
|
1596
1746
|
},
|
|
1597
1747
|
events: {
|
|
1598
|
-
// Validate email
|
|
1599
|
-
onChangeEmail: (
|
|
1748
|
+
// Validate email after it changes — onChange receives only the new value
|
|
1749
|
+
onChangeEmail: (value) => {
|
|
1600
1750
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1601
|
-
m.isValid = emailRegex.test(
|
|
1751
|
+
m.isValid = emailRegex.test(value) && m.password.length >= 6;
|
|
1602
1752
|
},
|
|
1603
1753
|
|
|
1604
|
-
// Validate password
|
|
1605
|
-
onChangePassword: (
|
|
1606
|
-
m.isValid = m.email.includes('@') &&
|
|
1754
|
+
// Validate password after it changes
|
|
1755
|
+
onChangePassword: (value) => {
|
|
1756
|
+
m.isValid = m.email.includes('@') && value.length >= 6;
|
|
1607
1757
|
},
|
|
1608
1758
|
|
|
1609
|
-
// Sanitize input before setting
|
|
1759
|
+
// Sanitize input before setting — onChanging receives (oldValue, newValue)
|
|
1610
1760
|
onChangingEmail: (oldValue, newValue) => {
|
|
1611
1761
|
return newValue.toLowerCase().trim();
|
|
1612
1762
|
}
|
|
@@ -1730,302 +1880,128 @@ const useEffectDemo = (params: ComponentParams<Struct>) => {
|
|
|
1730
1880
|
|
|
1731
1881
|
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
1882
|
|
|
1733
|
-
## Examples (React)
|
|
1883
|
+
## More Examples (React)
|
|
1734
1884
|
|
|
1735
|
-
> **Note:**
|
|
1885
|
+
> **Note:** For basic examples (simple component, parent-child, bindings, events, effects) see the [Getting Started](#getting-started-react) and [Core Concepts](#core-concepts) sections above.
|
|
1736
1886
|
|
|
1737
|
-
###
|
|
1887
|
+
### Service Integration (API Calls)
|
|
1738
1888
|
|
|
1739
|
-
|
|
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';
|
|
1889
|
+
dynstruct integrates with the **service adapter** system from [@actdim/msgmesh](https://github.com/actdim/msgmesh/?tab=readme-ov-file#service-adapters). Adapters automatically register any service class (e.g. an API client) as a message bus provider — channel names, payload types, and return types are all derived from the service class at compile time.
|
|
1744
1890
|
|
|
1745
|
-
|
|
1746
|
-
props: { count: number };
|
|
1747
|
-
actions: { increment: () => void; decrement: () => void };
|
|
1748
|
-
}>;
|
|
1891
|
+
#### 1. Define an API client
|
|
1749
1892
|
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
};
|
|
1893
|
+
```typescript
|
|
1894
|
+
export type DataItem = { id: number; name: string };
|
|
1765
1895
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1896
|
+
export class TestApiClient {
|
|
1897
|
+
static readonly name = 'TestApiClient' as const;
|
|
1898
|
+
readonly name = 'TestApiClient' as const;
|
|
1769
1899
|
|
|
1770
|
-
|
|
1900
|
+
getDataItems(param1: number[], param2: string[]): Promise<DataItem[]> {
|
|
1901
|
+
return fetch('/api/data').then(r => r.json());
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1771
1904
|
```
|
|
1772
1905
|
|
|
1773
|
-
|
|
1906
|
+
#### 2. Set up adapters and service provider
|
|
1907
|
+
|
|
1908
|
+
Type utilities from [`@actdim/msgmesh/adapters`](https://www.npmjs.com/package/@actdim/msgmesh) transform the service class into a typed bus structure. Each public method becomes a channel (e.g. `getDataItems` → `API.TEST.GETDATAITEMS`). See [@actdim/msgmesh — Service Adapters](https://github.com/actdim/msgmesh/?tab=readme-ov-file#service-adapters) for details on how the type transformation works.
|
|
1774
1909
|
|
|
1775
1910
|
```typescript
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1911
|
+
import {
|
|
1912
|
+
BaseServiceSuffix, getMsgChannelSelector, MsgProviderAdapter,
|
|
1913
|
+
ToMsgChannelPrefix, ToMsgStruct,
|
|
1914
|
+
} from '@actdim/msgmesh/adapters';
|
|
1915
|
+
import { ServiceProvider } from '@actdim/dynstruct/services/react/ServiceProvider';
|
|
1916
|
+
|
|
1917
|
+
// "TestApiClient" → remove suffix "Client" → uppercase → "API.TEST."
|
|
1918
|
+
type ApiPrefix = 'API';
|
|
1919
|
+
type TestApiChannelPrefix = ToMsgChannelPrefix<
|
|
1920
|
+
typeof TestApiClient.name, ApiPrefix, BaseServiceSuffix
|
|
1921
|
+
>;
|
|
1780
1922
|
|
|
1781
|
-
//
|
|
1782
|
-
type
|
|
1783
|
-
props: { label: string; onClick: () => void };
|
|
1784
|
-
}>;
|
|
1923
|
+
// Transform service methods into a bus struct
|
|
1924
|
+
type ApiMsgStruct = ToMsgStruct<TestApiClient, TestApiChannelPrefix>;
|
|
1785
1925
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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);
|
|
1926
|
+
// Create adapter instances
|
|
1927
|
+
const services: Record<TestApiChannelPrefix, any> = {
|
|
1928
|
+
'API.TEST.': new TestApiClient(),
|
|
1797
1929
|
};
|
|
1798
1930
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
-
};
|
|
1931
|
+
const msgProviderAdapters = Object.entries(services).map(
|
|
1932
|
+
([_, service]) => ({
|
|
1933
|
+
service,
|
|
1934
|
+
channelSelector: getMsgChannelSelector(services),
|
|
1935
|
+
}) as MsgProviderAdapter,
|
|
1936
|
+
);
|
|
1846
1937
|
|
|
1847
|
-
|
|
1938
|
+
// React provider component — wraps children with registered adapters
|
|
1939
|
+
export const ApiServiceProvider = () => ServiceProvider({ adapters: msgProviderAdapters });
|
|
1848
1940
|
```
|
|
1849
1941
|
|
|
1850
|
-
|
|
1942
|
+
#### 3. Use in a component
|
|
1851
1943
|
|
|
1852
|
-
|
|
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
|
-
}>;
|
|
1944
|
+
Data loading is a plain async function — it has nothing to do with effects. Call it from an event handler (`onReady`) or directly from a button click.
|
|
1866
1945
|
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
-
)
|
|
1946
|
+
```typescript
|
|
1947
|
+
type Struct = ComponentStruct<ApiMsgStruct, {
|
|
1948
|
+
props: {
|
|
1949
|
+
dataItems: DataItem[];
|
|
1891
1950
|
};
|
|
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
1951
|
msgScope: {
|
|
1901
|
-
subscribe:
|
|
1902
|
-
|
|
1903
|
-
};
|
|
1952
|
+
subscribe: ComponentMsgChannels<'API.TEST.GETDATAITEMS'>;
|
|
1953
|
+
publish: ComponentMsgChannels<'API.TEST.GETDATAITEMS'>;
|
|
1904
1954
|
};
|
|
1905
1955
|
}>;
|
|
1906
1956
|
|
|
1907
|
-
const
|
|
1908
|
-
let c: Component<
|
|
1909
|
-
let m: ComponentModel<
|
|
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
|
-
}
|
|
1957
|
+
const useApiCallExample = (params: ComponentParams<Struct>) => {
|
|
1958
|
+
let c: Component<Struct>;
|
|
1959
|
+
let m: ComponentModel<Struct>;
|
|
1959
1960
|
|
|
1960
|
-
async
|
|
1961
|
-
|
|
1961
|
+
// Plain async function — not an effect, not an action
|
|
1962
|
+
async function loadData() {
|
|
1963
|
+
const msg = await c.msgBus.request({
|
|
1964
|
+
channel: 'API.TEST.GETDATAITEMS',
|
|
1965
|
+
payloadFn: (fn) => fn([1, 2], ['first', 'second']),
|
|
1966
|
+
});
|
|
1967
|
+
m.dataItems = msg.payload;
|
|
1962
1968
|
}
|
|
1963
1969
|
|
|
1964
|
-
async
|
|
1965
|
-
|
|
1970
|
+
async function clear() {
|
|
1971
|
+
m.dataItems.length = 0;
|
|
1966
1972
|
}
|
|
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
1973
|
|
|
1979
|
-
|
|
1980
|
-
|
|
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> = {
|
|
1974
|
+
const def: ComponentDef<Struct> = {
|
|
1975
|
+
regType: 'ApiCallExample',
|
|
1989
1976
|
props: {
|
|
1990
|
-
|
|
1991
|
-
loading: false
|
|
1977
|
+
dataItems: [],
|
|
1992
1978
|
},
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
-
}
|
|
1979
|
+
events: {
|
|
1980
|
+
// Load data when the component is ready
|
|
1981
|
+
onReady: () => { loadData(); },
|
|
2003
1982
|
},
|
|
2004
1983
|
view: (_, c) => (
|
|
2005
|
-
<div>
|
|
2006
|
-
<
|
|
2007
|
-
{
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
))}
|
|
2014
|
-
</ul>
|
|
2015
|
-
)}
|
|
1984
|
+
<div id={c.id}>
|
|
1985
|
+
<button onClick={loadData}>Load data</button>
|
|
1986
|
+
<button onClick={clear}>Clear</button>
|
|
1987
|
+
<ul>
|
|
1988
|
+
{m.dataItems.map((item) => (
|
|
1989
|
+
<li key={item.id}>{item.id}: {item.name}</li>
|
|
1990
|
+
))}
|
|
1991
|
+
</ul>
|
|
2016
1992
|
</div>
|
|
2017
|
-
)
|
|
1993
|
+
),
|
|
2018
1994
|
};
|
|
2019
1995
|
|
|
2020
1996
|
c = useComponent(def, params);
|
|
2021
|
-
m = c.model;
|
|
1997
|
+
m = c.model;
|
|
2022
1998
|
return c;
|
|
2023
1999
|
};
|
|
2024
2000
|
|
|
2025
|
-
export const
|
|
2001
|
+
export const ApiCallExample = toReact(useApiCallExample);
|
|
2026
2002
|
```
|
|
2027
2003
|
|
|
2028
|
-
###
|
|
2004
|
+
### Navigation
|
|
2029
2005
|
|
|
2030
2006
|
```typescript
|
|
2031
2007
|
// React implementation
|
|
@@ -2072,7 +2048,7 @@ const usePage = (params: ComponentParams<PageStruct>) => {
|
|
|
2072
2048
|
export const Page = toReact(usePage);
|
|
2073
2049
|
```
|
|
2074
2050
|
|
|
2075
|
-
###
|
|
2051
|
+
### Authentication & Security
|
|
2076
2052
|
|
|
2077
2053
|
```typescript
|
|
2078
2054
|
// React implementation
|
|
@@ -2174,9 +2150,6 @@ The framework provides standard message channels for common operations:
|
|
|
2174
2150
|
- `$AUTH_REFRESH` - Refresh authentication token
|
|
2175
2151
|
- `$AUTH_ENSURE` - Ensure user is authenticated
|
|
2176
2152
|
|
|
2177
|
-
#### Access Control
|
|
2178
|
-
- `$ACL_GET` - Get access control list
|
|
2179
|
-
|
|
2180
2153
|
### Component Lifecycle
|
|
2181
2154
|
|
|
2182
2155
|
Components go through the following lifecycle stages:
|
|
@@ -2200,9 +2173,11 @@ Messages can be filtered by source using `ComponentMsgFilter`:
|
|
|
2200
2173
|
msgBroker: {
|
|
2201
2174
|
subscribe: {
|
|
2202
2175
|
'MY-EVENT': {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2176
|
+
in: {
|
|
2177
|
+
callback: (msg, component) => {
|
|
2178
|
+
// Only triggered by parent/ancestor components
|
|
2179
|
+
},
|
|
2180
|
+
componentFilter: ComponentMsgFilter.FromAncestors
|
|
2206
2181
|
}
|
|
2207
2182
|
}
|
|
2208
2183
|
}
|
|
@@ -2259,12 +2234,13 @@ npm run storybook
|
|
|
2259
2234
|
```
|
|
2260
2235
|
|
|
2261
2236
|
Available stories:
|
|
2262
|
-
- **SimpleComponent** - Basic reactive component with props and children
|
|
2263
|
-
- **
|
|
2264
|
-
- **
|
|
2265
|
-
- **
|
|
2266
|
-
- **
|
|
2267
|
-
- **
|
|
2237
|
+
- **dynstruct / Basics / SimpleComponent** - Basic reactive component with props and children
|
|
2238
|
+
- **dynstruct / Basics / Api Call Example** - HTTP request integration with service adapters
|
|
2239
|
+
- **dynstruct / Basics / Effect Demo** - Auto-tracking reactive effects with pause/resume
|
|
2240
|
+
- **dynstruct / Basics / Local Msg Struct** - Local message structure with todo list
|
|
2241
|
+
- **dynstruct / Basics / Storage Service** - Storage service provider usage
|
|
2242
|
+
- **dynstruct / Connection / Basics** - Message bus producer/consumer pattern
|
|
2243
|
+
- **dynstruct / Connection / Parent/Child** - Parent-child component messaging
|
|
2268
2244
|
|
|
2269
2245
|
## Development
|
|
2270
2246
|
|