@smartbit4all/ng-client 7.0.7 → 7.0.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/WIDGETS.md CHANGED
@@ -1,206 +1,215 @@
1
- # Writing a widget for `@smartbit4all/ng-client` 7.0
2
-
3
- A **widget** is a component that renders one part of a screen component's backend model and
4
- reloads itself when the backend says that part changed: a grid, a tree, a form, a map, a
5
- diagram — or one of your own.
6
-
7
- Until 7.0 only the library could have widgets. `SmartComponentApiClient` collected its
8
- children through eight `@ViewChildren` over eight concrete widget classes and dispatched on
9
- `instanceof`, so a host-authored widget could not join model-change routing at all, and the
10
- base class statically imported every widget type (which is why `chart.js` landed in every
11
- host bundle). 7.0 inverts that dependency: **a widget finds its client through DI and
12
- registers itself, and the client knows no widget type**.
13
-
14
- This document is the protocol. It is deliberately kept next to the library, in git, because
15
- it is what a new widget author needs; the reasoning behind each decision lives in the
16
- project's ADRs (`docs/adr/0005`–`0008` in the `platform-angular2` working copy), which are
17
- not part of the published package.
18
-
19
- ## The whole contract
20
-
21
- ```ts
22
- import {
23
- injectSmartComponent,
24
- registerSmartWidget,
25
- } from '@smartbit4all/ng-client';
26
-
27
- @Component({
28
- selector: 'my-widget',
29
- template: `…`,
30
- })
31
- export class MyWidgetComponent {
32
- /** The identifier the backend knows this widget by. */
33
- readonly identifier = input.required<string>();
34
-
35
- /** The nearest screen component above this one, or undefined if there is none. */
36
- private readonly smartComponent = injectSmartComponent();
37
-
38
- private readonly model = signal<MyModel | undefined>(undefined);
39
-
40
- constructor() {
41
- registerSmartWidget(this.smartComponent, {
42
- identifier: () => this.identifier(),
43
- reload: () => this.load(),
44
- });
45
- }
46
-
47
- private async load(): Promise<void> {
48
- this.model.set(await lastValueFrom(this.api.get(this.identifier())));
49
- }
50
- }
51
- ```
52
-
53
- That is the entire registration. No module, no provider in the host, no entry in a registry,
54
- nothing to add to `SmartComponent`. Put `<my-widget identifier="…">` anywhere inside a screen
55
- component — including inside a layout the backend drives, inside a dialog, or inside one of
56
- your own sub-components — and it participates.
57
-
58
- ### Six rules that follow from it
59
-
60
- 1. **The nearest client above wins.** Resolution is plain element-injector resolution. An
61
- embedded view rendered into a layout slot shadows its container for its own widgets; a
62
- dialog page is its own root, because the library opens dialogs without a
63
- `viewContainerRef`.
64
-
65
- 2. **`identifier` is read lazily, every time.** It is a function in the registration, not a
66
- value, because the identifier usually arrives with an input *after* the widget was
67
- constructed. Never capture it.
68
-
69
- 3. **To opt out, use `[smartComponentDetached]`.** Put it on an element whose subtree
70
- deliberately renders something other than the client's model. Widgets below it resolve an
71
- empty slot and neither reload nor take part in submit or validation.
72
-
73
- ```html
74
- <div smartComponentDetached>
75
- <smart-grid [smartGrid]="myOwnGrid" [uuid]="myOwnUuid"></smart-grid>
76
- </div>
77
- ```
78
-
79
- 4. **A widget outside any screen component is legal.** `injectSmartComponent()` returns
80
- `undefined`, `registerSmartWidget` does nothing, and the widget works standalone from its
81
- inputs. Do not guard against it.
82
-
83
- 5. **Command handlers must be synchronous.** The client's `commands` stream is an RxJS
84
- `Subject`, and the two commands that need an answer — `collectInvalidFields` and
85
- `widgetChanged` — pass a mutable object that the caller reads back the moment `next()`
86
- returns. A handler that defers (a `debounce`, an `await` before writing) silently
87
- contributes nothing, and nothing at compile time will tell you.
88
-
89
- 6. **`reloadDuringInitialSync` is a per-widget decision.** Right after the client's own
90
- `load()` it replays the model's widget list with `skipLoad` set. A widget that fetches
91
- from its own `ngAfterViewInit` leaves the flag off — that is the only reason it exists.
92
- A widget that only fetches when told to sets it.
93
-
94
- ## State is a signal
95
-
96
- 7.0 runs without zone.js. Every component of the library is `OnPush`, and nothing is checked
97
- just because something, somewhere, ticked. So:
98
-
99
- - **What the widget writes and the template reads is a signal.** State written from a
100
- subscription, a promise, a timer or a third-party callback needs one. State written from a
101
- template event or an input does not.
102
- - **`markForCheck()` still works** — the scheduler ticks on it — and it remains the honest
103
- answer for state the widget does not own: a host-supplied object mutated in place,
104
- Angular's or Material's own state (`touched`, a `MatTree` data source), a third-party
105
- callback.
106
- - **Anything the backend mutates *in place* needs its own signal or an explicit bump.** This
107
- is the single hazard the library hit most often while converting. The model object arriving
108
- with a field replaced inside it is invisible to a `computed` over the model signal.
109
-
110
- A convention worth copying, if you are converting an existing widget: keep a private signal
111
- and expose a getter/setter pair of the same name, so no read site and no template moves.
112
-
113
- ```ts
114
- private readonly modelState = signal<MyModel | undefined>(undefined);
115
- @Input() set model(value: MyModel | undefined) { this.modelState.set(value); }
116
- get model(): MyModel | undefined { return this.modelState(); }
117
- ```
118
-
119
- ## Toolbars
120
-
121
- A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The list
122
- comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the screen
123
- component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is not an
124
- address.
125
-
126
- So a widget that carries a `toolbarId` in its model renders:
127
-
128
- ```html
129
- <smart-ui-action-toolbar [id]="toolbarId"></smart-ui-action-toolbar>
130
- ```
131
-
132
- and needs nothing else: the actions find it. Bind `[executor]` only when the actions are
133
- performed by something other than the screen component (a tree service, a dialog service of
134
- your own); `[widgetId]`, `[nodeId]` and `[actionParams]` belong on the toolbar too, not on
135
- each entry.
136
-
137
- If your widget *owns* a subtree whose toolbars must render a list it computed — the case the
138
- grid card hits, where the backend's row layout carries toolbar ids that no template in this
139
- library can bind — provide a `SmartActionHost` for that subtree and write the list into it:
140
-
141
- ```ts
142
- @Component({ …, providers: [SmartActionHost] })
143
- export class MyRowComponent {
144
- private readonly actionHost = inject(SmartActionHost);
145
- // …
146
- this.actionHost.actionModels.set(this.row.actions);
147
- }
148
- ```
149
-
150
- Leaving it `undefined` means "not my business", and the toolbars below fall back to the
151
- client — which is what makes providing it unconditionally safe.
152
-
153
- **A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
154
- `readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
155
- toolbar is already rendering never reached the screen under zone.js either — it only appeared
156
- to work when some other event happened to tick the application.
157
-
158
- ```ts
159
- this.actions = this.actions.map((a) =>
160
- a.uiAction.code === code ? { ...a, cssClass: 'active' } : a
161
- );
162
- ```
163
-
164
- Reassigning the **array** is what re-renders. `this.actions[0] = { …this.actions[0] }`
165
- compiles and does nothing.
166
-
167
- ## Three traps that have actually bitten
168
-
169
- 1. **Declare your inputs — a plain public field is not one.**
170
- `ComponentFactoryService` instantiates the components the library creates imperatively
171
- (grid cards, expandable content, table cell components, the form's `COMPONENT` widget). It
172
- writes every **declared** input with `ref.setInput()`, so a signal input, an aliased input
173
- and `ngOnChanges` all behave exactly as they would under a template binding. An
174
- **undeclared** field is still assigned — host components are free to have plain public
175
- fields and 7.0 does not break them — but it warns, and such a field can never become a
176
- signal input.
177
-
178
- Until 7.0 the service assigned the field in every case, which *destroyed* a signal input:
179
- the input is a function on the instance, so assigning over it replaced the function with
180
- the value and the next `this.x()` threw `x is not a function`. That cost two debugging
181
- rounds in the library (`parentLayoutComponent`, `gridRow`). If you are porting a widget
182
- that carries a comment about this, the constraint is gone.
183
-
184
- 2. **`@for (… ; track item)` over objects the backend rebuilds destroys the subtree every
185
- refresh** (`NG0956`), and the DOM churn is real — a form inside is rebuilt. The backend
186
- hands back new objects on each model refresh, so track by `$index`, or by a key you
187
- synthesize yourself. Do not reach for a backend `identifier` field that is only sometimes
188
- set: an all-`undefined` sibling list is `NG0955`, which is worse.
189
-
190
- 3. **Style application is clear-then-apply.** `SmartStyleUtility.applyStyle` remembers what
191
- it applied to an element and removes exactly that before applying the next set. If you
192
- decorate an element the library also styles, decorate your *own* child element instead of
193
- pushing classes into the object the backend sent — that object is shared, and mutating it
194
- is invisible to change detection anyway.
195
-
196
- ## The executable version
197
-
198
- Two specs in the library are written to be read as examples, and they travel with the source:
199
-
200
- - `src/lib/smart-client/smart-component-host.spec.ts` a widget the library knows nothing
201
- about joining model-change routing, the detach brake, and the nearest-client rule.
202
- - `src/lib/view-context/smart-ui-action/smart-action-host.spec.ts` a subtree supplying the
203
- actions its toolbars render, and the fallback when it does not.
204
-
205
- If a rule here and one of those specs disagree, the spec is right — say so and this document
206
- gets fixed.
1
+ # Writing a widget for `@smartbit4all/ng-client` 7.0
2
+
3
+ A **widget** is a component that renders one part of a screen component's backend model and
4
+ reloads itself when the backend says that part changed: a grid, a tree, a form, a map, a
5
+ diagram — or one of your own.
6
+
7
+ Until 7.0 only the library could have widgets. `SmartComponentApiClient` collected its
8
+ children through eight `@ViewChildren` over eight concrete widget classes and dispatched on
9
+ `instanceof`, so a host-authored widget could not join model-change routing at all, and the
10
+ base class statically imported every widget type (which is why `chart.js` landed in every
11
+ host bundle). 7.0 inverts that dependency: **a widget finds its client through DI and
12
+ registers itself, and the client knows no widget type**.
13
+
14
+ This document is the protocol. It is deliberately kept next to the library, in git, because
15
+ it is what a new widget author needs; the reasoning behind each decision lives in the
16
+ project's ADRs (`docs/adr/0005`–`0008` in the `platform-angular2` working copy), which are
17
+ not part of the published package.
18
+
19
+ ## The whole contract
20
+
21
+ ```ts
22
+ import {
23
+ injectSmartComponent,
24
+ registerSmartWidget,
25
+ } from '@smartbit4all/ng-client';
26
+
27
+ @Component({
28
+ selector: 'my-widget',
29
+ template: `…`,
30
+ })
31
+ export class MyWidgetComponent {
32
+ /** The identifier the backend knows this widget by. */
33
+ readonly identifier = input.required<string>();
34
+
35
+ /** The nearest screen component above this one, or undefined if there is none. */
36
+ private readonly smartComponent = injectSmartComponent();
37
+
38
+ private readonly model = signal<MyModel | undefined>(undefined);
39
+
40
+ constructor() {
41
+ registerSmartWidget(this.smartComponent, {
42
+ identifier: () => this.identifier(),
43
+ reload: () => this.load(),
44
+ });
45
+ }
46
+
47
+ private async load(): Promise<void> {
48
+ this.model.set(await lastValueFrom(this.api.get(this.identifier())));
49
+ }
50
+ }
51
+ ```
52
+
53
+ That is the entire registration. No module, no provider in the host, no entry in a registry,
54
+ nothing to add to `SmartComponent`. Put `<my-widget identifier="…">` anywhere inside a screen
55
+ component — including inside a layout the backend drives, inside a dialog, or inside one of
56
+ your own sub-components — and it participates.
57
+
58
+ ### Six rules that follow from it
59
+
60
+ 1. **The nearest client above wins.** Resolution is plain element-injector resolution. An
61
+ embedded view rendered into a layout slot shadows its container for its own widgets; a
62
+ dialog page is its own root, because the library opens dialogs without a
63
+ `viewContainerRef`.
64
+
65
+ 2. **`identifier` is read lazily, every time.** It is a function in the registration, not a
66
+ value, because the identifier usually arrives with an input *after* the widget was
67
+ constructed. Never capture it.
68
+
69
+ 3. **To opt out, use `[smartComponentDetached]`.** Put it on an element whose subtree
70
+ deliberately renders something other than the client's model. Widgets below it resolve an
71
+ empty slot and neither reload nor take part in submit or validation.
72
+
73
+ ```html
74
+ <div smartComponentDetached>
75
+ <smart-grid [smartGrid]="myOwnGrid" [uuid]="myOwnUuid"></smart-grid>
76
+ </div>
77
+ ```
78
+
79
+ 4. **A widget outside any screen component is legal.** `injectSmartComponent()` returns
80
+ `undefined`, `registerSmartWidget` does nothing, and the widget works standalone from its
81
+ inputs. Do not guard against it.
82
+
83
+ 5. **Command handlers must be synchronous.** The client's `commands` stream is an RxJS
84
+ `Subject`, and the two commands that need an answer — `collectInvalidFields` and
85
+ `widgetChanged` — pass a mutable object that the caller reads back the moment `next()`
86
+ returns. A handler that defers (a `debounce`, an `await` before writing) silently
87
+ contributes nothing, and nothing at compile time will tell you.
88
+
89
+ 6. **`reloadDuringInitialSync` is a per-widget decision.** Right after the client's own
90
+ `load()` it replays the model's widget list with `skipLoad` set. A widget that fetches
91
+ from its own `ngAfterViewInit` leaves the flag off — that is the only reason it exists.
92
+ A widget that only fetches when told to sets it.
93
+
94
+ ## State is a signal
95
+
96
+ 7.0 runs without zone.js. Every component of the library is `OnPush`, and nothing is checked
97
+ just because something, somewhere, ticked. So:
98
+
99
+ - **What the widget writes and the template reads is a signal.** State written from a
100
+ subscription, a promise, a timer or a third-party callback needs one. State written from a
101
+ template event or an input does not.
102
+ - **`markForCheck()` still works** — the scheduler ticks on it — and it remains the honest
103
+ answer for state the widget does not own: a host-supplied object mutated in place,
104
+ Angular's or Material's own state (`touched`, a `MatTree` data source), a third-party
105
+ callback.
106
+ - **Anything the backend mutates *in place* needs its own signal or an explicit bump.** This
107
+ is the single hazard the library hit most often while converting. The model object arriving
108
+ with a field replaced inside it is invisible to a `computed` over the model signal.
109
+
110
+ A convention worth copying, if you are converting an existing widget: keep a private signal
111
+ and expose a getter/setter pair of the same name, so no read site and no template moves.
112
+
113
+ ```ts
114
+ private readonly modelState = signal<MyModel | undefined>(undefined);
115
+ @Input() set model(value: MyModel | undefined) { this.modelState.set(value); }
116
+ get model(): MyModel | undefined { return this.modelState(); }
117
+ ```
118
+
119
+ ## Toolbars
120
+
121
+ A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The list
122
+ comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the screen
123
+ component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is not an
124
+ address.
125
+
126
+ So a widget that carries a `toolbarId` in its model renders:
127
+
128
+ ```html
129
+ <smart-ui-action-toolbar [id]="toolbarId"></smart-ui-action-toolbar>
130
+ ```
131
+
132
+ and needs nothing else: the actions find it. Bind `[executor]` only when the actions are
133
+ performed by something other than the screen component (a tree service, a dialog service of
134
+ your own); `[widgetId]`, `[nodeId]` and `[actionParams]` belong on the toolbar too, not on
135
+ each entry.
136
+
137
+ If your widget *owns* a subtree whose toolbars must render a list it computed — the case the
138
+ grid card hits, where the backend's row layout carries toolbar ids that no template in this
139
+ library can bind — provide a `SmartActionHost` for that subtree and write the list into it:
140
+
141
+ ```ts
142
+ @Component({ …, providers: [SmartActionHost] })
143
+ export class MyRowComponent {
144
+ private readonly actionHost = inject(SmartActionHost);
145
+ // …
146
+ this.actionHost.actionModels.set(this.row.actions);
147
+ }
148
+ ```
149
+
150
+ Leaving it `undefined` means "not my business", and the toolbars below fall back to the
151
+ client — which is what makes providing it unconditionally safe.
152
+
153
+ **A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
154
+ `readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
155
+ toolbar is already rendering never reached the screen under zone.js either — it only appeared
156
+ to work when some other event happened to tick the application.
157
+
158
+ ```ts
159
+ this.actions = this.actions.map((a) =>
160
+ a.uiAction.code === code ? { ...a, cssClass: 'active' } : a
161
+ );
162
+ ```
163
+
164
+ Reassigning the **array** is what re-renders. `this.actions[0] = { …this.actions[0] }`
165
+ compiles and does nothing.
166
+
167
+ ## Three traps that have actually bitten
168
+
169
+ 1. **Declare your inputs — a plain public field is not one.**
170
+ `ComponentFactoryService` instantiates the components the library creates imperatively
171
+ (grid cards, expandable content, table cell components, the form's `COMPONENT` widget). It
172
+ writes every **declared** input with `ref.setInput()`, so a signal input, an aliased input
173
+ and `ngOnChanges` all behave exactly as they would under a template binding. An
174
+ **undeclared** field is still assigned — host components are free to have plain public
175
+ fields and 7.0 does not break them — but it warns, and such a field can never become a
176
+ signal input.
177
+
178
+ Until 7.0 the service assigned the field in every case, which *destroyed* a signal input:
179
+ the input is a function on the instance, so assigning over it replaced the function with
180
+ the value and the next `this.x()` threw `x is not a function`. That cost two debugging
181
+ rounds in the library (`parentLayoutComponent`, `gridRow`). If you are porting a widget
182
+ that carries a comment about this, the constraint is gone.
183
+
184
+ 2. **`@for (… ; track item)` over objects the backend rebuilds destroys the subtree every
185
+ refresh** (`NG0956`), and the DOM churn is real — a form inside is rebuilt. The backend
186
+ hands back new objects on each model refresh, so track by `$index`, or by a key you
187
+ synthesize yourself. Do not reach for a backend `identifier` field that is only sometimes
188
+ set: an all-`undefined` sibling list is `NG0955`, which is worse.
189
+
190
+ 3. **Style application is clear-then-apply.** `SmartStyleUtility.applyStyle` remembers what
191
+ it applied to an element and removes exactly that before applying the next set. If you
192
+ decorate an element the library also styles, decorate your *own* child element instead of
193
+ pushing classes into the object the backend sent — that object is shared, and mutating it
194
+ is invisible to change detection anyway.
195
+
196
+ ## Library widgets a backend layout can place
197
+
198
+ A `SmartComponentLayoutDefinition` node of type `WIDGET` places one of the library's widgets by
199
+ `widget.identifier`: `grid`, `tree`, `filter` (`smart-filter-widget`, a filter builder whose
200
+ identifier is its `filterId`), `toolbar`, `map`, `diagram` and `embedded_slot`. Each of them
201
+ follows the contract above, so a widget placed by the layout and one written into a page
202
+ template by hand register with the same client the same way. The table in
203
+ `src/lib/smart-component-layout/README.md` lists what each identifier means.
204
+
205
+ ## The executable version
206
+
207
+ Two specs in the library are written to be read as examples, and they travel with the source:
208
+
209
+ - `src/lib/smart-client/smart-component-host.spec.ts` — a widget the library knows nothing
210
+ about joining model-change routing, the detach brake, and the nearest-client rule.
211
+ - `src/lib/view-context/smart-ui-action/smart-action-host.spec.ts` — a subtree supplying the
212
+ actions its toolbars render, and the fallback when it does not.
213
+
214
+ If a rule here and one of those specs disagree, the spec is right — say so and this document
215
+ gets fixed.