@smartbit4all/ng-client 7.2.5 → 7.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/WIDGETS.md CHANGED
@@ -1,226 +1,226 @@
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 toolbar does not have to be in a template at all. The HTML widget **hydrates** one into every
154
- toolbar slot of the html the backend sent (`<smart-ui-action-toolbar data-sb4-toolbar="…">`), and
155
- such a toolbar resolves by the very same pull: a `SmartActionHost` above it if one has a list, the
156
- screen component otherwise. That is why a row layout's html gets the row's actions on a grid card
157
- with no code of its own, and a table cell rendered from a column template gets them the same way.
158
- An action trigger (`data-sb4-action`) looks its action up in the same list, and an icon slot
159
- (`data-sb4-icon`, `data-sb4-icon-key`) hydrates into a `smart-icon` the same way. See
160
- `HTML-WIDGET.md`.
161
-
162
- **A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
163
- `readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
164
- toolbar is already rendering never reached the screen under zone.js either — it only appeared
165
- to work when some other event happened to tick the application.
166
-
167
- ```ts
168
- this.actions = this.actions.map((a) =>
169
- a.uiAction.code === code ? { ...a, cssClass: 'active' } : a
170
- );
171
- ```
172
-
173
- Reassigning the **array** is what re-renders. `this.actions[0] = { …this.actions[0] }`
174
- compiles and does nothing.
175
-
176
- ## Three traps that have actually bitten
177
-
178
- 1. **Declare your inputs — a plain public field is not one.**
179
- `ComponentFactoryService` instantiates the components the library creates imperatively
180
- (grid cards, expandable content, table cell components, the form's `COMPONENT` widget). It
181
- writes every **declared** input with `ref.setInput()`, so a signal input, an aliased input
182
- and `ngOnChanges` all behave exactly as they would under a template binding. An
183
- **undeclared** field is still assigned — host components are free to have plain public
184
- fields and 7.0 does not break them — but it warns, and such a field can never become a
185
- signal input.
186
-
187
- Until 7.0 the service assigned the field in every case, which *destroyed* a signal input:
188
- the input is a function on the instance, so assigning over it replaced the function with
189
- the value and the next `this.x()` threw `x is not a function`. That cost two debugging
190
- rounds in the library (`parentLayoutComponent`, `gridRow`). If you are porting a widget
191
- that carries a comment about this, the constraint is gone.
192
-
193
- 2. **`@for (… ; track item)` over objects the backend rebuilds destroys the subtree every
194
- refresh** (`NG0956`), and the DOM churn is real — a form inside is rebuilt. The backend
195
- hands back new objects on each model refresh, so track by `$index`, or by a key you
196
- synthesize yourself. Do not reach for a backend `identifier` field that is only sometimes
197
- set: an all-`undefined` sibling list is `NG0955`, which is worse.
198
-
199
- 3. **Style application is clear-then-apply.** `SmartStyleUtility.applyStyle` remembers what
200
- it applied to an element and removes exactly that before applying the next set. If you
201
- decorate an element the library also styles, decorate your *own* child element instead of
202
- pushing classes into the object the backend sent — that object is shared, and mutating it
203
- is invisible to change detection anyway.
204
-
205
- ## Library widgets a backend layout can place
206
-
207
- A `SmartComponentLayoutDefinition` node of type `WIDGET` places one of the library's widgets by
208
- `widget.identifier`: `grid`, `tree`, `filter` (`smart-filter-widget`, a filter builder whose
209
- identifier is its `filterId`), `toolbar`, `map`, `diagram` and `embedded_slot`. Each of them
210
- follows the contract above, so a widget placed by the layout and one written into a page
211
- template by hand register with the same client the same way. Inside a `FORM` node the form
212
- widget type `HTML` places server-sent html with client placeholders, toolbar slots and action
213
- triggers — `HTML-WIDGET.md` is its reference. The table in
214
- `src/lib/smart-component-layout/README.md` lists what each identifier means.
215
-
216
- ## The executable version
217
-
218
- Two specs in the library are written to be read as examples, and they travel with the source:
219
-
220
- - `src/lib/smart-client/smart-component-host.spec.ts` — a widget the library knows nothing
221
- about joining model-change routing, the detach brake, and the nearest-client rule.
222
- - `src/lib/view-context/smart-ui-action/smart-action-host.spec.ts` — a subtree supplying the
223
- actions its toolbars render, and the fallback when it does not.
224
-
225
- If a rule here and one of those specs disagree, the spec is right — say so and this document
226
- 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 toolbar does not have to be in a template at all. The HTML widget **hydrates** one into every
154
+ toolbar slot of the html the backend sent (`<smart-ui-action-toolbar data-sb4-toolbar="…">`), and
155
+ such a toolbar resolves by the very same pull: a `SmartActionHost` above it if one has a list, the
156
+ screen component otherwise. That is why a row layout's html gets the row's actions on a grid card
157
+ with no code of its own, and a table cell rendered from a column template gets them the same way.
158
+ An action trigger (`data-sb4-action`) looks its action up in the same list, and an icon slot
159
+ (`data-sb4-icon`, `data-sb4-icon-key`) hydrates into a `smart-icon` the same way. See
160
+ `HTML-WIDGET.md`.
161
+
162
+ **A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
163
+ `readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
164
+ toolbar is already rendering never reached the screen under zone.js either — it only appeared
165
+ to work when some other event happened to tick the application.
166
+
167
+ ```ts
168
+ this.actions = this.actions.map((a) =>
169
+ a.uiAction.code === code ? { ...a, cssClass: 'active' } : a
170
+ );
171
+ ```
172
+
173
+ Reassigning the **array** is what re-renders. `this.actions[0] = { …this.actions[0] }`
174
+ compiles and does nothing.
175
+
176
+ ## Three traps that have actually bitten
177
+
178
+ 1. **Declare your inputs — a plain public field is not one.**
179
+ `ComponentFactoryService` instantiates the components the library creates imperatively
180
+ (grid cards, expandable content, table cell components, the form's `COMPONENT` widget). It
181
+ writes every **declared** input with `ref.setInput()`, so a signal input, an aliased input
182
+ and `ngOnChanges` all behave exactly as they would under a template binding. An
183
+ **undeclared** field is still assigned — host components are free to have plain public
184
+ fields and 7.0 does not break them — but it warns, and such a field can never become a
185
+ signal input.
186
+
187
+ Until 7.0 the service assigned the field in every case, which *destroyed* a signal input:
188
+ the input is a function on the instance, so assigning over it replaced the function with
189
+ the value and the next `this.x()` threw `x is not a function`. That cost two debugging
190
+ rounds in the library (`parentLayoutComponent`, `gridRow`). If you are porting a widget
191
+ that carries a comment about this, the constraint is gone.
192
+
193
+ 2. **`@for (… ; track item)` over objects the backend rebuilds destroys the subtree every
194
+ refresh** (`NG0956`), and the DOM churn is real — a form inside is rebuilt. The backend
195
+ hands back new objects on each model refresh, so track by `$index`, or by a key you
196
+ synthesize yourself. Do not reach for a backend `identifier` field that is only sometimes
197
+ set: an all-`undefined` sibling list is `NG0955`, which is worse.
198
+
199
+ 3. **Style application is clear-then-apply.** `SmartStyleUtility.applyStyle` remembers what
200
+ it applied to an element and removes exactly that before applying the next set. If you
201
+ decorate an element the library also styles, decorate your *own* child element instead of
202
+ pushing classes into the object the backend sent — that object is shared, and mutating it
203
+ is invisible to change detection anyway.
204
+
205
+ ## Library widgets a backend layout can place
206
+
207
+ A `SmartComponentLayoutDefinition` node of type `WIDGET` places one of the library's widgets by
208
+ `widget.identifier`: `grid`, `tree`, `filter` (`smart-filter-widget`, a filter builder whose
209
+ identifier is its `filterId`), `toolbar`, `map`, `diagram` and `embedded_slot`. Each of them
210
+ follows the contract above, so a widget placed by the layout and one written into a page
211
+ template by hand register with the same client the same way. Inside a `FORM` node the form
212
+ widget type `HTML` places server-sent html with client placeholders, toolbar slots and action
213
+ triggers — `HTML-WIDGET.md` is its reference. The table in
214
+ `src/lib/smart-component-layout/README.md` lists what each identifier means.
215
+
216
+ ## The executable version
217
+
218
+ Two specs in the library are written to be read as examples, and they travel with the source:
219
+
220
+ - `src/lib/smart-client/smart-component-host.spec.ts` — a widget the library knows nothing
221
+ about joining model-change routing, the detach brake, and the nearest-client rule.
222
+ - `src/lib/view-context/smart-ui-action/smart-action-host.spec.ts` — a subtree supplying the
223
+ actions its toolbars render, and the fallback when it does not.
224
+
225
+ If a rule here and one of those specs disagree, the spec is right — say so and this document
226
+ gets fixed.