@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/MIGRATION-7.0.md +1722 -1722
- package/README.md +199 -199
- package/WIDGETS.md +215 -206
- package/fesm2022/smartbit4all-ng-client.mjs +997 -953
- package/fesm2022/smartbit4all-ng-client.mjs.map +1 -1
- package/package.json +1 -1
- package/smartbit4all-ng-client-7.0.9.tgz +0 -0
- package/types/smartbit4all-ng-client.d.ts +22 -1
- package/smartbit4all-ng-client-7.0.7.tgz +0 -0
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
|
-
##
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|