@smartbit4all/ng-client 7.3.32 → 7.3.34

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 CHANGED
@@ -1,2280 +1,2280 @@
1
- # @smartbit4all/ng-client 6.x → 7.0 migration guide
2
-
3
- > 7.0.0 targets **Angular 22.0.8 / TypeScript 6.0.3 / Material 22.0.6**, with PrimeNG fully
4
- > removed (Material/CDK only), every library component `OnPush`, and **no zone.js** — the
5
- > configuration 7.0 is tested in. Angular's `moment` date adapter is replaced by `date-fns`.
6
- > It is a major because a host has to act; nothing here is a silent behaviour change that
7
- > you could ignore.
8
-
9
- ## Where to start
10
-
11
- 1. **Run the codemod** — [Migrating a host: the codemod](#migrating-a-host-the-codemod). It
12
- does the mechanical part (the module imports, the provider configuration, the dead legacy
13
- calls) and prints `file:line` for everything that needs a decision.
14
- 2. Read [Full standalone + `provideSmartNgClient()`](#full-standalone--providesmartngclient-phase-32--the-big-one).
15
- That section rewrites your `app.module.ts`; everything else is small next to it.
16
- 3. Read [Dates](#dates-moment--date-fns-and-the-timezone-contract-phase-38). It is the one
17
- change that needs action **at install time**, and the one that breaks at *runtime* on the
18
- first screen with a date field rather than at build time.
19
- 4. Then the rest in order. The **PrimeNG removal** sections (1.1–1.10) are API changes; the
20
- **hop sections** carry the framework defaults you inherit (OnPush, fetch vs XHR, `?.`).
21
- 5. If you write your own widget, `WIDGETS.md` next to this file is the protocol.
22
-
23
- Measured across the 12 hosts in scope on 2026-07-28, so you know what to expect: 12–17
24
- `Smart*Module` imports per host, 1–20 string-keyed providers, `.cssClass =` on an action
25
- entry at 72 sites in 7 hosts, `this.uiActionModels =` in 5 hosts, `UseUiAction` in 6.
26
-
27
- **How big is this for *your* host?** Codemod dry-run counts for the five hosts in scope
28
- (2026-07-30). The first column the codemod applies for you; the second is the list it refuses to
29
- guess at, which is the real work:
30
-
31
- | host | mechanical | needs a decision |
32
- |---|---|---|
33
- | p014 *(done — the reference)* | 121 | ~100 |
34
- | p043 | 121 | 96 |
35
- | app-tournament | 86 | 34 |
36
- | app-finance-ai | 62 | 18 |
37
- | app-fitnessmirror | 61 | 14 |
38
-
39
- The spread is not about host size — all five are within 274–841 source files — but about which
40
- APIs they happened to use. p043 is p014's twin and repeats its whole decision list; the other
41
- three have **no `.cssClass =` sites at all**, and their lists are dominated by `smart-modules`,
42
- the 3-line `moment-adapter` swap, and `zone-js` (which is **optional** — dropping zone.js is not
43
- required). Take a small one first if you want the process shaken out cheaply.
44
-
45
- **One number that is now zero: the library install.** p014 was migrated against an unpublished
46
- build and paid for it in hand-copies, tarballs and `ERESOLVE`. 7.0.0 is on npm, so a host today
47
- writes `"@smartbit4all/ng-client": "^7.0.0"` and runs `npm i`. None of that thread applies to you.
48
-
49
- ## Migrating a host: the codemod
50
-
51
- It lives in the `platform-angular2` repository — `tools/ng-client-7-codemod/` — and you run
52
- it with plain Node, from a checkout, against your own source directory. Nothing to install.
53
-
54
- ```bash
55
- node tools/ng-client-7-codemod/migrate.mjs ../my-host/ui-angular/src --dry-run # look first
56
- node tools/ng-client-7-codemod/migrate.mjs ../my-host/ui-angular/src # then write
57
- ```
58
-
59
- It **refuses to run on a dirty git tree** (its diff is how you review it, and `git checkout .`
60
- is how you undo it), it is **idempotent**, and it prints a verdict for every rule it knows —
61
- including `no sites found`, because a codemod that silently matched nothing looks exactly
62
- like one that had nothing to do.
63
-
64
- **What it rewrites:** the `Smart*Module` imports (into the standalone declarables of the
65
- mapping table below), the string-keyed providers and both `forRoot()` payloads (into one
66
- `provideSmartNgClient(config, ...features)` call), the redundant core services in the root
67
- providers array, `this.useQueryLists = true` / `handleDataChangeSubscriptions()`,
68
- `[parentSmartComponent]` and the dead `[parent]` inputs,
69
- `execute(x.uiAction, x)` → `execute(x)`, the `async` / `await` around
70
- `getActionDescriptor`, the `SmartdialogService` `super(…)` call and its constructor
71
- parameters, and PrimeNG module imports **no template in your repository renders**.
72
-
73
- **What it refuses to guess at**, printing `file:line` and the section here that explains it:
74
- `UseUiAction` → `UiActionExecutor`, `this.uiActionModels =`, `entry.cssClass =`, `addForm()`,
75
- the 4th argument of `addGrid()`, `getActionDescriptor(…).then(…)`, the moment adapter,
76
- component-level providers, every remaining `primeng/*` import, and `::ng-deep` selectors that
77
- reach into the form widget.
78
-
79
- **What it deliberately does not do:** convert *your* components to standalone. That is
80
- Angular's own schematic (`ng generate @angular/core:standalone`) and it is not required — an
81
- NgModule's `imports:` array takes standalone components, which is exactly what the codemod
82
- puts there. Do the library migration first, get the app running, then decide about standalone
83
- separately.
84
-
85
- Afterwards: run your formatter, swap the date packages ([Dates §1](#1-swap-the-peer-dependency)),
86
- build (expect one `NG8001` per template element whose component is not imported yet — that
87
- loop terminates), and **smoke the app**, because the DI changes are invisible to the compiler.
88
-
89
- ## Breaking changes
90
-
91
- ### Dialog stack: PrimeNG dynamicdialog → MatDialog (Phase 1.1)
92
-
93
- - `SmartdialogService` and `SmartViewContextDialogService` constructors no longer
94
- take a PrimeNG `DialogService` parameter. If your host extends or instantiates
95
- these services, drop the argument (MatDialog is injected internally).
96
-
97
- Both arguments PrimeNG contributed are gone, so the base constructor lost two of
98
- its four parameters:
99
-
100
- ```ts
101
- // 6.x
102
- constructor(dialog: MatDialog, dialogService: DialogService, inject: Injector,
103
- @Inject(COMPONENT_LIBRARY) compLib: ComponentLibrary) {
104
- super(dialog, dialogService, inject, compLib);
105
- }
106
- // 7.0
107
- constructor(dialog: MatDialog, inject: Injector) {
108
- super(dialog, inject);
109
- }
110
- ```
111
-
112
- **The codemod does this** (rule `smartdialog-super`), including the constructor
113
- parameters and the now-unused `primeng/dynamicdialog` import — except where a
114
- dropped parameter carries an accessibility modifier (`protected dialogService: …`),
115
- which declares a class *property* the body may still read. Those are reported.
116
- - The `ComponentLibrary` switch no longer has a PrimeNG branch in the dialog
117
- layer — all dialogs open via `MatDialog` regardless of `COMPONENT_LIBRARY`.
118
-
119
- ### smart-form widgets: PrimeNG removed (Phase 1.2)
120
-
121
- - `ComparableDropdownDirective` and `ComparableMultiselectDirective` are removed
122
- (they decorated the PrimeNG `p-dropdown` / `p-multiSelect` elements, which no
123
- longer exist). Use `SmartformwidgetComponent.compareItems` semantics via
124
- `mat-select [compareWith]`, which the widget template already wires up.
125
- - `SmartformwidgetComponent` constructor no longer takes the injected
126
- `COMPONENT_LIBRARY` token; the widget renders Material-only.
127
- - `SmartformwidgetComponent` removed PrimeNG-only public members:
128
- `errorMessages`, `getControlMessages()`, `onPrimeRichTextEditorContentChanged()`,
129
- `safeDropDownBlur()`/`onDropDownShow()`/`onDropDownInput()`,
130
- `safeCalendarBlur()`/`onCalendarShow()`/`onCalendarInput()`, `defaultDate`.
131
- - `SmartFormService.setValuesFromModel()` / `toFormGroup()` /
132
- `createFormControls()` no longer take a `ComponentLibrary` parameter.
133
- - `SmartViewContextModule` no longer imports the PrimeNG form modules
134
- (Dropdown, MultiSelect, Calendar, Chips, InputSwitch, Checkbox, InputNumber,
135
- InputText, InputTextarea, InputMask, FloatLabel, Messages) and no longer
136
- provides `Dropdown` / `MultiSelect`.
137
-
138
- ### fileupload: PrimeNG removed (Phase 1.4)
139
-
140
- - `SmartFileUploaderComponent` (selector `smart-file-uploader4sc`) is removed
141
- from the public API. It was a PrimeNG `p-fileUpload` wrapper declared in
142
- `SmartNgClientModule` but referenced nowhere; the Material-style
143
- `SmartfileuploaderComponent` (selector `smartfileuploader`) is the uploader.
144
- - The internal `PrimeFileUploaderComponent` (selector `prime-file-uploader`)
145
- is removed; `smart-upload-widget` always renders `smartfileuploader`.
146
- - `UploadWidgetComponent` constructor no longer takes the injected
147
- `COMPONENT_LIBRARY` token, and its PrimeNG-only members are gone
148
- (`fileUploadPrime`, `uploadFiles()`).
149
- - `SmartNgClientModule` and `SmartViewContextModule` no longer import the
150
- PrimeNG `FileUploadModule`; `SmartViewContextModule` also dropped the dead
151
- `OverlayPanelModule` import (Phase 1.3).
152
-
153
- ### editor: Quill 1.3 → 2, quill-emoji removed (Phase 1.5)
154
-
155
- - Peer dependencies changed: `quill` `^1.3.7` → `^2.0.3`, `ngx-quill`
156
- `16.2.1 - 24.0.5` → `^25.3.3`. Hosts must bump both together (ngx-quill 25.x
157
- is the Quill 2 line for Angular 17; later Angular hops will raise it further).
158
- - `quill-emoji` peer dependency is **removed** (dead, Quill 1-only, unmaintained).
159
- If a host needs emoji support, pick a Quill 2-compatible module on its own.
160
- - `@types/quill` must be removed from host devDependencies — Quill 2 ships its
161
- own TypeScript types, and the stale `@types/quill` 1.x conflicts with them.
162
- - The bundled `quill.snow.css` (imported by the smart-form widget) is now the
163
- Quill 2.0.3 stylesheet; hosts that import `quill/dist/quill.snow.css`
164
- themselves must serve the Quill 2 version.
165
- - `SmartViewContextModule` no longer imports the PrimeNG `EditorModule`
166
- (`<p-editor>` died with Phase 1.2; RICH_TEXT renders via ngx-quill).
167
-
168
- ### smart-diagram: p-chart replaced by direct chart.js (Phase 1.6)
169
-
170
- - `SmartDiagramComponent` renders its own `<canvas>` and instantiates chart.js
171
- directly; the PrimeNG `<p-chart>` wrapper (and the `ChartModule` import in
172
- `SmartDiagramModule`) is gone. Chart.js controllers are now registered by the
173
- component itself (`Chart.register(...registerables)`), so no `chart.js/auto`
174
- import is needed anywhere.
175
- - The public `chart` property (and `getChart()`) is now the chart.js `Chart`
176
- instance instead of the PrimeNG `UIChart` component. `getBase64Image()` and
177
- `refresh()` keep their signatures (delegating to `toBase64Image()` /
178
- `update()`).
179
- - `SmartDiagramComponent` constructor no longer takes the injected
180
- `COMPONENT_LIBRARY` token; the component has a single render path.
181
- - DOM/CSS hooks changed: the internal structure is
182
- `div.chart-host > div.chart-container > canvas`; host styles targeting
183
- `::ng-deep p-chart` no longer match. The aspect-ratio classes
184
- (`default-aspect-ratio` / `pie-aspect-ratio`) are now applied on
185
- `.chart-host` and size `.chart-container`.
186
-
187
- ### smart-grid: PrimeNG branch removed (Phase 1.7)
188
-
189
- - `SmartGridComponent` renders Material-only. The `@if (compLib === PRIMENG)`
190
- template branch (the inline `p-table` / `p-paginator` / `p-menu` /
191
- `p-multiSelect` grid) is gone; the grid always renders through the PrimeNG-free
192
- `smart-table` (the `#table` slot), `mat-tree`, `app-smart-grid-card`, and
193
- `mat-paginator`.
194
- - `SmartGridComponent` constructor no longer takes the injected
195
- `COMPONENT_LIBRARY` token, nor the `SmartDatePipe` / `SmartDateTimePipe` /
196
- `SmartTimePipe` (they only fed the removed inline `p-table` cell renderer);
197
- those pipes are dropped from the component `providers` too.
198
- - Removed PrimeNG-only public members: `columns`, `menuButtons`, `menu`,
199
- `onOptionsClick()`, `previousmultiSortMeta`, `gridSort()`, `lazyLoad()`,
200
- `headerChange()`, `onColOrder()`, `getOrderColumNames()`, `getColValue()`,
201
- `onPrimeChangePage()`, `onRowSelect()`, `onRowUnselect()`, `onSelectAllRow()`,
202
- `getImageResourceIcons()`, `getImageResourceStyle()`, `getRowColumnAction()`,
203
- `showCellToolbar()`, `shouldShowOptionsButton()`, `calculateMenuActions()`,
204
- `createCellToActionMap()`, `getRowMenuActionModelArray()`, `rowTrackByFn()`,
205
- `cellToActionMap`, `columnMetaByName`, and the `_headerToolbar`
206
- (`#headerToolbar`) view child. The `headerToolbar` getter now returns the
207
- smart-table's header toolbar unconditionally (its only remaining consumer,
208
- `SmartComponentApiClient`, is unaffected).
209
- - `SmartGridModule` no longer imports the PrimeNG `TableModule`, `ButtonModule`,
210
- `MenuModule`, `PaginatorModule`, `MultiSelectModule`; `MatTooltipModule` is
211
- added (the grid refresh button is now a Material `mat-icon-button` +
212
- `matTooltip` instead of `pButton` + `pTooltip`).
213
- - Note: this is the PrimeNG-branch deletion only. The `smart-grid` internals
214
- rewrite to `mat-table` + `MatPaginator` and the `smart-grid`/`smart-table`
215
- merge remain a Phase 3 (post-Angular-22) change.
216
-
217
- ### misc widgets: PrimeNG branches removed (Phase 1.8)
218
-
219
- The remaining components that still carried a dead PrimeNG branch alongside an
220
- already-active Material one are now Material-only. In every case the Material
221
- branch was the rendered path under `COMPONENT_LIBRARY = MATERIAL` (the value all
222
- 7.0 hosts use), so this is render-neutral; the `COMPONENT_LIBRARY` token, where
223
- these components still injected it, is now ignored by them.
224
-
225
- - `ExpandableSectionComponent` renders Material-only (the `p-accordion` branch is
226
- gone; always `mat-expansion-panel`). Its constructor no longer takes the
227
- injected `COMPONENT_LIBRARY` token. `SmartExpandableSectionModule` no longer
228
- imports the PrimeNG `AccordionModule`.
229
- - `UiActionButtonComponent` renders Material-only (the `pButton`/`pRipple` branch
230
- is gone; always `mat-button`). Its constructor no longer takes the optional
231
- `COMPONENT_LIBRARY` token (it previously defaulted to `PRIMENG` when unset).
232
- `getType()` no longer has a PrimeNG class-map branch — it returns the
233
- `mat-mdc-*` class strings unconditionally. (The dynamic `getbtnClass()`'s
234
- `p-button-<color>` class token was later dropped in Phase 1.9.)
235
- - `UiActionToolbarComponent` constructor no longer takes the injected
236
- `COMPONENT_LIBRARY` token. The dead `getType()` / `getbtnClass()` methods
237
- (never referenced by the template) were removed, and the scroll-affordance
238
- buttons use the Material icons `arrow_back` / `arrow_forward` unconditionally
239
- (the PrimeNG `chevron-left` / `chevron-right` fallback is gone).
240
- - `UiActionConfirmDialogComponent` and `UiActionInputDialogComponent` render
241
- their close button as a Material `mat-icon-button` unconditionally (the `@else`
242
- `p-button` branch is gone); both constructors no longer take the injected
243
- `COMPONENT_LIBRARY` token.
244
- - `SmartIconModule` no longer imports the PrimeNG `BadgeModule` (badges render
245
- via the custom `ui-badge` component, unaffected).
246
- - `SmartViewContextModule` no longer imports the PrimeNG `ButtonModule`,
247
- `TooltipModule`, `ToastModule`, `ImageModule`, or `OrderListModule` (all
248
- unused after the branch deletions). The `primeng/api` `SharedModule`
249
- (`PrimeSharedModule`) import was later dropped in Phase 1.9's final sweep.
250
-
251
- ### PrimeNG package dependency removed (Phase 1.9)
252
-
253
- The `primeng` npm package is gone. Most importantly it is dropped from
254
- `@smartbit4all/ng-client`'s own **`peerDependencies`** (in
255
- `projects/smart-ng-client/package.json`, which flows into the published
256
- `npms/smart-ng-client/package.json`) — that entry is what previously forced every
257
- consuming host to install PrimeNG. It is also removed from the dev workspace
258
- (`package.json`, `package-lock.json`, `node_modules`), and both PrimeNG theme
259
- stylesheets (`primeng/resources/themes/saga-blue/theme.css`,
260
- `primeng/resources/primeng.min.css`) are removed from the app-playground `styles`
261
- in `angular.json`. Hosts that still list `primeng` as a dependency or load its
262
- theme CSS should remove both; 7.0 renders entirely on Material/CDK.
263
-
264
- #### Your own PrimeNG usage
265
-
266
- Removing the peer dependency does not remove PrimeNG from *your* code, and the measured
267
- hosts carry more of it than their authors expect — mostly as **dead module imports**. Across
268
- the 12 hosts, `DialogService` is imported 56 times, but 8 of those in p014 turned out to be
269
- nothing but the base-constructor argument above, and four PrimeNG modules in its
270
- `app.module.ts` rendered nothing at all.
271
-
272
- So the codemod separates the two cases, and the separation is decidable rather than
273
- guessed:
274
-
275
- - **`primeng-modules`** — a PrimeNG NgModule in an `imports:` array is **deleted** when none
276
- of its selectors appears in any template in the repository. The scan runs over `.html`
277
- *and* `.ts` (for inline templates) before anything is rewritten, and it distinguishes an
278
- element (`<p-tree>`) from an attribute directive (`pTooltip`) so that a leftover
279
- `class="p-button"` is not read as a component still in use. What the scan found is printed
280
- in the header, because it is what licenses the deletions.
281
- - **`primeng-imports`** — everything else from `primeng/*` is reported with its replacement.
282
- These are the real ports.
283
-
284
- The replacements, for what the hosts actually import:
285
-
286
- | PrimeNG | Material / CDK |
287
- |---|---|
288
- | `p-button` / `pButton` | `MatButtonModule` — `mat-button`, `mat-icon-button` |
289
- | `p-tree` | the library's `<smart-tree>`, or `MatTreeModule` |
290
- | `p-progressSpinner` | `MatProgressSpinnerModule` — `<mat-spinner>` |
291
- | `pTooltip` | `MatTooltipModule` (`matTooltip`), or `SmartTooltipDirective` |
292
- | `p-sidebar` | `MatSidenavModule` |
293
- | `pInputText` | `MatInputModule` — `matInput` inside `<mat-form-field>` |
294
- | `p-fieldset` | `MatCardModule`, or `<fieldset>` + `<mat-divider>` |
295
- | `p-divider` | `MatDividerModule` |
296
- | `p-accordion` | `MatExpansionModule`, or `<smart-expandable-section>` |
297
- | `p-table` | the library's `<smart-grid>`, or `MatTableModule` |
298
- | `p-editor` | ngx-quill's `<quill-editor>` (already a library dependency) |
299
- | `p-badge` / `pBadge` | `MatBadgeModule`, or `UiBadgeComponent` / `UiBadgeDirective` |
300
- | `p-menu`, `p-overlayPanel` | `MatMenuModule`, or the CDK Overlay |
301
- | `p-inputSwitch` | `MatSlideToggleModule` |
302
- | `p-dialog` | `MatDialog` — open a component instead of toggling `[visible]` |
303
- | `DialogService` | `MatDialog`. There is **no `header` option**: put `<h2 mat-dialog-title>` in the component |
304
- | `DynamicDialogRef` | `MatDialogRef` — `afterClosed()` replaces `onClose` |
305
- | `DynamicDialogConfig` | `MAT_DIALOG_DATA` for the data, `MatDialogRef` for the rest |
306
- | `PrimeNGConfig` | **delete it.** `setTranslation()` of day and month names is already covered by `MAT_DATE_LOCALE` + `SmartDateFnsAdapter` — the date-fns `hu` locale carries both the names and the Monday week start. **Confirmed on a running host** (p014, 2026-07-29, against a live backend): the datepicker opens on `2026. JÚL.` with `H K Sz Cs P Sz V`, i.e. Hungarian and Monday-first, exactly what the deleted `firstDayOfWeek: 1` + `dayNamesMin` used to buy. See [Dates](#dates-moment--date-fns-and-the-timezone-contract-phase-38) |
307
- | `MessageService` | `MatSnackBar` |
308
- | `ConfirmationService` | the library's `UiActionConfirmDialogService` |
309
- | `MenuItem` | the library's `UiActionModel`, or a plain `<mat-menu>` item |
310
- | `SharedModule` (`primeng/api`) / `PrimeTemplate` | a plain `<ng-template>` — PrimeNG needed `pTemplate`, Material does not |
311
-
312
- `primeicons` is a separate package. Nothing in 7.0 needs it, but nothing breaks if you keep
313
- it either — `pi pi-*` class names are just a font.
314
-
315
- ### `QuillModule.forRoot()` in your root module
316
-
317
- `provideSmartNgClient()` calls `importProvidersFrom(QuillModule.forRoot())` itself — ngx-quill
318
- has no provider function, so the module is the only way to reach its root configuration. A
319
- root module that also calls it is configuring quill twice, so the codemod removes the call
320
- and its import. In a *feature* module the same call is that module's own configuration and is
321
- left alone.
322
-
323
- **Keep `ngx-quill` and `quill` in your `package.json`.** They are peer dependencies of the
324
- library — the host provides them. npm installs peers automatically, which is why hosts have
325
- got away with not declaring them, but any install that runs with `--legacy-peer-deps` drops
326
- them silently and the next build fails on an import that has always worked. Two of the twelve
327
- measured hosts were in exactly that state.
328
-
329
- **7.0 adds four peer dependencies 6.x did not have**, and they bite in a way the TypeScript
330
- errors do not prepare you for:
331
-
332
- ```bash
333
- npm install ngx-mask@^22.0.0 fast-equals@^5.0.1 wavesurfer.js@^7.9.5 @angular/youtube-player@^22.0.0
334
- ```
335
-
336
- If you install the library the normal way, npm brings these in for you. If you *swap a local
337
- build into `node_modules` by hand* — which is how you would try the library before it is
338
- published — npm never runs, so nothing installs them. The symptom arrives **after** the last
339
- TypeScript error is fixed, as five `Module not found` errors reported against the library's
340
- own `fesm2022` bundle rather than against any file of yours:
341
-
342
- ```
343
- ./node_modules/@smartbit4all/ng-client/fesm2022/smartbit4all-ng-client.mjs:7:0-60 -
344
- Error: Module not found: Error: Can't resolve 'ngx-mask'
345
- ```
346
-
347
- Four packages, five errors: `wavesurfer.js` is missing twice, once for its `record` plugin.
348
-
349
- - `UiActionButtonComponent.getbtnClass()` no longer emits the dead
350
- `p-button-<color>` class token — it returns only `sb4-<color>` (nothing styled
351
- `.p-button-*`; the platform button theme keys off `sb4-<color>` in
352
- `custom-theme.scss`). Hosts with their own `.p-button-<color>` overrides for
353
- sb4 action buttons should migrate them to `.sb4-<color>`.
354
- - `SmartViewContextModule` no longer imports the `primeng/api` `SharedModule`
355
- (`PrimeSharedModule`); no template in the module used `pTemplate`.
356
- - `SmartComponentLayoutModule` no longer imports `InputGroupModule` /
357
- `InputGroupAddonModule` (they were dead — no `p-inputGroup` template existed).
358
-
359
- The `ComponentLibrary` enum still exists with only its `MATERIAL` member in
360
- active use; its `PRIMENG` member and the remaining dead `ComponentLibrary.PRIMENG`
361
- code branches (icon/menu/widget components, dead `.p-*`/`.pi` CSS) are removed in
362
- a follow-up internal-cleanup step (1.10) — they are not a package dependency and
363
- do not affect hosts. `deviceInfo.componentLibrary` continues to report
364
- `material` to the backend.
365
-
366
- ### ComponentLibrary / COMPONENT_LIBRARY removed entirely (Phase 1.10)
367
-
368
- 7.0 is Material-only, so the `ComponentLibrary` abstraction (which had no second
369
- implementation once PrimeNG was gone) is deleted outright.
370
-
371
- - **Host action required:** `ComponentLibrary` (enum) and `COMPONENT_LIBRARY`
372
- (injection token) are no longer exported from `@smartbit4all/ng-client`. Hosts
373
- that provide `{ provide: COMPONENT_LIBRARY, useValue: ComponentLibrary.MATERIAL }`
374
- in their root providers must **remove that provider and its import** — it now
375
- fails to compile (no exported member). No replacement is needed; every component
376
- renders Material-only.
377
- - Every `@Inject(COMPONENT_LIBRARY)` constructor argument is gone from the library
378
- (smart-icon, click/hover tiered menu, photo-capture / voice-record widgets,
379
- smart-voice-recorder, smart-file-editor, smart-multi-file-editor, sortable
380
- widget, smart-filter-editor-content, validation-result-page, message-dialog, the
381
- dialog services `SmartdialogService` / `SmartViewContextDialogService` /
382
- `SmartViewContextErrorDialogService` / the ui-action confirm/input/file-upload
383
- dialog services, and `PdfViewerDialogService` in `@smartbit4all/document-explorer`).
384
- The `SmartdialogService` base constructor is now `(dialog, injector)` — subclasses
385
- that called `super(dialog, dialogService, injector, compLib)` keep the first and
386
- third arguments only. See [Phase 1.1](#dialog-stack-primeng-dynamicdialog--matdialog-phase-11);
387
- the codemod rewrites it.
388
- - `SmartViewContextService` no longer exposes the public `componentLibrary` field
389
- and no longer injects the token; it now writes the literal `'material'` into
390
- `deviceInfo.componentLibrary` on every view-context update (the backend keys its
391
- icon set on this string — `images-playground.properties` has `.material=` vs
392
- `.primeng=` variants — so the value keeps flowing; the DTO field is unchanged).
393
- - All dead `ComponentLibrary.PRIMENG` branches and their PrimeNG icon names
394
- (`chevron-right`, `pi pi-*`, `power-off`, `trash`, `file`, `exclamation-circle`,
395
- `play-circle`/`stop-circle`, etc.) are removed; the Material icon path is now
396
- unconditional. Dead `.p-*` / `.pi` CSS rules were purged from the library
397
- (smart-icon, smartform, smartformwidget, the file editors/uploader,
398
- ui-action-file-upload-dialog) and from the app-playground `custom-theme.scss`;
399
- the app-playground `index.html` no longer loads the `primeicons` CDN stylesheet.
400
- - After 1.10, `grep -rin primeng projects` reports a single residual: the
401
- auto-generated `deviceInfo.ts` DTO doc comment (`(Material, PrimeNg)`), which
402
- mirrors the backend OpenAPI field description and is regenerated from it — left
403
- untouched by design (the field itself is intentionally kept).
404
-
405
- ### Angular 18 hop (Phase 2, 17 → 18)
406
-
407
- The library line now builds against Angular 18. Host-relevant changes:
408
-
409
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
410
- `@angular/*` `^18.0.0`, `@angular/youtube-player` `^18.2.14` (was lagging on
411
- `^16` — its `^16 || ^17` peer range breaks a strict `npm install` on Angular
412
- 18, so it can no longer lag), `ngx-quill` `^26.0.0` (the Angular 18 line;
413
- quill stays `^2.0.3`), `ngx-mask` `^18.0.0`, and — for
414
- `@smartbit4all/document-explorer` — `ngx-extended-pdf-viewer` `^21.0.0`
415
- (the 19.x/20.x lines cap their Angular peer below 18).
416
- - **`parchment` hoisting quirk (ngx-quill 26):** the ngx-quill 26 typings
417
- reference `import("parchment")`, but npm may leave `parchment` nested under
418
- `node_modules/quill/` (even after `npm dedupe`), which fails the host build
419
- with `TS2307: Cannot find module 'parchment'`. Fix: add
420
- `"parchment": "^3.0.0"` to the host devDependencies so it is hoisted to the
421
- top level (quill 2 depends on parchment 3, so versions cannot conflict).
422
- - **Material theming (M2 compat APIs):** the `ng update @angular/material@18`
423
- schematic rewrites M2 theme SCSS to the prefixed compat API
424
- (`mat.define-palette` → `mat.m2-define-palette`,
425
- `mat.$indigo-palette` → `mat.$m2-indigo-palette`,
426
- `mat.define-light-theme` → `mat.m2-define-light-theme`, …). The theme stays
427
- visually **M2** — no M3 switch happens at this hop; hosts running the
428
- schematic get this rewrite automatically.
429
- - **`HttpClientModule` deprecation:** the `ng update @angular/core@18`
430
- migration replaces `HttpClientModule` imports with
431
- `provideHttpClient(withInterceptorsFromDi())`. Not a library breaking
432
- change (the lib's generated API services were migrated internally), but
433
- hosts will get the same automatic migration in their own modules.
434
- - TypeScript requirement raised to `>=5.4` (the repo builds on 5.5).
435
-
436
- ### Angular 19 hop (Phase 2, 18 → 19)
437
-
438
- The library line now builds against Angular 19. Host-relevant changes:
439
-
440
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
441
- `@angular/*` `^19.0.0`, `@angular/youtube-player` `^19.0.0`,
442
- `ngx-quill` `^27.0.0` (the Angular 19 line; quill stays `^2.0.3`),
443
- `ngx-mask` `^19.0.0`, and — for `@smartbit4all/document-explorer` —
444
- `ngx-extended-pdf-viewer` `^22.0.0` (the 21.x line caps its Angular peer
445
- below 19).
446
- - **ngx-extended-pdf-viewer 22 input rename:** the `[showScrollingButton]`
447
- input no longer exists — it is `[showScrollingButtons]` (plural,
448
- `ResponsiveVisibility`). Hosts that use `<ngx-extended-pdf-viewer>` directly
449
- must rename the binding, otherwise the template fails with NG8002.
450
- - **Standalone-by-default:** in Angular 19 components/directives/pipes are
451
- standalone unless declared otherwise. The `ng update @angular/core@19`
452
- migration automatically adds `standalone: false` to every NgModule-declared
453
- declarable in the host codebase — run it and review the (large, mechanical)
454
- diff. Declarables not referenced by any NgModule are skipped by the
455
- schematic.
456
- - **Material theming:** the `ng update @angular/material@19` schematic splits
457
- `@include mat.core()` into `@include mat.elevation-classes()` +
458
- `@include mat.app-background()`. No visual change; hosts running the
459
- schematic get this rewrite automatically.
460
- - **New NG8111 extended diagnostic** (warning): "Function in event binding
461
- should be invoked". Uninvoked event bindings like
462
- `(click)="(someCallback)"` — previously silently dead — are now flagged at
463
- build time. Warning only, the build stays green.
464
- - TypeScript requirement stays satisfied by 5.5 (Angular 19 supports
465
- TS 5.5–5.8). `ng update @angular/core@19` can install **TS 5.9** all the same, and the
466
- build then stops with "requires TypeScript >=5.5.0 and <5.9.0"; pin `typescript ~5.8.3`
467
- (p009-angular, 2026-10-27).
468
- - TS 5.8 reports two dead constructs as errors: **TS2869** on a `` `${x}` ?? '' `` (a template
469
- literal is never nullish) and **TS2353** on a property the widget type does not have, e.g.
470
- `direction` on a `SELECT` widget. Both never did anything; delete them (p034).
471
-
472
- ### Angular 20 hop (Phase 2, 19 → 20)
473
-
474
- The library line now builds against Angular 20. Host-relevant changes:
475
-
476
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
477
- `@angular/*` `^20.0.0`, `@angular/youtube-player` `^20.0.0`,
478
- `ngx-quill` `^28.0.0` (the Angular 20 line; quill stays `^2.0.3`),
479
- `ngx-mask` `^20.0.0`, and — for `@smartbit4all/document-explorer` —
480
- `ngx-extended-pdf-viewer` `^23.0.0` (the 22.x line caps its Angular peer
481
- below 20). No pdf-viewer API rename at this bump (unlike 22.x).
482
- - **TypeScript 5.9 required** (Angular 20 supports 5.8–5.9); `ng update`
483
- bumps it automatically. TS 5.9 adds **TS2872 "This kind of expression is
484
- always truthy"**, which turns previously-silent dead `||` alternatives into
485
- build errors — expect one or two in any older codebase.
486
- - **Material 20 button labels no longer set `white-space: nowrap`.** Material
487
- 20 dropped the MDC stylesheet (`@material/button`), which used to carry it.
488
- Multi-word button labels now wrap, and any rule that lets the button size to
489
- its content (the lib's `.mdc-button { height: fit-content }`) makes the
490
- button grow taller. The lib restores the pre-20 look inside
491
- `ui-action-button` (`.mdc-button__label { white-space: nowrap }`); **hosts
492
- that render Material buttons of their own must add the same rule** if they
493
- relied on single-line labels (typical symptom: a navigation bar that
494
- suddenly wraps and overflows into scroll arrows).
495
- - **Material 20 renamed the MDC-era CSS custom properties**: e.g.
496
- `--mdc-text-button-container-height` → `--mat-button-text-container-height`.
497
- The `ng update @angular/material@20` schematic rewrites the ones it finds in
498
- `.css`/`.scss` files; token names embedded in TS strings or host templates
499
- must be renamed by hand.
500
- - **`DOCUMENT` moved from `@angular/common` to `@angular/core`.** Automatic
501
- migration; `@angular/common`'s re-export is deprecated.
502
- - **`moduleResolution: "node"` → `"bundler"`** in `tsconfig.json` (and any
503
- `tsconfig.lib.json` that sets it explicitly). Automatic migration; it changes
504
- how sub-path exports resolve, so a host with hand-written `paths` entries
505
- should re-verify its build. A deep import such as
506
- `import { UiAction } from '@smartbit4all/ng-client/projects'` stops resolving
507
- (TS2307); import from `@smartbit4all/ng-client` instead. A 4.5-era deep import of a
508
- generated model (`@smartbit4all/document-explorer/lib/api/...`,
509
- `@smartbit4all/ng-client/lib/smart-generic-pages/api/...`) breaks the same way and has no
510
- root export: take the host's own generated model, or derive the type from the field it
511
- copies (`AdminTemplate['file']`) (p034).
512
- - **A host without `skipLibCheck` that still imports `primeng/dynamicdialog` stays red from
513
- this hop**: PrimeNG 17's typings name `InjectFlags`, which Angular 20 removed (TS2305 inside
514
- `node_modules/primeng`). The 6.x `SmartdialogService` subclasses need that import until the
515
- codemod removes it, so this hop joins 21 and 22 as red until the 7.0 library is in (p034).
516
- - **Workspace generation defaults**: the CLI adds a `schematics` block to
517
- `angular.json` pinning the pre-20 file-naming style (`type: "component"`,
518
- `typeSeparator: "."`). Cosmetic — it only affects newly generated files.
519
- - Optional migrations **not** run here (deferred to the modernization phase):
520
- `use-application-builder` (esbuild), `control-flow-migration`,
521
- `router-current-navigation`.
522
- - The `InjectFlags`, `TestBed.get` → `TestBed.inject` and
523
- `provideServerRendering` migrations were no-ops for this codebase.
524
- - The NG8111 warnings introduced at the 19 hop are no longer emitted by the
525
- Angular 20 compiler (the dead bindings themselves are unchanged).
526
-
527
- ### Angular 21 hop (Phase 2, 20 → 21)
528
-
529
- The library line now builds against Angular 21. Host-relevant changes:
530
-
531
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
532
- `@angular/*` `^21.0.0`, `@angular/youtube-player` `^21.0.0`,
533
- `ngx-quill` `^29.0.0` (the Angular 21 line; quill stays `^2.0.3`),
534
- `ngx-mask` `^21.0.0`, and — for `@smartbit4all/document-explorer` —
535
- `ngx-extended-pdf-viewer` `^25.0.0` (the 24.x line still caps its Angular
536
- peer below 21). No pdf-viewer API rename at this bump.
537
- - **`ngx-quill` 30.x is intentionally *not* used.** 30.x is the "zoneless"
538
- line and drops the `zone.js` peer dependency; the 7.0 lib is still
539
- zone-based. Hosts that have already gone zoneless may use 30.x, but the
540
- lib is only validated against 29.x.
541
- - **TypeScript stays 5.9** and **zone.js stays 0.15** (Angular 21 accepts
542
- `~0.15.0 || ~0.16.0`). **Node** must satisfy
543
- `^20.19.0 || ^22.12.0 || >=24.0.0`.
544
- - **`MatCommonModule` is removed from `@angular/material/core` (breaking).**
545
- It was already deprecated in Material 20 (`@breaking-change 21.0.0`). Any
546
- host NgModule that imports it will fail to compile with
547
- `Unknown reference` — and because a broken NgModule cascades, the real
548
- error usually shows up as a flood of `NG8001: '<some-component>' is not a
549
- known element` / `NG6002: … does not appear to be an NgModule class`
550
- further down the build log. **Fix: just delete the import and the array
551
- entry.** `MatCommonModule` had no template surface; it only
552
- (a) applied the `cdk-high-contrast-*` body classes — `A11yModule` still
553
- does this, and Material components that need it import `A11yModule`
554
- themselves — and (b) re-exported `BidiModule`. If your host actually uses
555
- the `Dir` directive or injects `Directionality`, import
556
- `BidiModule` from `@angular/cdk/bidi` explicitly.
557
- The lib dropped it from all 10 of its own modules.
558
- - **`provideZoneChangeDetection()` is now added at bootstrap** by the
559
- `ng update` migration, e.g.
560
- `bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()] })`.
561
- This keeps zone-based change detection explicit; behaviour is unchanged.
562
- - **The control-flow migration (`*ngIf` → `@if`) now runs as a *mandatory*
563
- `ng update` migration**, not an optional one. It rewrites every structural
564
- directive in the workspace. The lib **reverted** it to keep the version hop
565
- reviewable — `NgIf`/`NgForOf`/`NgSwitch` still ship in Angular 21, so
566
- `*ngIf`/`*ngFor` keep working. Hosts may keep or revert it as they prefer;
567
- if you want it as a separate change, run
568
- `npx ng generate @angular/core:control-flow` on its own commit.
569
- - **New `NG8107` extended diagnostic** (warning): an optional chain `?.`
570
- whose left side can no longer be `null`/`undefined` under Angular 21's
571
- sharpened template inference. Warning only — expect a batch of them in any
572
- older template set.
573
- - **`tsconfig.json` loses its explicit `lib` array** (the CLI derives it from
574
- `target`). Automatic migration.
575
- - The Material and CDK v21 migration schematics made no source changes, and
576
- the `Router.lastSuccessfulNavigation` migration was a no-op. The optional
577
- `router-current-navigation` migration was not run.
578
- - The `.mdc-button__label { white-space: nowrap }` workaround introduced at
579
- the Angular 20 hop is **still required on Material 21** — the class still
580
- exists in the button template and Material still ships no `white-space`
581
- rule for it.
582
-
583
- ### Angular 22 hop (Phase 2, 21 → 22) — final hop
584
-
585
- The library line now builds against **Angular 22**. This is the biggest
586
- host-facing hop of the series: it changes the **change-detection default**, the
587
- **HTTP backend default** and the **meaning of `?.` in templates**. All three are
588
- handled by `ng update` migrations, but you must let them run.
589
-
590
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
591
- `@angular/*` `^22.0.0`, `@angular/youtube-player` `^22.0.0`,
592
- `ngx-quill` `^31.0.0` (the Angular 22 line; quill stays `^2.0.3`),
593
- `ngx-mask` `^22.0.0`, and — for `@smartbit4all/document-explorer` —
594
- `ngx-extended-pdf-viewer` `^28.0.0` (25/26/27 all cap their Angular peer
595
- below 22). **No pdf-viewer API rename at this bump.**
596
- `@ngx-translate/core` `^14` still works (its peer is `>=13`).
597
- - **TypeScript must be 6.0** (`@angular/compiler-cli` peers `>=6.0 <6.1`);
598
- **zone.js stays 0.15** (`~0.15.0 || ~0.16.0`) and **rxjs is unchanged**.
599
- **Node** must satisfy `^22.22.3 || ^24.15.0 || >=26.0.0` — note the
600
- **22.22.3** floor, which is stricter than Angular 21's `^22.12.0`.
601
-
602
- #### 1. `ChangeDetectionStrategy.OnPush` is the new default
603
-
604
- In Angular 22 the enum is redefined: `OnPush = 0` (default), `Eager = 1`, and
605
- `Default = 1` is kept as a **deprecated alias of `Eager`**. Any component
606
- without an explicit `changeDetection` therefore switches from CheckAlways to
607
- OnPush — a real behaviour change for components that mutate their own state
608
- without signals or `markForCheck()`.
609
-
610
- An `ng update` schematic **automatically adds
611
- `changeDetection: ChangeDetectionStrategy.Eager` to every component that had
612
- none**, which keeps behaviour bit-identical. The lib took exactly this route
613
- (114 components) and did **not** opt into OnPush at this hop. Recommended host
614
- approach: let the schematic run, verify your app, and move components to OnPush
615
- deliberately afterwards.
616
-
617
- Watch out for components the schematic can skip — it only touches components it
618
- can resolve. After updating, grep for `@Component` files with no
619
- `changeDetection` and decide about each one; a component that is dynamically
620
- instantiated but never statically declared can slip through.
621
-
622
- #### 2. `provideHttpClient` now defaults to the fetch backend
623
-
624
- Angular 22 switches `HttpClient` from `XMLHttpRequest` to `fetch`. An
625
- `ng update` migration inserts **`withXhr()`** into your `provideHttpClient(...)`
626
- calls to preserve the old backend:
627
-
628
- ```ts
629
- provideHttpClient(withXhr(), withInterceptorsFromDi())
630
- ```
631
-
632
- The lib does this in `SmartViewContextModule`. **Hosts that call
633
- `provideHttpClient` themselves must do the same** (or consciously move to
634
- fetch). This matters most for **upload progress**: `reportProgress` events
635
- behave differently on the fetch backend, so if your host reports upload
636
- progress, keep `withXhr()` until you have retested it.
637
-
638
- #### 3. `?.` in templates now yields `undefined`, not `null`
639
-
640
- Angular 22's safe-navigation operator returns `undefined` where it used to
641
- return `null`. Where that difference is observable, an `ng update` migration
642
- wraps the expression in the **`$safeNavigationMigration()` compiler builtin**:
643
-
644
- ```html
645
- [ngStyle]="calcStyle($safeNavigationMigration(diagramModel?.descriptor?.style))"
646
- ```
647
-
648
- This is an official builtin (handled like `$any()` — the compiler unwraps it and
649
- the type-checker sees straight through it), not a temporary marker, so wrapped
650
- templates are safe to keep. The same `ng update` run also sets the
651
- `nullishCoalescingNotNullable` and `optionalChainNotNullable` extended
652
- diagnostics to `suppress` in your tsconfigs, which silences the `NG8107`
653
- warnings the Angular 21 hop introduced.
654
-
655
- #### 4. `ComponentFactoryResolver` is removed (breaking)
656
-
657
- `ComponentFactoryResolver` is gone and `ComponentFactory` is no longer public
658
- (it only survives as `ɵRender3ComponentFactory`). Code like this stops
659
- compiling with `TS2305` / `NG2003`:
660
-
661
- ```ts
662
- constructor(private resolver: ComponentFactoryResolver) {}
663
- ngAfterViewInit() {
664
- const factory = this.resolver.resolveComponentFactory(MyComponent);
665
- this.vcRef.createComponent(factory);
666
- }
667
- ```
668
-
669
- Replace it with the type-based overload — behaviour is identical (v22 builds the
670
- same factory internally and resolves the injector from the parent):
671
-
672
- ```ts
673
- ngAfterViewInit() {
674
- this.vcRef.createComponent(MyComponent);
675
- }
676
- ```
677
-
678
- **Public-API change in the lib:** `ComponentFactoryService` no longer exposes
679
- its `factory` field. `createComponent()` / `destroyComponent()` keep their
680
- signatures, so hosts that only call those need no change.
681
-
682
- #### 5. TypeScript 6.0 deprecates `baseUrl` and `downlevelIteration`
683
-
684
- TS 6.0 raises **`TS5101`** for both options ("deprecated and will stop
685
- functioning in TypeScript 7.0"). Two ways out:
686
-
687
- - Remove them. `downlevelIteration` is a no-op at `target: ES2022`. `baseUrl`
688
- can only go once every non-relative first-party import (`from 'projects/…'`,
689
- `from 'src/…'`) is relative or covered by a `paths` entry.
690
- - Or silence them for now with `"ignoreDeprecations": "6.0"` in
691
- `compilerOptions`. This is what the lib workspace does, because `baseUrl` is
692
- still load-bearing there. It only buys time until TS 7.
693
-
694
- #### 6. Other notes
695
-
696
- - **Material 22 removed no further modules or tokens** — the CDK and Material
697
- v22 migration schematics made no source changes. CDK 22 moves to a
698
- package `exports` map (the per-entry-point sub-directories are gone from
699
- `node_modules/@angular/cdk`), but every `@angular/cdk/*` import still
700
- resolves; this only matters if you referenced those paths on disk.
701
- - The `.mdc-button__label { white-space: nowrap }` workaround introduced at the
702
- Angular 20 hop is **still required on Material 22** — the class still appears
703
- in Material's button template and Material still ships no `white-space` rule
704
- for it.
705
- - **The control-flow migration did not re-run at this hop** (it was mandatory at
706
- 21). The lib still ships `*ngIf`/`*ngFor` templates; `NgIf`/`NgForOf`/
707
- `NgSwitch` continue to work in Angular 22.
708
- - **The webpack-based builders are deprecated.** `@angular-devkit/build-angular`
709
- now prints a notice recommending `@angular/build`. Nothing breaks yet, but
710
- plan the switch.
711
- - `strictTemplates` is on by default in v22; the lib workspace was already
712
- strict, so nothing changed. If your host was not strict, the `ng update`
713
- migration adds `"strictTemplates": false` to keep it that way.
714
- - A `@HostListener('document:keydown.enter', ['$event'])` handler typed `KeyboardEvent` fails
715
- with TS2345 (`Event` is not assignable); type the parameter as `Event` when the handler only
716
- reads `target` (p034).
717
- - **An Angular CLI 1x `src/test.ts` breaks `ng test`**: `require.context` is a webpack API the
718
- 22 karma builder no longer provides (`__webpack_require__(...).context is not a function`,
719
- 0 specs run). Delete the `require.context` block; the builder collects the specs from
720
- `tsconfig.spec.json` itself (p034).
721
-
722
- ### The third-party packages the library does *not* own (Phase 2)
723
-
724
- Every hop section above lists the library's own peer dependencies. Your host almost
725
- certainly carries packages the library never sees, and those have their own Angular
726
- peer ceilings — each one can stop a hop dead. Measured on p014 (2026-07-29), which
727
- went 17 → 22 with the library still installed:
728
-
729
- - **`@swimlane/ngx-charts` needs a ladder of its own**: `20 → 22 → 22 → 23 → 24 → 25`
730
- across the five hops. At the Angular 21 hop it peer-caps the CDK, and because
731
- `ng update`'s own `npm` step is **not** `--force`d (even when you passed `--force`
732
- to `ng update`), the install fails there while `package.json` has already been
733
- written correctly. The fix both times was to redo the install by hand, not to
734
- re-run the update. `p043` and `app-tournament` both carry ngx-charts `^20.4.1`
735
- and will hit this; `app-fitnessmirror` and `app-finance-ai` do not.
736
- - **`ngx-extended-pdf-viewer` moved to `.mjs`** at the 21 hop and stopped shipping
737
- `pdf-3.10.560-es5.min.js`. If your `angular.json` names that file under `scripts`
738
- or `assets`, the build fails on a missing file rather than on anything Angular.
739
- - **`karma` and `@angular/build`**: `@angular/build` 22 declares
740
- `peerOptional karma@^6.4.0`. A host pinned to `~6.3.0` gets a wall of
741
- `npm warn ERESOLVE overriding peer dependency` — **warnings, exit code 0**, so it
742
- is not blocking. It becomes a *hard* install failure the moment `@angular/build`
743
- is a direct devDependency rather than a nested one, which is what happened in the
744
- library workspace. `karma@~6.4.4` clears it.
745
- - **`ngx-markdown` 22 lazily imports `marked-katex-extension`**, an optional peer. The
746
- application builder leaves that import alone; the webpack `browser` builder every measured
747
- host still runs resolves it, and the build fails with `Can't resolve
748
- 'marked-katex-extension'` against ngx-markdown's own bundle. Declare
749
- `katex@^0.16` and `marked-katex-extension@^5`: webpack only puts them in a lazy chunk that
750
- loads when KaTeX is switched on. ngx-markdown 22 also wants `marked@^17 || ^18` declared
751
- (app-formmate, 2026-09-22).
752
-
753
- The general shape: **`ng update` needs `--force` at every hop** (the library and
754
- PrimeNG both peer-cap at Angular 17 until the very end), and `--force` covers the
755
- Angular schematics but not the `npm install` they trigger. When that install fails,
756
- check `package.json` before assuming the hop did nothing — it is usually already
757
- correct.
758
-
759
- ### Housekeeping (Phase 3.0)
760
-
761
- #### The dialog's built-in Ok/Cancel buttons are gone
762
-
763
- `smartdialog.component.html` no longer renders an `Ok` and a `Cancel` button.
764
- They were dead: their `(click)` handlers were the no-op expressions
765
- `(click)="(data.okCallback)"` / `(click)="(data.cancelCallback)"` — a bare
766
- property read, not a call — so pressing them did nothing, and the whole block
767
- was behind `*ngIf="!data.customComponent"`, which is never true for a host
768
- dialog (`SmartDialog.ngAfterViewInit` unconditionally instantiates
769
- `data.customComponent`).
770
-
771
- **The `okCallback` / `cancelCallback` model fields stay.** Hosts set and invoke
772
- them from their own `customComponent` dialogs, and that keeps working
773
- unchanged. Only the library-rendered buttons are gone. The `actionCallback` /
774
- `actionLabel` button — the one with a real handler (`onActionClick()`) — is
775
- untouched.
776
-
777
- Action needed: none, unless you relied on the library rendering those two
778
- buttons — in which case they never worked, and you should render them in your
779
- own dialog component.
780
-
781
- ### Full standalone + `provideSmartNgClient()` (Phase 3.2) — **the big one**
782
-
783
- This is the change that rewrites your `app.module.ts`. Everything else in this
784
- guide is small next to it.
785
-
786
- #### Every NgModule is gone
787
-
788
- The library no longer ships a single `@NgModule`. All 58 are deleted —
789
- `SmartNgClientModule`, `SmartViewContextModule`, `SmartSessionModule`,
790
- `SmartGridModule`, `SmarttableModule`, `SmarttreeModule`, `SmartdialogModule`,
791
- `SmartIconModule`, `SmartNavbarModule`, `SmartFilterModule`,
792
- `SmartFilterEditorModule`, `SmartComponentLayoutModule`,
793
- `SmartExpandableSectionModule`, `SmartMapModule`, `SmartDiagramModule`,
794
- `SmartNavigationModule`, `SmartValidationModule`, `SmartGenericPagesModule`,
795
- `ComponentFactoryServiceModule`, `SharedModule`, `SmartTabGroupModule`,
796
- `SmartDocuStoreExplorerModule` and the generated OpenAPI `ApiModule`s.
797
-
798
- Every component, directive and pipe is now **standalone**. Import the ones your
799
- templates use directly, in the component that uses them:
800
-
801
- ```ts
802
- @Component({
803
- selector: 'app-my-page',
804
- templateUrl: './my-page.component.html',
805
- imports: [SmartGridComponent, UiActionToolbarComponent, SmartEmbeddedSlotDirective],
806
- })
807
- export class MyPageComponent extends SmartComponent<MyModel> { … }
808
- ```
809
-
810
- **The compiler tells you what is missing** — one `NG8001: '<smart-grid>' is not
811
- a known element` per unresolved element, per template. Work through them; there
812
- is no guessing involved. In practice ~16 module imports become ~40 component
813
- imports spread across the components that actually use them.
814
-
815
- **The compiler does NOT tell you about the DI changes below.** Nothing fails at
816
- build time if you get those wrong; you find out on the first page load. Smoke
817
- your app.
818
-
819
- #### `provideSmartNgClient(config, ...features)` replaces the module imports
820
-
821
- ```ts
822
- bootstrapApplication(AppComponent, {
823
- providers: [
824
- provideAnimations(),
825
- provideRouter(ROUTES),
826
- provideSmartNgClient(
827
- {
828
- gridMenuIcon: 'more_horiz',
829
- treeMenuIcon: 'more_horiz',
830
- aclEditingViewName: Pages.ACL_MATRIX_PAGE,
831
- invalidSmartlinkPageName: Pages.INVALID_SMARTLINK_PAGE_NAME,
832
- namedValidators: [MY_VALIDATOR_FACTORY],
833
- },
834
- withSmartMap({ engine: MapEngine.LEAFLET }),
835
- withSmartDiagram({ customOptions: [MY_CHART] })
836
- ),
837
- { provide: HTTP_INTERCEPTORS, useClass: MyLoadingInterceptor, multi: true },
838
- ],
839
- });
840
- ```
841
-
842
- The core is always wired and cannot be forgotten: session, view context, the
843
- three BFF interceptors, layout, form, grid, table, tree, dialog, icon, navbar,
844
- filter, filter editor, validation, expandable section, navigation, shared and
845
- the generic pages. That is the point — the session and header interceptors used
846
- to be easy to leave out, and leaving them out fails *silently* (no
847
- `Authorization` header).
848
-
849
- Only two features are opt-in, because they pull heavy third-party code that not
850
- every host renders: `withSmartMap()` (leaflet / Google Maps) and
851
- `withSmartDiagram()` (chart.js). There is **no `withSmartTabGroup()`** —
852
- `smart-tab-group` had no component left, only an empty module, and is deleted.
853
-
854
- `provideSmartNgClient()` calls `provideHttpClient()` itself, with
855
- `withInterceptorsFromDi()` and its own three functional interceptors, and leaves the backend
856
- on the framework default (fetch). Your own class-based `HTTP_INTERCEPTORS` keep working and
857
- still run after the library's three. Pass extra `HttpClient` features in
858
- `config.httpFeatures` rather than making a second `provideHttpClient()` call — see
859
- [HTTP](#http-the-library-no-longer-forces-the-xhr-backend-phase-38), which is also where the
860
- 6.x "do not call it twice" trap went.
861
-
862
- #### String-keyed providers become typed config fields
863
-
864
- Delete these from your providers array and pass them to `provideSmartNgClient`:
865
-
866
- | 6.x provider | 7.0 config field |
867
- |---|---|
868
- | `{ provide: 'gridMenuIcon', useValue: … }` | `gridMenuIcon` |
869
- | `{ provide: 'treeMenuIcon', useValue: … }` | `treeMenuIcon` |
870
- | `{ provide: 'searchPageName' / 'searchComponentName', … }` | `searchPageName` / `searchComponentName` |
871
- | `{ provide: 'genericPageName' / 'genericComponentName', … }` | `genericPageName` / `genericComponentName` |
872
- | `{ provide: 'subjectSelectorPageName' / 'subjectSelectorComponentName', … }` | `subjectSelectorPageName` / `subjectSelectorComponentName` |
873
- | `{ provide: 'validationResultPageName' / 'validationResultComponentName', … }` | `validationResultPageName` / `validationResultComponentName` |
874
- | `{ provide: 'noPermissionPageName' / 'noPermissionComponentName', … }` | `noPermissionPageName` / `noPermissionComponentName` |
875
- | `{ provide: 'invalidSmartlinkPageName' / 'invalidSmartlinkComponentName', … }` | `invalidSmartlinkPageName` / `invalidSmartlinkComponentName` |
876
- | `{ provide: 'aclEditingViewName', useValue: … }` | `aclEditingViewName` |
877
- | `{ provide: DIALOG_DISABLE_CLOSE, useValue: … }` | `dialogDisableClose` |
878
- | `{ provide: MAT_DATE_LOCALE, useValue: 'hu-HU' }` | `dateLocale` — **or keep the provider as it is**, which is what the codemod does; see [Dates §2](#2-leave-your-mat_date_locale-provider-alone) |
879
- | `SmartWidgetSettings.useUtc = true` (a static, not a provider) | **delete it** — the flag is gone with the moment adapter; see [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) |
880
- | `{ provide: NAMED_VALIDATOR, useValue: F, multi: true }` | `namedValidators: [F]` |
881
- | `{ provide: SMART_DEFAULT_VIEW_COMPONENTS, useValue: E, multi: true }` | `defaultViewComponents: [E]` |
882
- | `SmartValidationModule.forRoot([...])` | `namedValidators` — **the element type changes**, see below |
883
- | `SmartDiagramModule.forRoot([...])` | `withSmartDiagram({ customOptions })` |
884
- | `{ provide: MAP_ENGINE, useValue: … }` | `withSmartMap({ engine })` |
885
-
886
- `'pageName'`, `'componentName'` and `'treeId'` stay as string tokens on purpose:
887
- they identify one component instance rather than configuring the library, and
888
- hosts also provide them at component level.
889
-
890
- You can also delete these from your **root** providers array — the core provides them:
891
- `SmartSessionService`, `SmartViewContextService`, `SmartNavigationService`,
892
- `SmartIconService`, `SmartFilterEditorService`, `SmartFormService`,
893
- `SmartCookieService`, `NamedValidatorService`, `ComponentFactoryService`, and the date
894
- adapter wiring (`MAT_DATE_FORMATS`, `DateAdapter`).
895
-
896
- ⚠️ **`namedValidators` takes factories, not providers.** `SmartValidationModule.forRoot()`
897
- took `Provider[]`, so hosts wrote the wrapper by hand:
898
-
899
- ```ts
900
- // 6.x — validator.factories.ts
901
- export const VALIDATOR_PROVIDER_EMPTY_INPUT: Provider = {
902
- provide: NAMED_VALIDATOR, useValue: VALIDATOR_FACTORY_EMPTY_INPUT, multi: true,
903
- };
904
- // app.module.ts
905
- imports: [SmartValidationModule.forRoot([VALIDATOR_PROVIDER_EMPTY_INPUT])]
906
- ```
907
-
908
- `provideSmartNgClient()` adds that wrapper itself, so the config takes the bare
909
- `ValidatorFactory[]`. Export the factories and pass those:
910
-
911
- ```ts
912
- // 7.0
913
- export const VALIDATOR_FACTORY_EMPTY_INPUT: ValidatorFactory = { … };
914
- provideSmartNgClient({ namedValidators: [VALIDATOR_FACTORY_EMPTY_INPUT] })
915
- ```
916
-
917
- The codemod moves the argument into the config and **reports it**, because the entries are
918
- normally named constants declared in another file and only you know what each one wraps. If
919
- you miss the note, `TS2322: Type 'Provider' is not assignable to type 'ValidatorFactory'`
920
- lands on every element.
921
-
922
- ⚠️ **The same line on a `@Component` is not the same thing.**
923
- `providers: [SmartFilterEditorService]` on a page component asks for one instance *per page*,
924
- which is usually deliberate — p014 does it in five components. Leave those alone; the codemod
925
- does.
926
-
927
- #### The module → standalone mapping
928
-
929
- What each `Smart*Module` used to export, reduced to what 7.0 still exports publicly. An
930
- NgModule's `imports:` array accepts standalone components, so this substitution is all a host
931
- that is still NgModule-based needs to keep compiling — the codemod performs it.
932
-
933
- | 6.x module | 7.0 imports |
934
- |---|---|
935
- | `SmartNgClientModule` | the union of the fourteen it re-exported (everything below except the generic pages, the filter editor, the map and the diagram) — trim it to what your templates use |
936
- | `SmartComponentLayoutModule` | `SmartComponentLayoutComponent` |
937
- | `SmartViewContextModule` | `UiActionToolbarComponent`, `UiActionButtonComponent`, `UiActionDialogButtonComponent`, `SmartformComponent`, `SmartfileuploaderComponent`, `SmartFileEditorComponent`, `SmartMultiFileEditorComponent`, `SmartVoiceRecorderComponent`, `SmartEmbeddedSlotDirective`, `HighlightPipe` |
938
- | `SmartGridModule` | `SmartGridComponent` |
939
- | `SmarttreeModule` | `SmartTreeComponent` |
940
- | `SmartdialogModule` | `SmartDialog` |
941
- | `SmartIconModule` | `SmartIconComponent`, `UiBadgeComponent`, `UiBadgeDirective` |
942
- | `SmartNavbarModule` | `SmartNavbarComponent` |
943
- | `SmartFilterModule` | `SmartFilterComponent` |
944
- | `SmartFilterEditorModule` | `SmartFilterEditorContentComponent` |
945
- | `SmartExpandableSectionModule` | `ExpandableSectionComponent` |
946
- | `SmartSessionModule` | `SmartSessionTimerComponent` |
947
- | `SmartGenericPagesModule` | `SearchPageComponent`, `GenericPageComponent`, `SubjectSelectorComponent`, `InvalidSmartlinkComponent` |
948
- | `SmartMapModule` | `SmartMapComponent` + `withSmartMap({ engine })` |
949
- | `SmartDiagramModule` | `SmartDiagramComponent` + `withSmartDiagram({ customOptions })` |
950
- | `SharedModule` | `SmartTooltipDirective`, `SmartDatePipe`, `SmartDateTimePipe`, `SmartTimePipe` |
951
- | `SmartValidationModule` | nothing — `forRoot()`'s validators become `namedValidators` |
952
- | `SmarttableModule` | nothing — `SmarttableComponent` is no longer public; render a `<smart-grid>` |
953
- | `SmartNavigationModule`, `ComponentFactoryServiceModule` | nothing — they only carried services, which the core provides |
954
- | `SmartTabGroupModule` | nothing — deleted; it had no component left |
955
- | `SmartDocuStoreExplorerModule` (`@smartbit4all/document-explorer`) | `SmartDocuStoreExplorerComponent`, `FolderContentComponent`, `PdfViewerDialogPageComponent` |
956
-
957
- Two things this table will not tell you, and the compiler will not either: an import you no
958
- longer need is invisible (unused entries in an NgModule `imports:` array are not reported),
959
- and a name you already bind from elsewhere — a `SmartDatePipe` of your own — must not be
960
- imported twice. The codemod reports both cases rather than guessing.
961
-
962
- #### Your `SmartComponent` subclasses silently become OnPush
963
-
964
- This is an **Angular 22 fact rather than something 7.0.0 introduces**, but it
965
- lands on you at the same moment, so it is worth stating plainly: `SmartComponent`
966
- is an abstract `@Component`, and each of your subclasses carries its own
967
- `@Component` decorator. On Angular 22 a component without an explicit
968
- `changeDetection` gets the new `OnPush` default. p014 has 75 such subclasses,
969
- p043 has 70.
970
-
971
- Let the `ng update` schematic add `changeDetection: ChangeDetectionStrategy.Eager`
972
- to all of them (that is what the library did), verify your app, and move to
973
- OnPush deliberately afterwards. A subclass that mutates its own state outside a
974
- signal and without `markForCheck()` will stop repainting under OnPush.
975
-
976
- #### Removed from the public API
977
-
978
- A major is the only chance to narrow the surface, so these are no longer
979
- exported. All of them are library internals; none is used by p014, p043,
980
- app-formmate or `@smartbit4all/document-explorer`:
981
-
982
- - `SmarttableComponent`, `SmarttableService` — use `<smart-grid>`, which renders
983
- the table internally. **All `SmartTable*` model types stay** (`SmartTableType`,
984
- `SmartTableInterfaceTypeEnum`, `SmartTableHeader`, …). p043 imports
985
- `SmarttableComponent` in `ad-groups.component.ts` but never uses it — delete
986
- that one import line. An import is not the only way in: p009-angular created it
987
- through `ComponentFactoryService` (`recommendations-page`), so grep for the class
988
- name, not only for the import, and rebuild such a page on `<smart-grid>`. p034 used it
989
- three times for a client-side check box table (a `<smarttable>` in a template and two
990
- `ComponentFactoryService` sites) whose rows come from the host, not from a backend grid;
991
- there a small host component rendering the still public `SmartTable` model with
992
- `mat-table` keeps the pages, because `<smart-grid>` needs a backend grid model.
993
- - `GoogleMap`, `LeafletMap`, `AbstractMap` — select an engine with
994
- `withSmartMap({ engine })`.
995
- - `SmartFilterParamComponent`, `SmartFilterParamsComponent`,
996
- `SmartFilterExpressionItemComponent`, `SmartFilterExpressionItemsComponent` —
997
- internals of `SmartFilterEditorContentComponent`, which stays public.
998
- - `HoverMenuComponent`, `ClickMenuComponent` — rendered by the ui-action toolbar.
999
- - `UiActionConfirmDialogComponent`, `UiActionInputDialogComponent` — opened by
1000
- their services, which stay public.
1001
- - `UploadWidgetComponent`, `PhotoCaptureWidgetComponent`,
1002
- `VoiceRecordWidgetComponent` — contents of the ui-action upload dialog.
1003
- - `SmartNgClientService` — deleted; it was an empty stub with no members.
1004
- - The generated `ApiModule`s — they had no usages and their `forRoot()` told you
1005
- to import `HttpClientModule`, which no longer applies.
1006
-
1007
- Two internal string tokens are gone as well, replaced by real `InjectionToken`s
1008
- (`'confirmDialogService'`, `'textFieldDialogService'`, `'fileUploadDialogService'`).
1009
- No host provided them, so no action is needed.
1010
-
1011
- #### Behaviour notes
1012
-
1013
- - `<mat-slide-toggle>` in the form widget no longer carries a `value` binding,
1014
- `<mat-nested-tree-node>` no longer carries an interpolated `matTreeNodeToggle`,
1015
- and the ui-action menus no longer carry `[subActions]`. All four were dead
1016
- bindings that the NgModules' `CUSTOM_ELEMENTS_SCHEMA` had been hiding — they
1017
- wrote a DOM property nobody read. Rendering is unchanged.
1018
- - The same schema on a **host** module hides library selectors that 7.0 no longer
1019
- exports: the template compiles and the element renders nothing.
1020
- `<smart-filter-group-params>` (p009-angular, two admin pages) is one; grep the
1021
- templates for `smart-` selectors that the build does not resolve.
1022
- - The dead `ExpandableGridComponent` is deleted (it was declared in
1023
- `SmartGridModule` but its selector was used nowhere).
1024
-
1025
- ### Widget ↔ SmartComponent: the model routing is inverted (Phase 3.4)
1026
-
1027
- A widget no longer waits to be collected by the screen component it lives in: it finds its
1028
- `SmartComponentApiClient` through DI and registers itself. `SmartComponent` carries the
1029
- channel as an inherited host directive, so **no host component has to be changed for the
1030
- routing itself** — everything below is the *legacy surface* that went with the old
1031
- collection path. The protocol is written up in `WIDGETS.md` next to this file; the decision
1032
- record behind it is ADR-0005 in the `platform-angular2` working copy.
1033
-
1034
- #### What a host must delete (all of it mechanical, all reported by the compiler)
1035
-
1036
- | Delete | Why |
1037
- |---|---|
1038
- | `this.useQueryLists = true;` | The property is gone. Its setter rejected anything but `true`, so every branch behind it was unreachable — this was a no-op that logged a warning. |
1039
- | `[parentSmartComponent]="this"` on `<smart-component-layout>` | The input is gone; the layout injects the client. `strictTemplates` reports leftovers as **NG8002**. |
1040
- | `this.addForm(id, smartForm, formComponent)` | Gone. A `<smartform>` that displays the client's model registers itself. Two call styles existed; where the return value was used, it was the `smartForm` argument — use that (which makes those lines self-assignments, so they just go). **Delete the scaffolding around the call too**: in p014 the 21 calls left behind 9 empty `if (this.formX) { }` guards and 3 `new Promise((r) => setTimeout(r, 500)).then(() => { })` wrappers whose only content had been the registration. The compiler does not complain about either. |
1041
- | `this.widgets.delete(…)` / `this.widgets.set(…)` / `this.formWidgets…` | The maps are gone (they were only written by the legacy path and read by nothing). |
1042
- | the 4th argument of `addGrid(smartGrid, options, useAsDefaultGrid, componentReference)` | `addGrid()` keeps the options and the default-grid role; the services it used to copy into the grid model (`uiActionService`, `uiActionDescriptorService`, `serviceToUse`, `viewContextService`) are the grid's own DI job now. |
1043
- | `override getSmartXxxQL()` (e.g. the two `throw new Error` overrides in a `FilterPageService`) | The eight abstract getters are gone from `SmartComponentApiClient`. |
1044
- | `this.getSmartGridsQL()` and friends | If a component really wants its own widgets, it declares the view query itself: `@ViewChildren(SmartGridComponent) grids!: QueryList<SmartGridComponent>` — same reach as the removed getter had. |
1045
- | `handleDataChangeSubscriptions()` | Gone. A form publishes its own value-change keys when it constructs. |
1046
-
1047
- Removed from the public API in the same step, in case a host imported them directly:
1048
- `SmartComponentLayoutUtility` and its `EmbeddedSlotInfo` interface (the whole file — its
1049
- collectors walked the layout tree for widgets, which nothing does now), and the `[parent]`
1050
- inputs of `<smart-map>` / `<smart-diagram>` (they were bound to `parentSmartComponent` and read
1051
- by nothing; both widgets resolve the client themselves). `initActions()` also runs less often:
1052
- only when the model or its action list changes, no longer after every layout render, grid view
1053
- init and filter model change — a host override that relied on one of those extra calls has to
1054
- key off `dataChanged` instead.
1055
-
1056
- The [codemod](#migrating-a-host-the-codemod) deletes the first two rows and the last one, and
1057
- lists the rest — `addForm()`, the 4th `addGrid()` argument, the getter overrides — with their
1058
- line numbers, because each of those is a small decision rather than a substitution.
1059
-
1060
- #### What stays
1061
-
1062
- - `SmartService` — a view-less client. Its `@ViewChildren` never populated anyway; they are
1063
- simply gone now.
1064
- - `initActions()` — still the hook a host overrides to build its own action list. The base
1065
- implementation only asks for a render; toolbars pull their actions themselves.
1066
- - `setUpDefaultTree()` / `createTreeService()`, `addGrid()`, `submitForms()`,
1067
- `getInvalidFields()`, `uiActionModels` (now a getter over the model's actions).
1068
-
1069
- #### `uiActionModels` is read-only now — assigning to it no longer compiles
1070
-
1071
- It became a getter over the `actionModels` computed, so a host that *replaced* the list in
1072
- place gets `TS2540: Cannot assign to 'uiActionModels' because it is a read-only property`.
1073
- Measured in **p014 and p043**, `document-storage-editor.component.ts`:
1074
-
1075
- ```ts
1076
- // before — no longer compiles
1077
- this.uiActionModels = this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL');
1078
-
1079
- // after — filter into a field of your own and bind it
1080
- protected readonly visibleActions = signal<UiActionModel[]>([]);
1081
- // …
1082
- this.visibleActions.set(this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL'));
1083
- ```
1084
-
1085
- ```html
1086
- <smart-ui-action-toolbar [uiActionModels]="visibleActions()"></smart-ui-action-toolbar>
1087
- ```
1088
-
1089
- A plain field works just as well; what matters is that the **array reference changes**, which
1090
- is what tells the toolbar to re-render. See also the entry-freeze note in the Phase 3.7
1091
- section.
1092
-
1093
- #### Behavioural deltas, accepted
1094
-
1095
- - **Wider reach.** A widget in a host's *own* sub-component now participates, where a view
1096
- query could not see it (measured on p014/p043: 5 toolbars and 3–4 forms per host). A form
1097
- takes part only if it displays the client's component model, which is what the forms the
1098
- queries used to find have in common; a form that renders something else of its own (a
1099
- search box, a field editor) does not.
1100
- - Two widgets sharing one identifier both reload; the old map kept only the last.
1101
- - `getInvalidFields()` collects in subscription order rather than view order — visible only
1102
- in the order of the names in the validation dialog.
1103
- - A toolbar entry is tracked by a synthesized key (action code + ordinal among entries with
1104
- the same code), so a button's DOM node — with its focus and ripple — follows its action
1105
- across reordering. Rendering is otherwise identical.
1106
-
1107
- #### `[smartComponentDetached]`
1108
-
1109
- The one brake on the automatic registration. Put it on an element whose subtree deliberately
1110
- renders something other than the client's model; widgets below it resolve an empty slot and
1111
- neither reload nor take part in submit/validation.
1112
-
1113
- ```html
1114
- <div smartComponentDetached>
1115
- <smart-grid [smartGrid]="ownGrid" [uuid]="ownUuid"></smart-grid>
1116
- </div>
1117
- ```
1118
-
1119
- #### `SmartSubject` is deleted
1120
-
1121
- It was a no-op wrapper over `Subject`: `subscription.add(this.unsubscribe$.subscribe())`
1122
- registers the teardown in the wrong direction, so after `destroy$.next()`, `.complete()` or
1123
- `.unsubscribe()` subscribers still received values. Replace `new SmartSubject(destroy$)`
1124
- with `new Subject()`; where a subscription really must die with a component, use
1125
- `takeUntil(this._destroy$)` (which is what actually stopped delivery all along) or
1126
- `takeUntilDestroyed()`. No host used it.
1127
-
1128
- ### Toolbars resolve their own actions and own the execution context (Phase 3.5)
1129
-
1130
- One rule decides which actions a toolbar shows, and the toolbar — not each list entry —
1131
- says who performs them. `WIDGETS.md`, *Toolbars*, is the short version; the decision record
1132
- behind it is ADR-0006 in the `platform-angular2` working copy.
1133
-
1134
- > A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The
1135
- > list comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the
1136
- > screen component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is
1137
- > not an address, so a toolbar with neither an id nor a binding shows nothing.
1138
-
1139
- `[uiActionModels]="uiActionModels"` therefore keeps meaning "put my page's unaddressed
1140
- actions here" and needs no change.
1141
-
1142
- #### `UseUiAction` is deleted; `UseUiAction2` is now `UiActionExecutor`
1143
-
1144
- There is one executor interface. If a host class implements `UseUiAction` — a `submit` /
1145
- `reSubscribeToChange` subject handshake — replace the two subjects with the two methods:
1146
-
1147
- ```ts
1148
- // before
1149
- export class DelegateInboxDialogService implements UseUiAction {
1150
- submit: Subject<void> = new Subject();
1151
- reSubscribeToChange: Subject<void> = new Subject();
1152
- …
1153
- }
1154
-
1155
- // after
1156
- export class DelegateInboxDialogService implements UiActionExecutor {
1157
- submitForm(validate: boolean): void {
1158
- this.submit.next(); // keep the subject if a component subscribes to it
1159
- }
1160
- getInvalidFields(): SmartFormInvalidFields {
1161
- return { invalidFieldKeys: [], invalidFieldNames: [] };
1162
- }
1163
- …
1164
- }
1165
- ```
1166
-
1167
- `Subject.next()` is synchronous, so a component that subscribed to `submit` has already run
1168
- by the time `submitForm()` returns — which is exactly what the old handshake tried to
1169
- express. Keep `reSubscribeToChange` only if something subscribes to it; the library no longer
1170
- fires it.
1171
-
1172
- This also **fixes a silent hang**: the old branch `await`ed `submit.toPromise()`, which only
1173
- settles when somebody `complete()`s the subject. Three of the five implementors never did, so
1174
- a `submit`/`model` action on those services waited forever.
1175
-
1176
- #### `UiActionService.execute()` lost its first argument
1177
-
1178
- It was always `uiActionModel.uiAction`:
1179
-
1180
- ```ts
1181
- - this.uiActionService.execute(uiActionModel.uiAction, uiActionModel);
1182
- + this.uiActionService.execute(uiActionModel);
1183
- ```
1184
-
1185
- #### The execution context moved from the entry to the toolbar
1186
-
1187
- | what | bind on the toolbar | entry field (still honoured) |
1188
- |---|---|---|
1189
- | who performs the action | `[executor]` | `serviceToUse` — **deprecated** |
1190
- | which widget it runs against | `[widgetId]` | `widgetId` — **deprecated**, wins if set |
1191
- | which row inside it | `[nodeId]` | `nodeId` — **deprecated**, wins if set |
1192
- | extra params for every action | `[actionParams]` | (clone the `UiAction` — no longer necessary) |
1193
-
1194
- `[executor]` defaults to the screen component the toolbar sits under, which is what almost
1195
- every entry named. Bind it only when the actions belong to another API — a tree service, a
1196
- filter editor, a dialog service of your own:
1197
-
1198
- ```html
1199
- <smart-ui-action-toolbar [uiActionModels]="actions" [executor]="myService"></smart-ui-action-toolbar>
1200
- ```
1201
-
1202
- `[actionParams]` replaces cloning a `UiAction` per row just to inject a model:
1203
-
1204
- ```html
1205
- <smart-ui-action-toolbar
1206
- [uiActionModels]="rowActions"
1207
- [widgetId]="gridId"
1208
- [nodeId]="row.id"
1209
- [actionParams]="{ model: row }"
1210
- ></smart-ui-action-toolbar>
1211
- ```
1212
-
1213
- #### Removed from the public API
1214
-
1215
- | Removed | Replacement |
1216
- |---|---|
1217
- | `UseUiAction` | `UiActionExecutor` (see above) |
1218
- | the third `SmartTable` constructor parameter and `getServiceToUse()` | nothing — the executor no longer travels through the table (0 host call sites) |
1219
- | `SmartformComponent.getToolbars()` | nothing collects toolbars any more; each one resolves its own actions |
1220
- | `ISmartFilterEditorService.get/setComplexToolbars()` and the `submit` / `reSubscribeToChange` members | as above |
1221
- | `FileEditorToolbarComponent` | a plain `<smart-ui-action-toolbar>` with `[actionParams]` |
1222
- | `SmartGridComponent.setupToolbar()` / `.toolbar` / `.headerToolbar` | the id is a binding; there is nothing to reach into |
1223
-
1224
- #### Behavioural deltas, accepted
1225
-
1226
- - A widget renders a toolbar exactly when its model carries a `toolbarId`, with no type list.
1227
- A `toolbarId` on a type whose template has no place for a toolbar (`RECORDING_UPLOADER`)
1228
- now logs a warning instead of being silently dropped.
1229
- - Only the actions a toolbar actually shows are scheduled. Since 3.4 a toolbar armed every
1230
- scheduled action of the view, so one action could fire from several toolbars at once.
1231
- - A grid row's actions are no longer cloned per row; the row model travels as
1232
- `[actionParams]`. The multi-file editor's per-file toolbars appear when their own list
1233
- resolves rather than when a hidden sibling's did, and the tree's `doAction` clones instead
1234
- of mutating the caller's `UiAction`.
1235
-
1236
- ### The form widget's rendering decisions (Phase 3.6)
1237
-
1238
- Nothing here needs a code change in a host — there is no API to adapt to. What
1239
- changes is what the widget puts on the page, so read this before you compare
1240
- screenshots.
1241
-
1242
- #### Every widget carried a spare empty wrapper; it is gone
1243
-
1244
- The `COMPONENT` branch's wrapper had no type guard, so
1245
- `<div class="widgetContainer"><div class="widgetContent"><ng-template
1246
- #customComponent>` rendered after **every** widget of every type. Measured on the
1247
- playground: 14 widgets, 28 `.widgetContainer` — one spare, empty, per widget.
1248
-
1249
- The library's own CSS puts no box properties on `.widgetContainer`, so most pages
1250
- will look identical. **Yours may not**, if you style that class with anything that
1251
- occupies space. Both in-house hosts do, on one page:
1252
-
1253
- ```css
1254
- /* p014 + p043, dossier-publication.component.css */
1255
- :host ::ng-deep .widgetContainer { margin-bottom: 0.5rem !important; }
1256
- ```
1257
-
1258
- There, every widget loses half a rem of trailing space. Grep your stylesheets for
1259
- `.widgetContainer` and check the pages the matching selectors reach. Rules that
1260
- need content still match what they always matched — an empty wrapper never
1261
- satisfied `.label.widgetContainer` or `.widgetContainer:has(…)`.
1262
-
1263
- #### `MATRIX` and `YOUTUBE_PLAYER` now obey `isVisible` and get their `cssClass`
1264
-
1265
- Those two branches sat outside the `@if (isVisible)` guard and outside the
1266
- `<div [ngClass]="cssClass" class="container">` every other type lives in, so
1267
- `isVisible: false` did not hide them and `cssClass` never reached them. Both are
1268
- inside now. If a backend view relies on a matrix rendering while marked invisible,
1269
- it will stop rendering.
1270
-
1271
- #### A container keeps its invisible children
1272
-
1273
- A `CONTAINER` widget's `ngOnInit` used to overwrite `widgetInstance.valueList`
1274
- with the visible subset, destroying the invisible children in the model the
1275
- backend sent. The visible subset is derived per check now, so the model stays
1276
- whole and a later `applyConstraints()` that flips a child's `isVisible` brings it
1277
- back — which it could not before. The DOM is unchanged: the same children render.
1278
-
1279
- Side effect worth knowing: the model walks that recurse into a container's
1280
- `valueList` (`applyComponentConstraints`, `copyValueFromFormToWidgetBeans`,
1281
- `translateWidgets`) now see the invisible children too. Their form controls always
1282
- existed — `createFormControls` runs over the whole model before any widget renders
1283
- — so this only makes the widget beans agree with the controls.
1284
-
1285
- #### A YouTube shorts link resolves
1286
-
1287
- `parseYoutubeUrl` threw away its own `replace('/shorts/', '/watch?v=')`, so a
1288
- shorts link produced no player. It plays now.
1289
-
1290
- #### Removed from the public API
1291
-
1292
- | Removed | Replacement |
1293
- |---|---|
1294
- | `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.3 splits it into per-type components |
1295
- | `SmartWidgetSettings` (its only member was `static useUtc`) | nothing — **delete the assignment** |
1296
-
1297
- 3.6 moved the `useUtc` static into `SmartNgClientConfig.useUtcDates`; **3.8 then removed the
1298
- field altogether**, together with the moment adapter it fed. If you set
1299
- `SmartWidgetSettings.useUtc = true` before bootstrap, delete that line and read
1300
- [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) — the timezone contract is fixed now,
1301
- and the flag defaulted to `false` everywhere anyway.
1302
-
1303
- #### What deliberately did not change
1304
-
1305
- A required field is marked in both label positions, by two different mechanisms:
1306
- a shown label (`showLabel: true`) is the widget's own `<h4>` and carries a literal
1307
- ` *`; a `mat-label` sits inside a `mat-form-field`, which renders
1308
- `.mat-mdc-form-field-required-marker` from the control's own validators. That is
1309
- why `getWidgetLabel` ties its asterisk to `showLabel` — dropping the condition
1310
- would mark the floating label twice.
1311
-
1312
- ### Change detection: frozen action entries, no zone.js (Phase 3.7)
1313
-
1314
- 7.0 runs without zone.js. Every component of the library is `OnPush` — nothing is checked
1315
- just because something, somewhere, ticked — and the library says when its own state changed.
1316
- Two consequences reach a host.
1317
-
1318
- #### A `UiActionModel` is frozen: build an entry, never edit one
1319
-
1320
- Every field is `readonly`, and `[uiActionModels]` takes `readonly UiActionModel[]`.
1321
-
1322
- ```ts
1323
- // before — compiles on 6.x, and on 7.0 changes nothing on screen
1324
- entry.cssClass = 'active-nav-action';
1325
-
1326
- // after
1327
- this.actions = this.actions.map((a) =>
1328
- a.uiAction.code === code ? { ...a, cssClass: 'active-nav-action' } : a
1329
- );
1330
- ```
1331
-
1332
- Measured across the 12 hosts in scope (2026-07-28): **72 sites in 7 hosts**, all of them
1333
- `cssClass` — p009-angular alone has 38, p014 and p043 12 each. The codemod prints every one
1334
- of them with its line number; it cannot rewrite them, because only you know which array the
1335
- entry belongs to.
1336
-
1337
- The compile error and the fix are the same thing. Writing into an entry that a toolbar is
1338
- already rendering never reached the screen under zone.js either — it worked only because
1339
- some other event happened to tick the application. Rebuilding the array is what actually
1340
- re-renders.
1341
-
1342
- **The gap this does not close.** `this.actions[0] = { ...this.actions[0], cssClass: 'x' }`
1343
- compiles: `actions` is your field, and the freeze only covers the entries. It keeps the same
1344
- array reference, so the toolbar does not re-render. Reassign the array — or, if you hold it
1345
- in a `signal`, `update()` it.
1346
-
1347
- **A component's own toolbar entries come from its `actionModels` signal.** The 6.x pattern
1348
- `this.uiActionModels.forEach((a) => (a.cssClass = 'eji-button'))` in `initActions` becomes an
1349
- override of that signal, which bound, pulled and layout-embedded toolbars all read:
1350
-
1351
- ```ts
1352
- // the helper runs while this.actionModels is still the base class's signal
1353
- function withCssClass(component: SmartComponent<any>, cssClass: string) {
1354
- const source = component.actionModels;
1355
- return computed(() => source().map((a) => ({ ...a, cssClass })));
1356
- }
1357
-
1358
- override readonly actionModels = withCssClass(this, 'eji-button');
1359
- ```
1360
-
1361
- Reading `this.actionModels` directly in its own initializer is `TS2729`; passing `this` to a
1362
- function is what captures the base signal (p009-angular, `src/app/utils/action-css-class.ts`).
1363
- A host field of its own named `actionModels` now collides with the base signal
1364
- (`TS2416` + `TS4114`): rename it.
1365
-
1366
- When the mapping reads the component's data (mark the entry of `data.currentPage`), read
1367
- `component.modelSignal()` inside the `computed` as well: `data` is a plain field, so without it
1368
- the class does not follow a data change that leaves the action list alone (p034,
1369
- `src/app/utils/action-models.ts`).
1370
-
1371
- The descriptor a toolbar resolves is no longer written back into the entry either. Nothing
1372
- outside the toolbar read it; if you did, resolve it yourself through
1373
- `UiActionDescriptorService.getActionDescriptor(uiAction)` — which is
1374
- [synchronous in 7.0](#getactiondescriptor-is-synchronous-phase-39).
1375
-
1376
- #### Client-side translation is gone
1377
-
1378
- `SmartTranslateService`, `viewContext.translateService` and the `translateServiceChanged`
1379
- subject are removed, together with `originalLabel` / `originalPlaceholder` on the widget
1380
- interfaces, and the `translateService` field of `SmartForm`, `SmartGrid` and `SmartFilter`. The
1381
- backend localizes server-side, and in p014/p043 the three references were unused imports.
1382
-
1383
- **A host that sends translation keys still needs translating.** p009-angular keeps ngx-translate
1384
- keys in its client-built forms, its client action descriptors and its titles; deleting the
1385
- calls would put raw keys on screen. It now translates in the host: the widget labels,
1386
- placeholders, validator messages and item labels when a form is built, and the descriptor map
1387
- on every `onLangChange`, with `bootstrap.start()` waiting for the language to load. **Grid
1388
- column headers and filter labels have no host-side replacement**: they show what the backend
1389
- sends, so localize them on the backend. A page that read `this.translate` without assigning it
1390
- got `undefined` already on 6.x (the base class never set it), so check what a title showed
1391
- before calling a now-translated one a regression.
1392
-
1393
- What the frontend authors itself is now configuration:
1394
-
1395
- ```ts
1396
- provideSmartNgClient({
1397
- errorDialog: { title: 'Hiba', message: 'Váratlan hiba történt.', buttonLabel: 'Rendben' },
1398
- });
1399
- ```
1400
-
1401
- `SmartViewContextService.getSmartViewContextApiErrorByCode()` is no longer `async`.
1402
-
1403
- **`SmartComponent` no longer has a `translate` member**, and a host that injected
1404
- ngx-translate's `TranslateService` under that name as `override translate: TranslateService`
1405
- now gets `TS4113` on every such constructor (app-formmate had 22). Do not just delete
1406
- `override`: it was also what made the parameter a property, so without an access modifier
1407
- `this.translate` disappears. Write `public translate: TranslateService` — and keep `override`
1408
- on subclasses of your own pages, which now override *your* base class instead of the library's.
1409
-
1410
- #### If your host still bootstraps with zone.js
1411
-
1412
- Nothing forces you to drop it — the library works either way, because it no longer depends
1413
- on an application-wide tick. The playground runs `provideZonelessChangeDetection()` with no
1414
- `zone.js` polyfill, and that is the configuration 7.0 is tested in.
1415
-
1416
- If you do go zoneless, the same rule applies to your own components: state written from a
1417
- subscription, a promise or a timer needs a signal or a `markForCheck()`; state written from a
1418
- template event or an input does not.
1419
-
1420
- ### Dates: moment → date-fns, and the timezone contract (Phase 3.8)
1421
-
1422
- **This section needs action at install time.** The reasoning is ADR-0008 in the
1423
- `platform-angular2` working copy; the contract itself is stated in §4 below.
1424
-
1425
- #### 1. Swap the peer dependency
1426
-
1427
- `@angular/material-moment-adapter` is out of the library's `peerDependencies`;
1428
- `@angular/material-date-fns-adapter` and `date-fns` are in. `date-fns` is listed explicitly
1429
- rather than left transitive, because the library imports `date-fns/locale` itself.
1430
-
1431
- ```bash
1432
- npm uninstall @angular/material-moment-adapter moment
1433
- npm install @angular/material-date-fns-adapter@^22.0.0 date-fns@^4
1434
- ```
1435
-
1436
- If your own code imports moment for something unrelated, keep it — nothing here forces its
1437
- removal, only the adapter's.
1438
-
1439
- **But check *how* it imports moment.** `import * as moment from 'moment'` stops being callable
1440
- under `moduleResolution: "bundler"`, which the Angular 20 hop turns on: moment is an
1441
- `export =` module, and a namespace import of one is not a callable value. The symptom is
1442
- **`TS2349: This expression is not callable`** on every `moment(...)` call, and it arrives at the
1443
- Angular 20 hop rather than at this step. Two ways out:
1444
-
1445
- - `esModuleInterop: true` plus `import moment from 'moment'` — correct, but it changes how every
1446
- CommonJS import in the app resolves, which is a wide blast radius for one file.
1447
- - Replace the calls. Worth checking first how much moment is actually doing: in p014 the three
1448
- surviving calls were `moment(new Date()).toDate()` (i.e. `new Date()`) and two moments compared
1449
- with `<=` (`Date` compares through `valueOf()` identically). All three became plain `Date`s, and
1450
- **nothing under `src/` imported moment any more** — so the `npm uninstall … moment` above was
1451
- right for that host after all, just for a reason nobody predicted.
1452
-
1453
- #### 2. Leave your `MAT_DATE_LOCALE` provider alone
1454
-
1455
- If you have this line — p014 and p043 both do — **keep it as it is**:
1456
-
1457
- ```ts
1458
- { provide: MAT_DATE_LOCALE, useValue: 'hu-HU' },
1459
- ```
1460
-
1461
- The strict reading of the date-fns adapter would require a `Locale` **object** here, and a bare
1462
- string would throw inside date-fns the first time a user opened a date field. The library's
1463
- `SmartDateFnsAdapter` accepts both: it resolves `'hu'` / `'hu-HU'` / `'en'` / `'en-US'`
1464
- (case-insensitively) to a date-fns locale, passes a real `Locale` object through untouched, and
1465
- falls back to Hungarian with a `console.warn` for anything else.
1466
-
1467
- So: **hosts on Hungarian or English need no change.** For any other language, provide a real
1468
- date-fns locale:
1469
-
1470
- ```ts
1471
- import { de } from 'date-fns/locale';
1472
- …
1473
- { provide: MAT_DATE_LOCALE, useValue: de },
1474
- ```
1475
-
1476
- `SmartNgClientConfig.dateLocale` is unchanged — still a `string`, still defaulting to `'hu-HU'`.
1477
-
1478
- #### 3. If you override `MAT_DATE_FORMATS`, your format strings will throw — and you probably want to delete the override
1479
-
1480
- Measured across 12 hosts on 2026-07-28: three of them (**p014**, **p043**, **p009-angular**)
1481
- provide their own formats next to the locale, and two more (`app-vlab`, `app-tournament`) have the
1482
- same line commented out:
1483
-
1484
- ```ts
1485
- { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
1486
- ```
1487
-
1488
- with
1489
-
1490
- ```ts
1491
- export const MY_FORMATS = {
1492
- parse: { dateInput: 'YYYY.MM.DD' },
1493
- display: { dateInput: 'YYYY.MM.DD', monthYearLabel: 'YYYY',
1494
- dateA11yLabel: 'LL', monthYearA11yLabel: 'YYYY' },
1495
- };
1496
- ```
1497
-
1498
- Those are **moment tokens**. date-fns treats `YYYY`, `DD`, `YY` and `D` as *protected* and throws a
1499
- `RangeError` on them, and its `LL` means something else entirely (stand-alone month, not a
1500
- localized long date). So this override **crashes at runtime, on the first screen with a date
1501
- field** — the same failure shape as the string locale above, and just as invisible to the compiler.
1502
-
1503
- **The fix is almost certainly deletion.** That `MY_FORMATS` exists to render `YYYY.MM.DD`, which is
1504
- exactly what the library's own `SMART_DATE_FORMATS` now produces (`2026.07.28.`). Drop the
1505
- provider and the constant and you get the format you wanted, plus the more forgiving parsing.
1506
-
1507
- If you genuinely need a different format, translate the tokens rather than copying them:
1508
-
1509
- | moment | date-fns |
1510
- |---|---|
1511
- | `YYYY` | `yyyy` |
1512
- | `DD` | `dd` |
1513
- | `LL` | `PP` (or spell it out: `yyyy. MMMM d.`) |
1514
- | `MMM YYYY` | `yyyy. LLL` |
1515
-
1516
- Note that the library's `parse.dateInput` is an **array** — `DateFnsAdapter._parse` tries each entry
1517
- in turn (and ISO-8601 first), so you can list several accepted spellings instead of one.
1518
-
1519
- #### 4. `SmartNgClientConfig.useUtcDates` is removed
1520
-
1521
- Delete it if you set it. No known host does. It existed for a client-configurable timezone that
1522
- was abandoned as a requirement; the contract is now fixed and explicit:
1523
-
1524
- > **The browser's zone on screen. Zulu (`…Z`) on the wire. The server converts.**
1525
-
1526
- This is unchanged behaviour in practice — `useUtcDates` defaulted to `false`, and the moment
1527
- adapter with `useUtc: false` behaves exactly like a plain `Date`.
1528
-
1529
- #### 5. What a date widget's value now *is*
1530
-
1531
- Form control values from the date pickers are plain **`Date`** objects, where they were
1532
- `Moment` objects. The values reach your backend through a `UiActionRequest` param, so the wire
1533
- format is `JSON.stringify` of that value — and `Date.toJSON()` emits the same ISO-8601 UTC string
1534
- `Moment.toJSON()` did. **Nothing changes on the wire.** Only host code that read a value out of a
1535
- form control and called a moment method on it (`.format()`, `.add()`, `.startOf()`) needs
1536
- rewriting; the platform APIs never exposed a `Moment` in a signature.
1537
-
1538
- #### 6. The one visible difference
1539
-
1540
- Date inputs render **`2026.07.28.`** where 6.x rendered `2026.7.28.` — the documented `YYYY.MM.DD`
1541
- convention. The old rendering was not a decision: `MAT_MOMENT_DATE_FORMATS` used moment's `'l'`,
1542
- which strips the zero padding off the Hungarian `L`. Typed input is **more** forgiving than 6.x,
1543
- not less: `2026.07.28.`, `2026.07.28`, `2026.7.28.`, `2026.7.28` and ISO-8601 are all accepted.
1544
-
1545
- The month picker is unchanged (`07/2026`), and grid/table date columns were never affected —
1546
- `SmartDatePipe` and friends extend Angular's own `DatePipe` and never used moment.
1547
-
1548
- **Verified on a running host** (p014, 2026-07-29, live backend, `Europe/Budapest`). Picking
1549
- 2026-07-15 in a `Kezdődátum` field gave, end to end:
1550
-
1551
- | | |
1552
- |---|---|
1553
- | input | `2026.07.15.` |
1554
- | widget value | `instanceof Date` — not a moment, not a luxon `DateTime` |
1555
- | `JSON.stringify` of that value | `"2026-07-14T22:00:00.000Z"` |
1556
- | `getHours()` | `0` |
1557
-
1558
- That third row is the whole contract in one line: the value's `toJSON()` **is** the wire format
1559
- (see §4), and it is zulu, while the screen and `getHours()` stay local — 15 July 00:00 CEST is
1560
- 14 July 22:00 UTC. A grid column on the same host rendered `2026.04.20 0:03`, confirming the
1561
- untouched pipe path.
1562
-
1563
- ### HTTP: the library no longer forces the XHR backend (Phase 3.8)
1564
-
1565
- `provideSmartNgClient()` used to pass `withXhr()` to its internal `provideHttpClient()` call,
1566
- overriding the Angular 22 default on your behalf. It no longer does — the application runs on the
1567
- framework default **fetch** backend.
1568
-
1569
- For almost every host this is invisible. The one behavioural difference that matters: **the fetch
1570
- backend does not support upload progress.** A request with `reportProgress: true` and a body
1571
- throws `NG2824` instead of emitting `HttpUploadProgress` events.
1572
-
1573
- Measured 2026-07-28 across 12 hosts: **`reportProgress: true` and `observe: 'events'` appear
1574
- nowhere** outside the generated API services' unused parameters. So this is a theoretical risk for
1575
- the current host set, not a live one. If *your* code does ask for upload progress, pass the
1576
- feature back in:
1577
-
1578
- ```ts
1579
- import { withXhr } from '@angular/common/http';
1580
-
1581
- provideSmartNgClient({ httpFeatures: [withXhr()] })
1582
- ```
1583
-
1584
- The upside is that the "**do not call `provideHttpClient()` twice**" trap this guide warned about
1585
- is gone rather than merely documented: a second call now re-provides the same backend the library
1586
- already uses. The library's interceptors were never at risk from it — they are `multi` providers.
1587
-
1588
- ### `getActionDescriptor()` is synchronous (Phase 3.9)
1589
-
1590
- `UiActionDescriptorService.getActionDescriptor(uiAction)` returns a `UiActionDescriptor`.
1591
- It used to return a `Promise<UiActionDescriptor>`, and it was `async` for exactly one
1592
- reason: the client-side translation layer that [Phase 3.7](#client-side-translation-is-gone)
1593
- retired. Nothing it reads has been asynchronous since, so every caller was awaiting a value
1594
- that was already there.
1595
-
1596
- **If you only `await` the call, you are already fine.** `await` on a non-promise is legal
1597
- TypeScript, so this compiles and behaves identically before and after. The codemod drops the
1598
- `await` anyway, so the code stops claiming an asynchrony that is not there:
1599
-
1600
- ```ts
1601
- const descriptor = await this.uiActionDescriptor.getActionDescriptor(uiAction); // 6.x
1602
- const descriptor = this.uiActionDescriptor.getActionDescriptor(uiAction); // 7.0
1603
- ```
1604
-
1605
- Two shapes genuinely break. Measured 2026-07-28 across the 12 hosts in scope: **5 hosts,
1606
- 7 sites** — and none of them in a host's own `getActionDescriptor**s**()` map helper, which
1607
- is an unrelated API and is left alone.
1608
-
1609
- **1. A subclass that overrides it** — `TS2416`, because `Promise<UiActionDescriptor>` is not
1610
- assignable to `UiActionDescriptor`. Found in three hosts, as a byte-identical
1611
- `MdmUiActionDescriptorService`. **The codemod rewrites this one.**
1612
-
1613
- ```ts
1614
- // 6.x
1615
- override async getActionDescriptor(uiAction: UiAction): Promise<UiActionDescriptor> {
1616
- let d: UiActionDescriptor = await super.getActionDescriptor(basicUiAction);
1617
- …
1618
- }
1619
-
1620
- // 7.0
1621
- override getActionDescriptor(uiAction: UiAction): UiActionDescriptor {
1622
- let d: UiActionDescriptor = super.getActionDescriptor(basicUiAction);
1623
- …
1624
- }
1625
- ```
1626
-
1627
- This is also the service you hand to a toolbar through `[uiActionDescriptorService]`, so a
1628
- host with a custom one hits this at compile time rather than on screen.
1629
-
1630
- **2. `.then(…)` on the result** — `TS2339`, because a descriptor has no `.then`. Found in
1631
- two hosts, four sites. **The codemod reports this one and does not rewrite it**: turning a
1632
- callback into straight-line code means moving its body, which is a judgment call. The fix is
1633
- mechanical anyway — assign, then inline:
1634
-
1635
- ```ts
1636
- // 6.x
1637
- constructForm() {
1638
- this.actionDescriptorService.getActionDescriptor(this.uiAction).then((d) => {
1639
- this.buttonTitle = d.title;
1640
- this.buttonColor = d.color;
1641
- });
1642
- …
1643
- }
1644
-
1645
- // 7.0
1646
- constructForm() {
1647
- const d = this.actionDescriptorService.getActionDescriptor(this.uiAction);
1648
- this.buttonTitle = d.title;
1649
- this.buttonColor = d.color;
1650
- …
1651
- }
1652
- ```
1653
-
1654
- If the callback was the reason a field lived in a `signal` — because the value arrived after
1655
- the first render — it no longer needs to. The library made that same simplification in its
1656
- own three action dialogs and its snack bar.
1657
-
1658
- **What went synchronous with it, inside the library:** the toolbar's descriptor resolution,
1659
- `SmartGridComponent`'s header construction, and `SmartTreeGenericService.cacheActionDesciptors()`
1660
- and **`syncTree()`**. `syncTree()` now returns `void`, which is what
1661
- `SmartTreeServiceInterface` always declared; a host that calls it and discards the result —
1662
- the only shape measured — needs no change, and a host that `await`s it still compiles.
1663
-
1664
- #### The same cleanup, elsewhere: `SmartdialogService.closeDialog()` returns `void`
1665
-
1666
- A sweep for the same shape found eight more library functions declared `async` with nothing
1667
- asynchronous in them. Only one of them is on a surface a host touches:
1668
- **`SmartdialogService.closeDialog(stopPropagate?)` returns `void`** instead of
1669
- `Promise<void>`.
1670
-
1671
- **Nothing to do.** Both measured hosts that extend `SmartdialogService` override this method
1672
- as `override async closeDialog(): Promise<void>`, and that **still compiles**: TypeScript
1673
- accepts any return type where the base declares `void`. `await this.closeDialog()` keeps
1674
- compiling too. This is the difference from `getActionDescriptor` above — that one returns a
1675
- *value*, which is why the same override shape fails there with `TS2416` and not here.
1676
-
1677
- You may of course drop the `async` from your own override; nothing requires it.
1678
-
1679
- ### Upload actions now wait for the server, and report their failures (Phase 3.9)
1680
-
1681
- **This one changes behaviour, on purpose.** `SmartViewContextService.performUploadAction()`
1682
- and `performUploadMultipleAction()` used to start the upload and resolve immediately,
1683
- without waiting for it and without passing on its failure. Every other action in the family
1684
- (`performAction`, `performWidgetAction`, `performWidgetMainAction`, `dataChanged`) always
1685
- waited; the two upload ones were an omission.
1686
-
1687
- Two things follow, and both are visible in a running app:
1688
-
1689
- 1. **An upload action now blocks until the server has answered.** Before, the action was
1690
- "done" the moment the request went out — so `UiActionService` showed its success snackbar
1691
- over an upload that was still in flight, and anything the action was sequenced before ran
1692
- against a view the server had not updated yet. Now the snackbar appears when the upload
1693
- really has succeeded. On a large file that is a longer wait than 6.x showed you.
1694
-
1695
- 2. **A failed upload now reaches your error handling.** The rejection used to be dropped —
1696
- it became an unhandled promise rejection, `UiActionService`'s `catch` never ran, and the
1697
- user was told the upload had worked. It now propagates, which means
1698
- `setActionErrorHandler()` (or the default error dialog) will fire on upload failures it
1699
- never fired on before. **If your host reports "new" upload errors after upgrading, they
1700
- are not new** — they were happening silently.
1701
-
1702
- Multi-file uploads also go out **one batch at a time** now. The batches were already being
1703
- computed from the descriptor's `maxSize` and `maxBatchSize`, but they were then all sent at
1704
- once, which put the whole set on the server simultaneously and defeated the split. If your
1705
- backend measured upload concurrency, this is why it drops.
1706
-
1707
- Nothing to change in host code.
1708
-
1709
- ### New in 7.0: a grid row can render a backend layout (#29717)
1710
-
1711
- A card-mode grid row that carries `layoutDescriptor.componentLayouts['GRID_ROW_LAYOUT']`
1712
- renders that layout instead of the card component the host registered under
1713
- `'<GRID_ID>Card'`. Both branches stay: a row without the descriptor behaves exactly as
1714
- before, and this needs no host change.
1715
-
1716
- Two consequences worth knowing if you style or extend it:
1717
-
1718
- - The rendered layout's root element gets the `gridCardLayout` class, applied by the card to
1719
- its own child rather than pushed into the `style.classesToAdd` of the object the backend
1720
- sent. Nothing mutates the server model.
1721
- - **The row's actions reach the layout's toolbars by a pull, not a walk.** The card provides
1722
- a `SmartActionHost` for its row subtree and writes the row's action list into it; a toolbar
1723
- inside the layout resolves that host before falling back to the page client. If you build
1724
- a component that owns such a subtree, do the same — `WIDGETS.md`, *Toolbars*.
1725
-
1726
- This shipped in 7.0.0 rather than as a 6.0.3x patch because it is built on the inverted
1727
- widget contract that 7.0 introduces.
1728
-
1729
- ### Table detail rows: rendered on demand, `example-*` classes renamed (7.0.10)
1730
-
1731
- The Material table used to define its expandable detail row the way the Angular Material
1732
- example does: a second `matRowDef` rendered under **every** data row, kept at zero height
1733
- with `tr.example-detail-row { height: 0 }` and an `@angular/animations` trigger. Any host
1734
- rule that gave table cells vertical padding, or rows a height, made those empty rows visible
1735
- — the p043 report — and every table carried twice its rows in the DOM.
1736
-
1737
- Now the detail row exists only in an expandable table (`SmartTable.expandable`), and only
1738
- under the row that is expanded. There is nothing a host stylesheet can inflate. Expanding
1739
- and collapsing animate through the native `animate.enter` / `animate.leave` of Angular 20.2+
1740
- (a `grid-template-rows: 0fr ↔ 1fr` keyframe, as `mat-expansion-panel` does it), so the table
1741
- no longer uses `@angular/animations` at all.
1742
-
1743
- The classes lost their `example-` prefix, taken over from the Material sample:
1744
-
1745
- | 6.x | 7.0.10 |
1746
- |---|---|
1747
- | `example-element-row` | `smart-table-row` |
1748
- | `example-expanded-row` | `smart-table-row-expanded` |
1749
- | `example-detail-row` | `smart-table-detail-row` |
1750
- | `example-element-detail` | `smart-table-detail` |
1751
-
1752
- `.example-element-diagram`, `-symbol`, `-description` and `-description-attribution` — the
1753
- sample's own styles, referenced by nothing — are gone.
1754
-
1755
- What to do in a host:
1756
-
1757
- - Rename the classes in any stylesheet that targets them. The usual suspects are a hover
1758
- rule on `tr.example-element-row:not(.example-expanded-row)` and a
1759
- `:host ::ng-deep tr.example-detail-row { display: table-row !important }` in a page that
1760
- expands rows; the latter can simply be deleted, the row is rendered when it is needed.
1761
- - A data row now carries its own bottom border. Before, the always-present detail row drew
1762
- it, and the data row's border was turned off to avoid a double line. Nothing changes on
1763
- screen unless you drew your own row separators to compensate.
1764
-
1765
- ### Writing your own widget
1766
-
1767
- `WIDGETS.md`, next to this file, is the protocol: how a widget finds its screen component
1768
- through DI and registers itself, when to opt out with `[smartComponentDetached]`, what has to
1769
- be a signal now that there is no zone, how a toolbar resolves its actions, and the three
1770
- traps that have actually bitten (imperatively-fed inputs cannot be signal inputs; `@for`
1771
- tracking over backend objects; clear-then-apply styling).
1772
-
1773
- In 6.x this was not possible at all — `SmartComponentApiClient` collected its children with
1774
- eight `@ViewChildren` over eight concrete widget classes.
1775
-
1776
- ### Imperatively created components are given their inputs properly
1777
-
1778
- `ComponentFactoryService` creates the components the library instantiates by hand — grid
1779
- cards, expanded rows, table cell components, the expandable section's content, the form's
1780
- `COMPONENT` widget, the filter editor's field editors — and most of those classes come from a
1781
- host. It used to write their inputs by assigning the field. Two things follow, and both are
1782
- fixed:
1783
-
1784
- - **A declared input is now written with `ref.setInput()`**, i.e. the same way a template
1785
- binding writes it. So a **signal input** works (assignment used to overwrite the input
1786
- function with the value, and the component's next `this.x()` threw `x is not a function`),
1787
- an **aliased** input is addressable under either name, and **`ngOnChanges` runs**.
1788
- - **Falsy values arrive.** The old guard was `if (value)`, so `false`, `0` and `''` never
1789
- reached the component. Only `undefined` is skipped now, which keeps meaning "not passed".
1790
-
1791
- If your component takes something from the library this way and the key is **not** a declared
1792
- input, the field is still assigned — nothing breaks — but you get one `console.warn` per
1793
- component type and key. Declaring it with `@Input()` (or as a signal input) is the fix.
1794
-
1795
- **Check your falsy defaults.** If a host component relied on the old guard — expecting to
1796
- keep its own default when the library passed `false` or `''` — it now receives the value.
1797
- This is the only part of 7.0 where a bugfix can change what you see on screen without any
1798
- code of yours changing.
1799
-
1800
- ### Model sync: the client's models travel with every call (7.1.0)
1801
-
1802
- Whether an action carried the page model used to be a per-action decision of the backend
1803
- (`UiAction.submit` / `model` / `composite`) plus one hidden rule (an action with no params sent
1804
- it anyway), and the model travelled under three ad-hoc `params` keys (`model`, `clientPageModel`,
1805
- `compositeModel`) that only one endpoint wrote into the backend view automatically. The bug that
1806
- forced the change: an action on an embedded view sent only its own model, the handler changed the
1807
- container's model, the container's whole component model came back, and whatever the user had
1808
- typed into the container since the last round-trip was gone.
1809
-
1810
- From 7.1.0 the client sends, with **every** action, widget action, upload and grid selection
1811
- call, the data of **every open view that changed since it was last synced** — slot-hosted and
1812
- dialog views alike — in a new `UiActionRequest.modelSync.models` map keyed by view uuid. The
1813
- backend writes each one into the matching view **before the handler runs and before it takes the
1814
- base of its change push**, so a handler always sees what is on screen and the client's own values
1815
- are never echoed back. A view equal to its last synced copy is left out; a view without a copy is
1816
- always sent.
1817
-
1818
- **Backend minimum, and the server switch.** The platform build that ships
1819
- `UiActionRequest.modelSync` (platform ADR-0011 on the server side), **with model sync switched
1820
- on**: `view.model-sync.enabled=true` in the host's `application.properties`. The switch defaults
1821
- to `false`, and in that mode the server behaves exactly as before model sync existed: nothing is
1822
- written into the views before the handler, and a request that carries `modelSync` is **rejected**
1823
- with an error naming the property, not ignored. A 7.1.0 client sends the envelope with every
1824
- call, so against a server with the switch off every call fails loudly; against an older platform
1825
- the field is silently ignored and every handler that read `params.model` sees nothing. **Order:
1826
- upgrade the backend, set the flag, then upgrade the client.** Server code can read the mode as
1827
- `viewApi.isModelSyncEnabled()`.
1828
-
1829
- **A 6.x client against a backend with the switch on** is rejected too: with
1830
- `view.model-sync.enabled=true` every request that can carry `modelSync` — an action, a widget or
1831
- upload action, a tree action, a grid selection change — must carry it, and one without it (an
1832
- old client, or host code that builds a `UiActionRequest` by hand and calls the view context
1833
- service directly) gets an error saying so. A call that cannot carry it (loading a view, a
1834
- message result, expanding a tree node, a map interaction) is neither synced nor refused. The old `params.model` / `clientPageModel` / `compositeModel`
1835
- keys are never read as a sync source; they are only *filled* by the backend, see below. So the
1836
- switch and the client upgrade go together per host: flip the flag in the same deploy that ships
1837
- the 7.1.0 client. Internal Java callers with nothing to sync send an empty `new ModelSync()`.
1838
-
1839
- #### What changed on the client
1840
-
1841
- - `UiAction.model` and `UiAction.composite` are **deprecated no-ops** for a view's own actions.
1842
- `submit` keeps one meaning: validate before sending, the embedded views' forms included. For an
1843
- executor that is not a view (a tree, the filter editor) `model` still flushes its form and its
1844
- model still rides on `params.model` as before, because it has no view to sync.
1845
- - Forms are flushed into their data (`submitForm(false)`) on every open view before a call, so an
1846
- unsaved field value is part of what is compared and sent. **Only the user's edits are
1847
- flushed**: the form remembers, per control, the value it last exchanged with the data (built
1848
- or refreshed from it, or written into it), and a control still equal to that writes nothing.
1849
- This is what keeps an untouched `false` or `0` from turning into `''` on a round trip (the
1850
- control is now built with `value ?? ''`, and a value written back takes the type the field has
1851
- in the data: `'true'` → `true`, `'12'` → `12`, `''` → `null`), and a backend push from
1852
- reaching the data only to be overwritten by the control's stale value on the next call. The
1853
- same rule the other way: a data field the backend changed reaches its control even when the
1854
- user edited it (the backend wins on a field it changed), a field it did not change keeps what
1855
- the user typed. `SmartFormService.resetExchangedValues()` starts over when a form is rebuilt.
1856
- The form's own dirty flag is not involved: it lives until the form is rebuilt and says nothing
1857
- about what the data holds.
1858
- - A push of some `data.*` paths marks only those paths as synced; whatever else the view holds
1859
- keeps its standing, so a value written into the data outside a form while a call was in flight
1860
- is still sent by the next one. A whole-model push and the initial load mark all of it.
1861
- - Calls run concurrently, and an envelope built earlier may complete later: its copies are never
1862
- written over the copies a later envelope or push already settled. The server-side ordering of
1863
- rapid per-field actions stays out of scope (ADR-0010).
1864
- - A dialog the backend closed (`ViewState.TO_CLOSE`) no longer fires a `DEFAULT_CLOSE` action of
1865
- its own on top of the response that closed it; only the user's own close does.
1866
- - The per-field actions a widget's `valueChangeMode` fires are ordinary actions and inherit the
1867
- sync; they no longer set `model: true`, and still carry `params.item`.
1868
-
1869
- #### Removed from the public API
1870
-
1871
- | Gone | What to do |
1872
- |---|---|
1873
- | `SmartComponentApiClient.sendModelOnWidgetAction` | Delete the assignment. Every widget action carries the changed models now. Loud: `this.sendModelOnWidgetAction = true` no longer compiles. |
1874
- | `SmartComponentApiClient.addModelToWidgetParams()` / `addModelToWidgetAction()` | Delete. The grid service builds its own envelope. |
1875
- | `SmartViewContextService.getCompositeModel()` | Delete. The backend gets every changed view. |
1876
- | `SmartViewContextService.submitEmbeddedForms()` | `validateEmbeddedForms(containerUuid)` — validation only; the flush is part of every call. |
1877
- | `SmartViewContextService.dataChanged()` | Delete. It was never wired on the client (`handleValueChanges` had no caller); a field change is a uiAction. |
1878
-
1879
- New on `SmartViewContextService`: `joinModelSync(uuid, participant)` / `leaveModelSync(uuid,
1880
- participant)` (the client does this on its own uuid; leaving only takes effect for the participant
1881
- that is registered, so a component destroyed after its successor joined under the same uuid does
1882
- not take the successor with it), `markModelSynced(uuid, keys?)` (the client calls it whenever the
1883
- backend's model reached it: without keys the whole data, with keys the pushed `data` paths), and `withModelSync(carrier, call)` for a host that issues its own backend call on a request
1884
- that can carry the envelope (a `UiActionRequest`, a `GridSelectionChange`): it puts the envelope
1885
- on the carrier, runs the call with it and settles the copies on success, and hands back the raw
1886
- `ViewContextChange` — what to do with the response stays with the caller. The grid and tree
1887
- services are the in-tree examples; `beginModelSync()` / `completeModelSync()` underneath are
1888
- public too.
1889
-
1890
- #### The Java side of the host: three kinds of handler
1891
-
1892
- The backend **backfills** the old `params` keys from the synced model on every request (with the
1893
- switch on, every request carries `modelSync`), so no handler has to change on the day the client
1894
- is upgraded. Two kinds must be
1895
- looked at anyway, and one of them breaks without a compiler error. Run
1896
- `node tools/model-sync-audit/cli.mjs ../<host>` — it lists every read of the request model with
1897
- one of these categories and a reason ([its README](../../tools/model-sync-audit/README.md)):
1898
-
1899
- | | The handler | What to do |
1900
- |---|---|---|
1901
- | **A** | reads `params.model` (`extractClientModel(request)`, `actionRequestHelper(request).get(UiActions.MODEL, …)`) and only passes it to `setModel(viewUuid, …)` | Delete both statements. The sync already wrote the client's model into the view. |
1902
- | **B** | reads it for something else: saves it, validates it, forwards it, mutates it before setting it back | Nothing required. `getModel(viewUuid)` is the honest read now; `extractClientModel` is `@Deprecated`. A grid row action's `params.model` is still the clicked row and is never synced. |
1903
- | **C** | reads the view's model too (`getModel(viewUuid)`, `view.getModel()`) and uses the two together — a diff, a merge, "what changed" | **Breaks silently.** After the sync `getModel` *is* the client's model, so the comparison compares a thing with itself. The server-side read must become `viewApi.isModelSyncEnabled() ? getPreviousModel(viewUuid) : getModel(viewUuid)`: the model as the server knew it before this call, request-scoped (`viewApi.getPreviousModel(uuid, Class)` outside a `PageApiImpl`). The off branch is the old code. |
1904
-
1905
- A handler that must work in both server modes — a shared module deployed to hosts with the
1906
- switch off as well as on — branches inline on `viewApi.isModelSyncEnabled()` and keeps its old
1907
- code verbatim in the off branch: an A site becomes
1908
- `if (!viewApi.isModelSyncEnabled()) { setModel(viewUuid, extractClientModel(request)); }`, a
1909
- client read `viewApi.isModelSyncEnabled() ? getModel(viewUuid) : extractClientModel(request)`, a
1910
- C site's server read the ternary above. No wrapper helper on purpose: once every host runs with
1911
- the switch on, the off branches are deleted line by line and what remains is the plain new code.
1912
- The platform's own handlers are written this way. `getPreviousModel` with the switch off returns
1913
- `getModel` and logs an ERROR: that code assumes a mode the server is not running in.
1914
-
1915
- Two traps the audit cannot see:
1916
-
1917
- - A handler that forwards the *request* to a callback which reads `params.model` (the platform's
1918
- password editor does this) depends on `extractClientModel` writing the typed instance back into
1919
- the params. Keep `extractClientModel` there, or hand the model over explicitly.
1920
- - A handler that reads `compositeModel` by hand gets it backfilled only when its action still
1921
- has `composite=true`. Read the children's models from `viewApi` instead; the flag goes away
1922
- once no host reads the key.
1923
-
1924
- The measured spread, over the 56 host repositories that were audited when this shipped: 1061 read
1925
- sites, 301 A, 671 B, 77 C.
1926
-
1927
- ### New in 7.2: HTML widget, row layouts, hydrated toolbars (#30388)
1928
-
1929
- Additive, with **one behaviour change** and one removed field. The reference is
1930
- `HTML-WIDGET.md`, shipped in the package next to this file.
1931
-
1932
- - **`SmartFormWidgetType.HTML`**: server-sent html whose client placeholders (`{{…}}`) the client
1933
- resolves against the model, with toolbars hydrated into its toolbar slots
1934
- (`<smart-ui-action-toolbar data-sb4-toolbar="…">`) and action triggers (`data-sb4-action`) on
1935
- any element. The `DIV` widget is untouched. A host that never sends the widget sees no change.
1936
- The widget can bring its own style sheet (`HtmlProperties.css`), which the client adds to the
1937
- document head once per distinct text; a host with a nonce-based `style-src` provides
1938
- Angular's `CSP_NONCE`. A style sheet can animate new html coming in and, through the
1939
- `sb4-html-leaving` class the client puts on the widget or cell before it replaces the html,
1940
- going out.
1941
- Inside a `{{#section}}` a placeholder reads the surrounding scope only when it says so,
1942
- `{{../title}}`: one step per entering section, never above the widget's root.
1943
- - **Icon slots (in the tree, released after 7.2.4)**: an element in the html marked with
1944
- `data-sb4-icon` hydrates into a
1945
- `smart-icon` — the same component a button or a cell uses. The marker addresses a name
1946
- (`data-sb4-icon="mail"`, coloured by `data-sb4-icon-color`, a theme or host class name), an
1947
- `ImageResource` (a placeholder that resolves to one, or the json `Tag.icon(resource)` writes
1948
- in), or a key of the template's icon dictionary (`data-sb4-icon-key`,
1949
- `HtmlProperties.icons`), which sends a finite type-to-icon mapping once with the template
1950
- rather than once per row. An address that resolves to nothing renders no icon and says
1951
- nothing; an unknown dictionary key warns. Nothing changes for a host that sends no icon slot.
1952
- - **Row layouts**: a cards grid can carry its card layouts once per grid
1953
- (`GridViewDescriptor.rowLayouts`, picked per row by `GridRow.rowLayout`) instead of once per
1954
- row. The per-row `layoutDescriptor.GRID_ROW_LAYOUT` still works and wins over them; a
1955
- host-registered `'<GRID_ID>Card'` component still renders the rows that get no layout.
1956
- - **Clicking a card rendered from a layout — 7.2.0 changed it, 7.2.1 takes the change back.**
1957
- 7.2.0 made a click select the row and moved the default row action to the double click. That
1958
- broke `mod-contact`'s contact list, and a cards grid naming no `selectionMode` lost its click
1959
- altogether. **From 7.2.1 a card behaves as in 7.1.x again unless the server says otherwise**: a
1960
- click fires the row's first applicable default action, and nothing is selected by click while
1961
- the click belongs to the default action. A host that put a root trigger on its cards for 7.2.0
1962
- can keep it; it is no longer needed.
1963
- - **`GridViewDescriptor.defaultRowActionTrigger` (7.2.1, `click` | `doubleClick`)** makes the
1964
- gesture data, so a cards grid is programmed like a table: `selectionMode` decides whether rows
1965
- are selectable, the trigger decides what fires the default row action. With `doubleClick` a
1966
- click selects the card and a double click fires the default action, as on a `ROW_SELECT` table.
1967
- With an explicit trigger several applicable default actions open a menu at the pointer, filtered
1968
- to what the row offers. Without a value a card keeps the click and a table the double click; in
1969
- 7.2 a table fires on double click whatever the value is (7.3 honours an explicit `click`, see
1970
- below). The second click of a double click is not a
1971
- click of its own on a card: it neither fires the default action again nor toggles the selection
1972
- back, and further clicks are dropped while the default action runs. Needs a platform that knows
1973
- the field; an older client ignores it.
1974
- - **The double-click menu of a table works again.** A grid row offering several of the grid's
1975
- `defaultRowActions` opens a menu at the pointer on double click. The position override behind it
1976
- had been written for an older Material signature and threw on open, so until 7.2.1 such a double
1977
- click did nothing.
1978
- - **`SmartGrid.layoutComponent` is removed.** The card gets the layout class through the
1979
- `SMART_ROW_LAYOUT_COMPONENT` token that `provideSmartNgClient()` provides, so a cards grid a
1980
- host builds with `addGrid()` renders backend layouts too. Delete the field if a host grid model
1981
- sets it; nothing else changes.
1982
- - A toolbar no longer lets a `dblclick` on its buttons through to the element around it (it
1983
- already kept the `click`).
1984
- - The toolbar's re-entry guard is per action now: while one action runs, another button of the
1985
- same toolbar can be fired. It is also released when the action fails.
1986
- - **Trusted Types**: the HTML widget writes backend-authored html as it is, as the `DIV` widget
1987
- always has. A host that enforces Trusted Types must allow that write.
1988
- - **Table cells**: `GridColumnMeta.contentType` now reaches the table. A column that names none
1989
- (or `html`) renders exactly as before, sanitized; `text` escapes the value. A column with
1990
- `htmlProperties` renders a column template — the HTML widget's dialect, resolved per row, with
1991
- toolbar slots and triggers offering the row's actions. A row action addressed to
1992
- `UiActions.NO_TOOLBAR` no longer shows in the row menu; in a table with column templates no row
1993
- action addressed to a toolbar does.
1994
-
1995
- ### New in 7.3: object navigation and editing screens (#23616)
1996
-
1997
- Additive for a host that uses the components as they are, with **visible style changes in every
1998
- host**; a host that calls `TreeService.selectNode2` or extends `Table` has source changes (see
1999
- "Source changes" at the end of this section). Needs the matching platform (`TreeSelectionRequest`,
2000
- `ViewContext.sequence`); release the two together. With an older platform the edits a tree
2001
- selection carried are not sent again and late responses are always applied.
2002
-
2003
- - **Theme token defaults.** The first `ThemeService` (every host has one through
2004
- `SmartViewContextService`) puts a `<style id="sb4-theme-defaults">` first in `<head>`: light
2005
- values of the library's tokens on `:root`, dark values under `.theme-dark`. Tokens a host sets
2006
- on `body` or `:root` win; tokens set on `html { }` do not, so move them to `body`. The defaults
2007
- cover 14 tokens (`--color-surface(-alt)`, `--color-text(-muted)`, `--color-border`,
2008
- `--primary / secondary / accent / warn / success / warning / info-color`,
2009
- `--primary-text-color`, `--tree-active-color`). The optional tokens the library also reads
2010
- (`--tree-active-background`, `--grid-open-row-background`, `--grid-open-row-bar`,
2011
- `--primary-light-color` and the other old names below) get no default and fall back where they
2012
- are read, for example `--tree-active-background` to `--accent-color`. A token the host never
2013
- defined now has a value, for example `--tree-active-color: #ffffff`. A host with a
2014
- nonce-based `style-src` provides Angular's `CSP_NONCE`, which the element then carries.
2015
- - **Dark defaults** stand under `.theme-dark` only (with `color-scheme: dark`), so they apply
2016
- only when the host offers a theme named `dark`. A host that sets its light tokens on a plain
2017
- `body` hides them and ships its dark theme with the full token set.
2018
- - **Components read the theme tokens.** The menus, action buttons, tables, the mobile table and
2019
- the grid take their colours from `--color-surface`, `--color-surface-alt`, `--color-text`,
2020
- `--color-text-muted`, `--color-border`, `--primary-color` and `--primary-text-color` instead of
2021
- fixed values. A host without these tokens sees the defaults above, which are close to, but not
2022
- exactly, the old colours (menu text `black` → `#132033`, disabled `#aaa` → `#68778e`, table
2023
- hover `whitesmoke` → `#f4f6fb`). The old names `--primary-light-color` (selected table row),
2024
- `--disabled` (disabled row), `--light-gray` (grid edit icon), `--mat-sys-primary` (selected
2025
- card outline) and `--primary-color-text` (activated view button) are still honoured before the
2026
- new ones. The grid overlay uses relative colour syntax (`rgb(from …)`, Chrome 119 / Safari
2027
- 16.4 / Firefox 128), inside Angular 22's browser floor.
2028
- - **Row states.** `GridRow.rowState` is rendered as the row class `sm-grid-row-<state>`
2029
- (`new`, `modified`, `deleted`, `moved`, `contains-change`, or an application code in lower
2030
- case) on table, mobile-table and card rows, with default styles: new green italic, modified
2031
- accent bold, deleted muted with struck-through cell text (`.smart-table-cell-text`), moved
2032
- italic, contains-change the accent colour only. A card only takes the colour: a deleted card is
2033
- muted, nothing is struck through. A selected row keeps its normal text colour. Any grid whose
2034
- server already fills `rowState` shows them.
2035
- - **Row state column.** `GridViewDescriptor.rowStateDisplay` (`GridRowStateDisplay`) picks how a
2036
- table and a mobile table show the states: `STYLE` (the default when absent) the row classes
2037
- above, `COLUMN` a first column of state icons without the row classes, `BOTH` the two
2038
- together. The column shows an icon for the platform states (an application state has none),
2039
- with the tooltip and accessible name from `rowStateLabels` (by state code); the column itself is
2040
- named by `rowStateColumnLabel` and has no header text. It is not a descriptor column: not
2041
- editable, not in the column chooser. The platform fills both labels in the session's locale.
2042
- A cards grid has no column and keeps the row classes whatever the value.
2043
- - **Mark of the open row.** A grid whose `GridViewDescriptor.markOpenRow` is true gives the row
2044
- of `GridPage.openRowId` the class `sm-grid-row-open` (table and mobile table): a light accent
2045
- tint and a bar at the row's start, restyled by `--grid-open-row-background` and
2046
- `--grid-open-row-bar`. A default row action, from a double click, a click or the row menu, moves
2047
- the mark at once; a grid page requested before the action's answer keeps the new mark, the first
2048
- one requested after it brings the server's `openRowId`.
2049
- - **`defaultRowActionTrigger: click` on a table.** A table now honours an explicit `click`: a click
2050
- on a row with an applicable default action fires it (several open the menu at the pointer), does
2051
- not select the row, and a double click does nothing more. A click on a button, link or input in
2052
- the row stays the control's own. Without a value or with `doubleClick` nothing changes. Table
2053
- rows are tracked by their id, so a reloaded row keeps its elements.
2054
- - **Tree grid controls.** The header of a TREE_GRID shows expand all / expand level / collapse
2055
- level / collapse all as small icon buttons, disabled when there is nothing to do. The server
2056
- picks and orders them with `GridViewDescriptor.treeGridControls` (`TreeGridControl`): absent or
2057
- null shows all four, `[]` none (before: two mini-fab buttons).
2058
- - **Trees.**
2059
- - A tree reload keeps the node objects of the nodes it still has and writes the new data
2060
- into them. Rows survive a reload, a click that started before it still lands, and
2061
- `smartTreeModelChanged` fires once per reload (before: twice, first empty).
2062
- - A node keeps the icon the backend sends; a node without one gets `Folders` if the host
2063
- registered it, Material `folder` otherwise (before: always `Folders`).
2064
- - A node action with its own `descriptor.title` is labelled by it.
2065
- - The caption shows the short description as a tooltip.
2066
- - Without a `[treeStyle]` a row is indented 15px per level and the selected row gets
2067
- `sm-tree-node-selected` (`--tree-active-background`, else `--accent-color` /
2068
- `--tree-active-color`); its caption and short description take the colour with
2069
- `color: inherit`, so a host rule on every element (`* { color: … }`) does not reach them.
2070
- With a `treeStyle` nothing changes.
2071
- - Server classes `sm-tree-branch-new|modified|deleted|foreign|own-other-view-context` have
2072
- default styles.
2073
- - **Tree selection syncs the models.** Selecting a tree node sends the client's changed models
2074
- like an action does (`TreeSelectionRequest.modelSync`). Needs the matching platform:
2075
- an older one ignores the body and the edits it carried are not sent again.
2076
- - **Tree widgets in a server layout** provision their own tree service from the widget
2077
- identifier when the screen's default tree is a different one or there is none, so a generic
2078
- page can show a tree and a layout can hold several.
2079
- - **Late responses.** The server raises `ViewContext.sequence` on every save of the view
2080
- context; the response of an action or of a tree selection whose view context is not newer
2081
- than the one the client holds no longer overwrites it, its changes are still applied. Needs
2082
- the matching platform: an older platform sends no sequence and its responses are always
2083
- applied. Such a response first waits for the view-context updates the client still has in
2084
- flight, and their answers count in the comparison (they are applied as before, without the
2085
- guard). A response that is not applied still closes the views it marks `TO_CLOSE`.
2086
- - **Views entering a slot.** When one response closes an embedded view and opens another in
2087
- the same slot, the two are exchanged in one step: the old component stays (inert, cut off
2088
- from the server) until the new one has its model, at most 3 s. Every view opened into a slot
2089
- of a server layout likewise stays hidden until its model arrives, so the layout beside it
2090
- changes once. Showing such a view fires one `window` `resize` event, so content that measured
2091
- itself while hidden (textarea autosize, scrolling toolbars) measures again.
2092
- - **Empty slots.** An embedded slot of a layout that shows no view carries `sm-slot-empty`; an
2093
- empty `sm-nav-pane` takes no width, so the list beside it has the full width.
2094
- - **Focus after a pane closes.** When the view of an `sm-nav-pane` closes and the focus went
2095
- with it, the focus moves to the open row (`sm-grid-row-open`) of the `sm-nav-list` beside the
2096
- pane, else to the row that was open while the pane showed the view, else to the list.
2097
- - **Navigation frame styles.** Default layout for the server classes `sm-nav-view`,
2098
- `sm-nav-pane`, `sm-nav-list`, `sm-nav-header`, `sm-nav-caption`, `sm-nav-section(-title)`,
2099
- `sm-nav-tree`, `sm-nav-slot`, `sm-nav-crumbs`, `sm-crumb-ahead`, `sm-crumb-current`; a host may
2100
- restyle them by the same names. `sm-nav-header` puts the caption of a pane view and the toolbar
2101
- with the pane's close on one line. Inside `.sm-nav-list` a grid toolbar without a button is
2102
- hidden and the grid has no top padding; the search page container has a 0.5rem gap there.
2103
- - **Icon buttons** get an `aria-label`: the action's title, else its tooltip. A button that shows
2104
- its title is named by it and gets none.
2105
- - **Row actions in a table's action column** stand side by side; the toolbar no longer wraps
2106
- in the narrow column (row actions the server shows as icons).
2107
- - **Prerendered pages.** A page shown after browser prerendering keeps its view context uuid
2108
- (sessionStorage written during prerendering is dropped; the client restores it from the request
2109
- header), so a tree on it loads its nodes.
2110
- - **Search page title**: `.search-page-title` is 1.35rem / 600 / no margin on every search
2111
- page, a deliberate global change.
2112
- - **`--primary-color` fallback.** Where the menus and the activated view button fall back
2113
- without the token, the value is `#384372` instead of `#1976d2`; the tree grid header buttons
2114
- are 28px squares.
2115
- - New public API: `GridViewDescriptor.treeGridControls`, enum `TreeGridControl`,
2116
- `SmartTableInterface.treeGridControls`, `SmartIconService.hasCustomIcon()`,
2117
- `ViewContext.sequence`, `ViewContextData.sequence`, `SmartTreeComponent.trackByIdentifier`,
2118
- `SmartGridComponent.calcRowClasses()`; `GridViewDescriptor.rowStateDisplay`, `rowStateLabels`,
2119
- `rowStateColumnLabel`, `markOpenRow`, enum `GridRowStateDisplay`, `GridPage.openRowId`;
2120
- `SmartTableInterface.defaultActionOnClick`, `markOpenRow`, `openRowId`, `rowStateDisplay`,
2121
- `rowStateLabels`, `rowStateColumnLabel`, `onRowAction`; `UiActionToolbarComponent.actionFired`;
2122
- `EmbeddedSlotProperties` (`singleView`: the server closes the view standing in the slot before
2123
- it opens the next; generated for completeness, the client does not read it).
2124
- - **Source changes** for a host that calls these members or extends the classes:
2125
- - `TreeService.selectNode2(viewUuid, treeId, nodeId, treeSelectionRequest?, observe?, …)`: the
2126
- optional `TreeSelectionRequest` stands before `observe` in every overload, and the call sends
2127
- a JSON body. A call that passed `observe` in the fourth place does not compile; insert
2128
- `undefined` or the request.
2129
- - `Table` (the base of the table components): `handleOnRowClick(element)` became
2130
- `handleOnRowClick(event: MouseEvent, element)`; the getters `expandTreeGridLevel` and
2131
- `closeTreeGridLevel` are gone (the controls come from `treeGridControls`); the rows and the
2132
- header render `displayedColumns` instead of `smartTable.tableHeaders`, and the expanded
2133
- detail row spans `displayedColumns.length`. New members: `trackRow`, `rowStateColumn`,
2134
- `onRowActionFired`. A host template copied from the library's must follow.
2135
- - `SmartTreeComponent.treeControl`'s children accessor returns an `Observable` of the children,
2136
- not the array. `SmarttreeGenericService` writes a reload into the node objects a host holds
2137
- (same objects, new data) and sets `treeFromBackend` to the merged nodes.
2138
-
2139
- ### New in 7.3.2: keybindings (#19122)
2140
-
2141
- Additive: a host without a keymap gets one binding, Escape closes a dialog that offers
2142
- `DEFAULT_CLOSE`. Needs the matching platform (`Keybinding`, `View.keybindings`,
2143
- `ComponentModel.keybindings`); release the two together.
2144
-
2145
- - **The keymap.** Properties files on the server, `CODE=chord[,chord]` binds every action of
2146
- that code, `viewName.CODE=chord[,chord]` only in that view and wins over `CODE`; an empty
2147
- `viewName.CODE=` disables the chord in that view. The host lists its own file before the
2148
- platform's, the first file with the key wins:
2149
- `spring.keybindings.basename=keybindings-<app>,keybindings-platform`. The platform's file binds
2150
- Escape to `DEFAULT_CLOSE`, `CANCEL` and `CLOSE_SEARCH_PAGE`, which close the view on every
2151
- platform page offering them; without the property only that file is read.
2152
- - **Chord grammar.** Modifiers, then one key, joined by `+`: `Mod+Shift+S`. `Mod` is Cmd on a Mac
2153
- and Ctrl elsewhere, so one chord serves both; `Ctrl`, `Alt`, `Shift`, `Meta` are the literal
2154
- keys. The server writes every chord canonically (modifier order `Mod`, `Ctrl`, `Alt`, `Shift`,
2155
- `Meta`, letters upper case), so the order and case in the file do not matter. The key is the
2156
- browser's `KeyboardEvent.key` name (`Escape`, `Enter`, `ArrowUp`, `F2`, `Delete`), the space
2157
- bar is `Space`. A letter or digit also matches by its physical key when a modifier changes the
2158
- character (Option+N on a Mac). Chords the browser keeps for itself (`Mod`/`Ctrl` with `W`,
2159
- `T`, `N`, `Q`, `R`, `Tab`, and `F5`) and unparsable ones are dropped with an ERROR log.
2160
- - **Bindings in code.** A page binds its own actions on the view, typically in `initModel`:
2161
- ```java
2162
- view.addKeybindingsItem(new Keybinding()
2163
- .chord(KeyChord.of(KeyboardKeys.MOD, KeyboardKeys.SHIFT, KeyboardKeys.ARROW_UP))
2164
- .uiActionCode(MOVE_UP)
2165
- .uiActionIdentifier(rowId)); // optional: without it every MOVE_UP action is bound
2166
- ```
2167
- Per action the code level wins over `viewName.CODE`, which wins over `CODE`; a code-level entry
2168
- replaces the keymap of its action, they are not merged. Only the code level can name an
2169
- identifier. When two actions of a view share a chord, the more specific source keeps it, then
2170
- the earlier action, with a WARN log. The server resolves all this into
2171
- `ComponentModel.keybindings`; a keymap entry of an action the view does not offer yields
2172
- nothing.
2173
- - **Which view gets the key.** One document listener serves every view. The key goes to the view
2174
- holding the focus, the innermost one when views are nested; when nothing has the focus, to the
2175
- view that had it last (the focus is lost when a pushed layout re-renders the field), else the
2176
- top-most dialog, else the current page. If that view has no enabled binding for the chord, the key
2177
- bubbles to the enclosing views, but never out of a dialog: the page behind it does not react
2178
- while a dialog is open. A NORMAL view opened by the user's action takes the focus, a view
2179
- restored on reload does not.
2180
- - **What does not fire.** Unmodified typing in an editable element (text inputs, textareas,
2181
- selects, contenteditable): letters, digits, editing and caret keys, and `Enter` in a multi-line
2182
- field stay the element's; `Escape`, the function keys, chords with Ctrl, Alt or Cmd, and `Enter`
2183
- in a single-line input reach the bindings. A disabled or not offered action does not fire: the
2184
- key bubbles on and reaches the browser when nothing takes it. Auto-repeated keys, a key a panel
2185
- already handled (`defaultPrevented`) and a key pressed in an overlay of no view (an open select
2186
- or menu panel) fire nothing. An unmodified `Enter` or `Space` on a focused button, link or
2187
- ARIA-interactive control stays with that control (the browser activates it), and the `Enter`
2188
- that confirms an IME composition fires nothing. A fired chord is `preventDefault`ed.
2189
- - **Tooltip and accessibility.** A toolbar button shows its chords in its tooltip, formatted for
2190
- the platform (`⌘⇧S` on a Mac, `Ctrl+Shift+S` elsewhere), after the server tooltip or alone; the
2191
- tooltip also opens on keyboard focus. The button names them in `aria-keyshortcuts`.
2192
- - **Two host rules.** Do not bind `Enter` on a view whose form uses `SmartForm.submitOnEnter`:
2193
- both would run. With `dialogDisableClose: false` MatDialog closes the dialog on Escape itself,
2194
- as before; the `DEFAULT_CLOSE=Escape` binding is what closes it under the default `true`.
2195
- - **Focus survives a push.** When a push re-renders the field being edited, the view focuses it
2196
- again: the field with the same widget key (`data-testid`) at the same position within the view,
2197
- with the caret or selection of a native input or textarea and of a Quill editor. Nothing
2198
- happens when the field left the layout or the focus moved elsewhere meanwhile (a dialog, a click).
2199
- - **Pending edits are saved first.** A fired action (a key, a toolbar click, an HTML trigger)
2200
- waits until the view's widgets have saved what was typed into them — today the filter editor,
2201
- which saves its fields on blur — so a query bound to `Enter` runs with the value just typed. If
2202
- that save fails, the action is not sent.
2203
- - **Messages.** A message with a single option focuses it and sends it on Escape. Otherwise the
2204
- first `confirm`-typed option gets the initial focus (Enter presses it) and Escape sends the first
2205
- `cancel`-typed option. A `reject` option is never chosen by a key, and untyped options (a YES/NO
2206
- without types) get neither: Escape does nothing, the focus stays on the first button.
2207
- - **Out of scope.** Row actions (grid, table, card rows) and actions inside widgets have no
2208
- keybindings; only the view's own actions do.
2209
- - **New in the public API:** model `Keybinding`, `View.keybindings`,
2210
- `ComponentModel.keybindings`; Java `KeyChord`, `KeyboardKeys`, `KeybindingSettingApi`.
2211
-
2212
- ### New in 7.3.5: a stored layout names its HTML template by key (#30388)
2213
-
2214
- Additive, and needs the matching platform. Nothing changes for a layout that names no key.
2215
-
2216
- - **`HtmlProperties.templateKey`.** An HTML widget of a stored layout (an
2217
- `ObjectLayoutDescriptor`) names the template its html and style sheet come from:
2218
- `h.templateKey("CONTACT_CARD")` in code, `html card { template: CONTACT_CARD }` in the layout
2219
- DSL. The key stays in the widget and wins over a stored html.
2220
- - **`resolvedCopy(ctx, viewName, layout)`** (`PageApiImpl`, `ObjectLayouts`) is how a stored
2221
- layout goes onto a view: a deep copy with the keys loaded and the server placeholders resolved.
2222
- The platform's own paths use it (`ObjectLayoutDescriptorUtilApi.initLayoutDescriptor`, the MDM
2223
- entry editor, object navigation). **A host that puts `descriptor.getLayouts()` or the layout of
2224
- `getObjectDisplay` onto a view itself has to call it** before it stores a key in such a layout.
2225
- - **A widget whose key was never loaded renders nothing** and logs one console warning naming
2226
- the key.
2227
- - **Behaviour changes on the server.** `ObjectLayoutDescriptorUtilApi.initLayoutDescriptor` now
2228
- puts a copy of the descriptor's layout on the view, not the descriptor's own instance, and
2229
- fails on an html with a server placeholder (`${alias:/…}`) it has no context for — pass the
2230
- context with the new `initLayoutDescriptor(view, descriptor, ctx)`. `NavigationNodeLayoutInternalApi
2231
- .createBodyLayoutWithFieldEdges` takes the view name as its first argument.
2232
- `PageApiImpl.gridBuilder(...)` hands the template resolver to the `GridBuilder`, so
2233
- `htmlColumn(c -> c.template(ctx, KEY))` works.
2234
- - **New in the public API:** `HtmlProperties.templateKey`; Java `HtmlWidgetBuilder.templateKey`,
2235
- `ObjectLayouts.resolvedCopy` / `templateKeys` / `unloadedTemplateKeys`, the `viewName` variants
2236
- of `resolveHtml`, `DataSheetPageApi.CONTEXT_DATA_SHEET`. See HTML-WIDGET.md, *Template-kulcs egy
2237
- tárolt layoutban*.
2238
-
2239
- ### Bugfixes shipped with 7.0
2240
-
2241
- - (7.3.2) A view dialog no longer disappears on the browser's back button. MatDialog's default
2242
- `closeOnNavigation` disposed every dialog on a Location change without any action, so the
2243
- backend kept the views open and a reload brought them back; view dialogs now open with it off.
2244
- - `SmartformwidgetComponent.ngAfterViewInit` no longer crashes with
2245
- `Cannot read properties of undefined (reading 'valueChanges')` when the form
2246
- contains a `MONTH_PICKER` widget (it wrongly subscribed to the non-existent
2247
- `<key>-time` control; only `DATE_TIME_PICKER` has one).
2248
- - A widget nested in a CONTAINER receives `blurSophisticatedValueChange`; the recursion never
2249
- passed it down, so a BLUR-mode child would have thrown on blur.
2250
- - `SmartformComponent` reaches widgets nested in a CONTAINER when it changes them in place
2251
- (values, constraints, the touched state after a failed submit). The view query it used
2252
- could not see them.
2253
- - `UiActionDescriptorService` no longer shares one dialog object between actions. Any action
2254
- whose descriptor had no `dialog` of its own was given the service's single placeholder — the
2255
- same object every time — and had its title and button caption written into it, so resolving
2256
- one action renamed the dialog every other one was holding. The service is
2257
- `providedIn: 'root'`, so this outlasted the view.
2258
-
2259
- What you saw on screen, measured on both versions:
2260
-
2261
- | The action | 6.x, when the dialog opened | 7.0 |
2262
- |---|---|---|
2263
- | described by the backend, no dialog of its own | **another action's code** | its own code |
2264
- | described by nothing at all | **another action's code**, or empty | empty |
2265
- | registered client-side, no dialog of its own | its own code | unchanged |
2266
-
2267
- The middle column is not a typo. An action the backend describes is resolved twice in the
2268
- normal flow — once when the toolbar renders it, again when the dialog it opens resolves it
2269
- for itself — and the first resolution wrote a dialog onto the action's own descriptor, so the
2270
- second one skipped the step that fills in the title. What the dialog then showed was whatever
2271
- code had last passed through the shared object. An **already open** dialog could change too,
2272
- since its title is re-read on every change-detection pass.
2273
-
2274
- An action nothing describes still gets an untitled placeholder dialog: its *button* carries
2275
- the code, the dialog does not. That is unchanged and deliberate — such an action is a gap in
2276
- the host's descriptor map, and this is the honest rendering of it.
2277
-
2278
- A resolved descriptor is now the caller's own object throughout, which also means
2279
- `getActionDescriptor()` no longer writes a placeholder dialog and a `SNACKBAR` feedbackType
2280
- into the `descriptor` the backend put on the action itself.
1
+ # @smartbit4all/ng-client 6.x → 7.0 migration guide
2
+
3
+ > 7.0.0 targets **Angular 22.0.8 / TypeScript 6.0.3 / Material 22.0.6**, with PrimeNG fully
4
+ > removed (Material/CDK only), every library component `OnPush`, and **no zone.js** — the
5
+ > configuration 7.0 is tested in. Angular's `moment` date adapter is replaced by `date-fns`.
6
+ > It is a major because a host has to act; nothing here is a silent behaviour change that
7
+ > you could ignore.
8
+
9
+ ## Where to start
10
+
11
+ 1. **Run the codemod** — [Migrating a host: the codemod](#migrating-a-host-the-codemod). It
12
+ does the mechanical part (the module imports, the provider configuration, the dead legacy
13
+ calls) and prints `file:line` for everything that needs a decision.
14
+ 2. Read [Full standalone + `provideSmartNgClient()`](#full-standalone--providesmartngclient-phase-32--the-big-one).
15
+ That section rewrites your `app.module.ts`; everything else is small next to it.
16
+ 3. Read [Dates](#dates-moment--date-fns-and-the-timezone-contract-phase-38). It is the one
17
+ change that needs action **at install time**, and the one that breaks at *runtime* on the
18
+ first screen with a date field rather than at build time.
19
+ 4. Then the rest in order. The **PrimeNG removal** sections (1.1–1.10) are API changes; the
20
+ **hop sections** carry the framework defaults you inherit (OnPush, fetch vs XHR, `?.`).
21
+ 5. If you write your own widget, `WIDGETS.md` next to this file is the protocol.
22
+
23
+ Measured across the 12 hosts in scope on 2026-07-28, so you know what to expect: 12–17
24
+ `Smart*Module` imports per host, 1–20 string-keyed providers, `.cssClass =` on an action
25
+ entry at 72 sites in 7 hosts, `this.uiActionModels =` in 5 hosts, `UseUiAction` in 6.
26
+
27
+ **How big is this for *your* host?** Codemod dry-run counts for the five hosts in scope
28
+ (2026-07-30). The first column the codemod applies for you; the second is the list it refuses to
29
+ guess at, which is the real work:
30
+
31
+ | host | mechanical | needs a decision |
32
+ |---|---|---|
33
+ | p014 *(done — the reference)* | 121 | ~100 |
34
+ | p043 | 121 | 96 |
35
+ | app-tournament | 86 | 34 |
36
+ | app-finance-ai | 62 | 18 |
37
+ | app-fitnessmirror | 61 | 14 |
38
+
39
+ The spread is not about host size — all five are within 274–841 source files — but about which
40
+ APIs they happened to use. p043 is p014's twin and repeats its whole decision list; the other
41
+ three have **no `.cssClass =` sites at all**, and their lists are dominated by `smart-modules`,
42
+ the 3-line `moment-adapter` swap, and `zone-js` (which is **optional** — dropping zone.js is not
43
+ required). Take a small one first if you want the process shaken out cheaply.
44
+
45
+ **One number that is now zero: the library install.** p014 was migrated against an unpublished
46
+ build and paid for it in hand-copies, tarballs and `ERESOLVE`. 7.0.0 is on npm, so a host today
47
+ writes `"@smartbit4all/ng-client": "^7.0.0"` and runs `npm i`. None of that thread applies to you.
48
+
49
+ ## Migrating a host: the codemod
50
+
51
+ It lives in the `platform-angular2` repository — `tools/ng-client-7-codemod/` — and you run
52
+ it with plain Node, from a checkout, against your own source directory. Nothing to install.
53
+
54
+ ```bash
55
+ node tools/ng-client-7-codemod/migrate.mjs ../my-host/ui-angular/src --dry-run # look first
56
+ node tools/ng-client-7-codemod/migrate.mjs ../my-host/ui-angular/src # then write
57
+ ```
58
+
59
+ It **refuses to run on a dirty git tree** (its diff is how you review it, and `git checkout .`
60
+ is how you undo it), it is **idempotent**, and it prints a verdict for every rule it knows —
61
+ including `no sites found`, because a codemod that silently matched nothing looks exactly
62
+ like one that had nothing to do.
63
+
64
+ **What it rewrites:** the `Smart*Module` imports (into the standalone declarables of the
65
+ mapping table below), the string-keyed providers and both `forRoot()` payloads (into one
66
+ `provideSmartNgClient(config, ...features)` call), the redundant core services in the root
67
+ providers array, `this.useQueryLists = true` / `handleDataChangeSubscriptions()`,
68
+ `[parentSmartComponent]` and the dead `[parent]` inputs,
69
+ `execute(x.uiAction, x)` → `execute(x)`, the `async` / `await` around
70
+ `getActionDescriptor`, the `SmartdialogService` `super(…)` call and its constructor
71
+ parameters, and PrimeNG module imports **no template in your repository renders**.
72
+
73
+ **What it refuses to guess at**, printing `file:line` and the section here that explains it:
74
+ `UseUiAction` → `UiActionExecutor`, `this.uiActionModels =`, `entry.cssClass =`, `addForm()`,
75
+ the 4th argument of `addGrid()`, `getActionDescriptor(…).then(…)`, the moment adapter,
76
+ component-level providers, every remaining `primeng/*` import, and `::ng-deep` selectors that
77
+ reach into the form widget.
78
+
79
+ **What it deliberately does not do:** convert *your* components to standalone. That is
80
+ Angular's own schematic (`ng generate @angular/core:standalone`) and it is not required — an
81
+ NgModule's `imports:` array takes standalone components, which is exactly what the codemod
82
+ puts there. Do the library migration first, get the app running, then decide about standalone
83
+ separately.
84
+
85
+ Afterwards: run your formatter, swap the date packages ([Dates §1](#1-swap-the-peer-dependency)),
86
+ build (expect one `NG8001` per template element whose component is not imported yet — that
87
+ loop terminates), and **smoke the app**, because the DI changes are invisible to the compiler.
88
+
89
+ ## Breaking changes
90
+
91
+ ### Dialog stack: PrimeNG dynamicdialog → MatDialog (Phase 1.1)
92
+
93
+ - `SmartdialogService` and `SmartViewContextDialogService` constructors no longer
94
+ take a PrimeNG `DialogService` parameter. If your host extends or instantiates
95
+ these services, drop the argument (MatDialog is injected internally).
96
+
97
+ Both arguments PrimeNG contributed are gone, so the base constructor lost two of
98
+ its four parameters:
99
+
100
+ ```ts
101
+ // 6.x
102
+ constructor(dialog: MatDialog, dialogService: DialogService, inject: Injector,
103
+ @Inject(COMPONENT_LIBRARY) compLib: ComponentLibrary) {
104
+ super(dialog, dialogService, inject, compLib);
105
+ }
106
+ // 7.0
107
+ constructor(dialog: MatDialog, inject: Injector) {
108
+ super(dialog, inject);
109
+ }
110
+ ```
111
+
112
+ **The codemod does this** (rule `smartdialog-super`), including the constructor
113
+ parameters and the now-unused `primeng/dynamicdialog` import — except where a
114
+ dropped parameter carries an accessibility modifier (`protected dialogService: …`),
115
+ which declares a class *property* the body may still read. Those are reported.
116
+ - The `ComponentLibrary` switch no longer has a PrimeNG branch in the dialog
117
+ layer — all dialogs open via `MatDialog` regardless of `COMPONENT_LIBRARY`.
118
+
119
+ ### smart-form widgets: PrimeNG removed (Phase 1.2)
120
+
121
+ - `ComparableDropdownDirective` and `ComparableMultiselectDirective` are removed
122
+ (they decorated the PrimeNG `p-dropdown` / `p-multiSelect` elements, which no
123
+ longer exist). Use `SmartformwidgetComponent.compareItems` semantics via
124
+ `mat-select [compareWith]`, which the widget template already wires up.
125
+ - `SmartformwidgetComponent` constructor no longer takes the injected
126
+ `COMPONENT_LIBRARY` token; the widget renders Material-only.
127
+ - `SmartformwidgetComponent` removed PrimeNG-only public members:
128
+ `errorMessages`, `getControlMessages()`, `onPrimeRichTextEditorContentChanged()`,
129
+ `safeDropDownBlur()`/`onDropDownShow()`/`onDropDownInput()`,
130
+ `safeCalendarBlur()`/`onCalendarShow()`/`onCalendarInput()`, `defaultDate`.
131
+ - `SmartFormService.setValuesFromModel()` / `toFormGroup()` /
132
+ `createFormControls()` no longer take a `ComponentLibrary` parameter.
133
+ - `SmartViewContextModule` no longer imports the PrimeNG form modules
134
+ (Dropdown, MultiSelect, Calendar, Chips, InputSwitch, Checkbox, InputNumber,
135
+ InputText, InputTextarea, InputMask, FloatLabel, Messages) and no longer
136
+ provides `Dropdown` / `MultiSelect`.
137
+
138
+ ### fileupload: PrimeNG removed (Phase 1.4)
139
+
140
+ - `SmartFileUploaderComponent` (selector `smart-file-uploader4sc`) is removed
141
+ from the public API. It was a PrimeNG `p-fileUpload` wrapper declared in
142
+ `SmartNgClientModule` but referenced nowhere; the Material-style
143
+ `SmartfileuploaderComponent` (selector `smartfileuploader`) is the uploader.
144
+ - The internal `PrimeFileUploaderComponent` (selector `prime-file-uploader`)
145
+ is removed; `smart-upload-widget` always renders `smartfileuploader`.
146
+ - `UploadWidgetComponent` constructor no longer takes the injected
147
+ `COMPONENT_LIBRARY` token, and its PrimeNG-only members are gone
148
+ (`fileUploadPrime`, `uploadFiles()`).
149
+ - `SmartNgClientModule` and `SmartViewContextModule` no longer import the
150
+ PrimeNG `FileUploadModule`; `SmartViewContextModule` also dropped the dead
151
+ `OverlayPanelModule` import (Phase 1.3).
152
+
153
+ ### editor: Quill 1.3 → 2, quill-emoji removed (Phase 1.5)
154
+
155
+ - Peer dependencies changed: `quill` `^1.3.7` → `^2.0.3`, `ngx-quill`
156
+ `16.2.1 - 24.0.5` → `^25.3.3`. Hosts must bump both together (ngx-quill 25.x
157
+ is the Quill 2 line for Angular 17; later Angular hops will raise it further).
158
+ - `quill-emoji` peer dependency is **removed** (dead, Quill 1-only, unmaintained).
159
+ If a host needs emoji support, pick a Quill 2-compatible module on its own.
160
+ - `@types/quill` must be removed from host devDependencies — Quill 2 ships its
161
+ own TypeScript types, and the stale `@types/quill` 1.x conflicts with them.
162
+ - The bundled `quill.snow.css` (imported by the smart-form widget) is now the
163
+ Quill 2.0.3 stylesheet; hosts that import `quill/dist/quill.snow.css`
164
+ themselves must serve the Quill 2 version.
165
+ - `SmartViewContextModule` no longer imports the PrimeNG `EditorModule`
166
+ (`<p-editor>` died with Phase 1.2; RICH_TEXT renders via ngx-quill).
167
+
168
+ ### smart-diagram: p-chart replaced by direct chart.js (Phase 1.6)
169
+
170
+ - `SmartDiagramComponent` renders its own `<canvas>` and instantiates chart.js
171
+ directly; the PrimeNG `<p-chart>` wrapper (and the `ChartModule` import in
172
+ `SmartDiagramModule`) is gone. Chart.js controllers are now registered by the
173
+ component itself (`Chart.register(...registerables)`), so no `chart.js/auto`
174
+ import is needed anywhere.
175
+ - The public `chart` property (and `getChart()`) is now the chart.js `Chart`
176
+ instance instead of the PrimeNG `UIChart` component. `getBase64Image()` and
177
+ `refresh()` keep their signatures (delegating to `toBase64Image()` /
178
+ `update()`).
179
+ - `SmartDiagramComponent` constructor no longer takes the injected
180
+ `COMPONENT_LIBRARY` token; the component has a single render path.
181
+ - DOM/CSS hooks changed: the internal structure is
182
+ `div.chart-host > div.chart-container > canvas`; host styles targeting
183
+ `::ng-deep p-chart` no longer match. The aspect-ratio classes
184
+ (`default-aspect-ratio` / `pie-aspect-ratio`) are now applied on
185
+ `.chart-host` and size `.chart-container`.
186
+
187
+ ### smart-grid: PrimeNG branch removed (Phase 1.7)
188
+
189
+ - `SmartGridComponent` renders Material-only. The `@if (compLib === PRIMENG)`
190
+ template branch (the inline `p-table` / `p-paginator` / `p-menu` /
191
+ `p-multiSelect` grid) is gone; the grid always renders through the PrimeNG-free
192
+ `smart-table` (the `#table` slot), `mat-tree`, `app-smart-grid-card`, and
193
+ `mat-paginator`.
194
+ - `SmartGridComponent` constructor no longer takes the injected
195
+ `COMPONENT_LIBRARY` token, nor the `SmartDatePipe` / `SmartDateTimePipe` /
196
+ `SmartTimePipe` (they only fed the removed inline `p-table` cell renderer);
197
+ those pipes are dropped from the component `providers` too.
198
+ - Removed PrimeNG-only public members: `columns`, `menuButtons`, `menu`,
199
+ `onOptionsClick()`, `previousmultiSortMeta`, `gridSort()`, `lazyLoad()`,
200
+ `headerChange()`, `onColOrder()`, `getOrderColumNames()`, `getColValue()`,
201
+ `onPrimeChangePage()`, `onRowSelect()`, `onRowUnselect()`, `onSelectAllRow()`,
202
+ `getImageResourceIcons()`, `getImageResourceStyle()`, `getRowColumnAction()`,
203
+ `showCellToolbar()`, `shouldShowOptionsButton()`, `calculateMenuActions()`,
204
+ `createCellToActionMap()`, `getRowMenuActionModelArray()`, `rowTrackByFn()`,
205
+ `cellToActionMap`, `columnMetaByName`, and the `_headerToolbar`
206
+ (`#headerToolbar`) view child. The `headerToolbar` getter now returns the
207
+ smart-table's header toolbar unconditionally (its only remaining consumer,
208
+ `SmartComponentApiClient`, is unaffected).
209
+ - `SmartGridModule` no longer imports the PrimeNG `TableModule`, `ButtonModule`,
210
+ `MenuModule`, `PaginatorModule`, `MultiSelectModule`; `MatTooltipModule` is
211
+ added (the grid refresh button is now a Material `mat-icon-button` +
212
+ `matTooltip` instead of `pButton` + `pTooltip`).
213
+ - Note: this is the PrimeNG-branch deletion only. The `smart-grid` internals
214
+ rewrite to `mat-table` + `MatPaginator` and the `smart-grid`/`smart-table`
215
+ merge remain a Phase 3 (post-Angular-22) change.
216
+
217
+ ### misc widgets: PrimeNG branches removed (Phase 1.8)
218
+
219
+ The remaining components that still carried a dead PrimeNG branch alongside an
220
+ already-active Material one are now Material-only. In every case the Material
221
+ branch was the rendered path under `COMPONENT_LIBRARY = MATERIAL` (the value all
222
+ 7.0 hosts use), so this is render-neutral; the `COMPONENT_LIBRARY` token, where
223
+ these components still injected it, is now ignored by them.
224
+
225
+ - `ExpandableSectionComponent` renders Material-only (the `p-accordion` branch is
226
+ gone; always `mat-expansion-panel`). Its constructor no longer takes the
227
+ injected `COMPONENT_LIBRARY` token. `SmartExpandableSectionModule` no longer
228
+ imports the PrimeNG `AccordionModule`.
229
+ - `UiActionButtonComponent` renders Material-only (the `pButton`/`pRipple` branch
230
+ is gone; always `mat-button`). Its constructor no longer takes the optional
231
+ `COMPONENT_LIBRARY` token (it previously defaulted to `PRIMENG` when unset).
232
+ `getType()` no longer has a PrimeNG class-map branch — it returns the
233
+ `mat-mdc-*` class strings unconditionally. (The dynamic `getbtnClass()`'s
234
+ `p-button-<color>` class token was later dropped in Phase 1.9.)
235
+ - `UiActionToolbarComponent` constructor no longer takes the injected
236
+ `COMPONENT_LIBRARY` token. The dead `getType()` / `getbtnClass()` methods
237
+ (never referenced by the template) were removed, and the scroll-affordance
238
+ buttons use the Material icons `arrow_back` / `arrow_forward` unconditionally
239
+ (the PrimeNG `chevron-left` / `chevron-right` fallback is gone).
240
+ - `UiActionConfirmDialogComponent` and `UiActionInputDialogComponent` render
241
+ their close button as a Material `mat-icon-button` unconditionally (the `@else`
242
+ `p-button` branch is gone); both constructors no longer take the injected
243
+ `COMPONENT_LIBRARY` token.
244
+ - `SmartIconModule` no longer imports the PrimeNG `BadgeModule` (badges render
245
+ via the custom `ui-badge` component, unaffected).
246
+ - `SmartViewContextModule` no longer imports the PrimeNG `ButtonModule`,
247
+ `TooltipModule`, `ToastModule`, `ImageModule`, or `OrderListModule` (all
248
+ unused after the branch deletions). The `primeng/api` `SharedModule`
249
+ (`PrimeSharedModule`) import was later dropped in Phase 1.9's final sweep.
250
+
251
+ ### PrimeNG package dependency removed (Phase 1.9)
252
+
253
+ The `primeng` npm package is gone. Most importantly it is dropped from
254
+ `@smartbit4all/ng-client`'s own **`peerDependencies`** (in
255
+ `projects/smart-ng-client/package.json`, which flows into the published
256
+ `npms/smart-ng-client/package.json`) — that entry is what previously forced every
257
+ consuming host to install PrimeNG. It is also removed from the dev workspace
258
+ (`package.json`, `package-lock.json`, `node_modules`), and both PrimeNG theme
259
+ stylesheets (`primeng/resources/themes/saga-blue/theme.css`,
260
+ `primeng/resources/primeng.min.css`) are removed from the app-playground `styles`
261
+ in `angular.json`. Hosts that still list `primeng` as a dependency or load its
262
+ theme CSS should remove both; 7.0 renders entirely on Material/CDK.
263
+
264
+ #### Your own PrimeNG usage
265
+
266
+ Removing the peer dependency does not remove PrimeNG from *your* code, and the measured
267
+ hosts carry more of it than their authors expect — mostly as **dead module imports**. Across
268
+ the 12 hosts, `DialogService` is imported 56 times, but 8 of those in p014 turned out to be
269
+ nothing but the base-constructor argument above, and four PrimeNG modules in its
270
+ `app.module.ts` rendered nothing at all.
271
+
272
+ So the codemod separates the two cases, and the separation is decidable rather than
273
+ guessed:
274
+
275
+ - **`primeng-modules`** — a PrimeNG NgModule in an `imports:` array is **deleted** when none
276
+ of its selectors appears in any template in the repository. The scan runs over `.html`
277
+ *and* `.ts` (for inline templates) before anything is rewritten, and it distinguishes an
278
+ element (`<p-tree>`) from an attribute directive (`pTooltip`) so that a leftover
279
+ `class="p-button"` is not read as a component still in use. What the scan found is printed
280
+ in the header, because it is what licenses the deletions.
281
+ - **`primeng-imports`** — everything else from `primeng/*` is reported with its replacement.
282
+ These are the real ports.
283
+
284
+ The replacements, for what the hosts actually import:
285
+
286
+ | PrimeNG | Material / CDK |
287
+ |---|---|
288
+ | `p-button` / `pButton` | `MatButtonModule` — `mat-button`, `mat-icon-button` |
289
+ | `p-tree` | the library's `<smart-tree>`, or `MatTreeModule` |
290
+ | `p-progressSpinner` | `MatProgressSpinnerModule` — `<mat-spinner>` |
291
+ | `pTooltip` | `MatTooltipModule` (`matTooltip`), or `SmartTooltipDirective` |
292
+ | `p-sidebar` | `MatSidenavModule` |
293
+ | `pInputText` | `MatInputModule` — `matInput` inside `<mat-form-field>` |
294
+ | `p-fieldset` | `MatCardModule`, or `<fieldset>` + `<mat-divider>` |
295
+ | `p-divider` | `MatDividerModule` |
296
+ | `p-accordion` | `MatExpansionModule`, or `<smart-expandable-section>` |
297
+ | `p-table` | the library's `<smart-grid>`, or `MatTableModule` |
298
+ | `p-editor` | ngx-quill's `<quill-editor>` (already a library dependency) |
299
+ | `p-badge` / `pBadge` | `MatBadgeModule`, or `UiBadgeComponent` / `UiBadgeDirective` |
300
+ | `p-menu`, `p-overlayPanel` | `MatMenuModule`, or the CDK Overlay |
301
+ | `p-inputSwitch` | `MatSlideToggleModule` |
302
+ | `p-dialog` | `MatDialog` — open a component instead of toggling `[visible]` |
303
+ | `DialogService` | `MatDialog`. There is **no `header` option**: put `<h2 mat-dialog-title>` in the component |
304
+ | `DynamicDialogRef` | `MatDialogRef` — `afterClosed()` replaces `onClose` |
305
+ | `DynamicDialogConfig` | `MAT_DIALOG_DATA` for the data, `MatDialogRef` for the rest |
306
+ | `PrimeNGConfig` | **delete it.** `setTranslation()` of day and month names is already covered by `MAT_DATE_LOCALE` + `SmartDateFnsAdapter` — the date-fns `hu` locale carries both the names and the Monday week start. **Confirmed on a running host** (p014, 2026-07-29, against a live backend): the datepicker opens on `2026. JÚL.` with `H K Sz Cs P Sz V`, i.e. Hungarian and Monday-first, exactly what the deleted `firstDayOfWeek: 1` + `dayNamesMin` used to buy. See [Dates](#dates-moment--date-fns-and-the-timezone-contract-phase-38) |
307
+ | `MessageService` | `MatSnackBar` |
308
+ | `ConfirmationService` | the library's `UiActionConfirmDialogService` |
309
+ | `MenuItem` | the library's `UiActionModel`, or a plain `<mat-menu>` item |
310
+ | `SharedModule` (`primeng/api`) / `PrimeTemplate` | a plain `<ng-template>` — PrimeNG needed `pTemplate`, Material does not |
311
+
312
+ `primeicons` is a separate package. Nothing in 7.0 needs it, but nothing breaks if you keep
313
+ it either — `pi pi-*` class names are just a font.
314
+
315
+ ### `QuillModule.forRoot()` in your root module
316
+
317
+ `provideSmartNgClient()` calls `importProvidersFrom(QuillModule.forRoot())` itself — ngx-quill
318
+ has no provider function, so the module is the only way to reach its root configuration. A
319
+ root module that also calls it is configuring quill twice, so the codemod removes the call
320
+ and its import. In a *feature* module the same call is that module's own configuration and is
321
+ left alone.
322
+
323
+ **Keep `ngx-quill` and `quill` in your `package.json`.** They are peer dependencies of the
324
+ library — the host provides them. npm installs peers automatically, which is why hosts have
325
+ got away with not declaring them, but any install that runs with `--legacy-peer-deps` drops
326
+ them silently and the next build fails on an import that has always worked. Two of the twelve
327
+ measured hosts were in exactly that state.
328
+
329
+ **7.0 adds four peer dependencies 6.x did not have**, and they bite in a way the TypeScript
330
+ errors do not prepare you for:
331
+
332
+ ```bash
333
+ npm install ngx-mask@^22.0.0 fast-equals@^5.0.1 wavesurfer.js@^7.9.5 @angular/youtube-player@^22.0.0
334
+ ```
335
+
336
+ If you install the library the normal way, npm brings these in for you. If you *swap a local
337
+ build into `node_modules` by hand* — which is how you would try the library before it is
338
+ published — npm never runs, so nothing installs them. The symptom arrives **after** the last
339
+ TypeScript error is fixed, as five `Module not found` errors reported against the library's
340
+ own `fesm2022` bundle rather than against any file of yours:
341
+
342
+ ```
343
+ ./node_modules/@smartbit4all/ng-client/fesm2022/smartbit4all-ng-client.mjs:7:0-60 -
344
+ Error: Module not found: Error: Can't resolve 'ngx-mask'
345
+ ```
346
+
347
+ Four packages, five errors: `wavesurfer.js` is missing twice, once for its `record` plugin.
348
+
349
+ - `UiActionButtonComponent.getbtnClass()` no longer emits the dead
350
+ `p-button-<color>` class token — it returns only `sb4-<color>` (nothing styled
351
+ `.p-button-*`; the platform button theme keys off `sb4-<color>` in
352
+ `custom-theme.scss`). Hosts with their own `.p-button-<color>` overrides for
353
+ sb4 action buttons should migrate them to `.sb4-<color>`.
354
+ - `SmartViewContextModule` no longer imports the `primeng/api` `SharedModule`
355
+ (`PrimeSharedModule`); no template in the module used `pTemplate`.
356
+ - `SmartComponentLayoutModule` no longer imports `InputGroupModule` /
357
+ `InputGroupAddonModule` (they were dead — no `p-inputGroup` template existed).
358
+
359
+ The `ComponentLibrary` enum still exists with only its `MATERIAL` member in
360
+ active use; its `PRIMENG` member and the remaining dead `ComponentLibrary.PRIMENG`
361
+ code branches (icon/menu/widget components, dead `.p-*`/`.pi` CSS) are removed in
362
+ a follow-up internal-cleanup step (1.10) — they are not a package dependency and
363
+ do not affect hosts. `deviceInfo.componentLibrary` continues to report
364
+ `material` to the backend.
365
+
366
+ ### ComponentLibrary / COMPONENT_LIBRARY removed entirely (Phase 1.10)
367
+
368
+ 7.0 is Material-only, so the `ComponentLibrary` abstraction (which had no second
369
+ implementation once PrimeNG was gone) is deleted outright.
370
+
371
+ - **Host action required:** `ComponentLibrary` (enum) and `COMPONENT_LIBRARY`
372
+ (injection token) are no longer exported from `@smartbit4all/ng-client`. Hosts
373
+ that provide `{ provide: COMPONENT_LIBRARY, useValue: ComponentLibrary.MATERIAL }`
374
+ in their root providers must **remove that provider and its import** — it now
375
+ fails to compile (no exported member). No replacement is needed; every component
376
+ renders Material-only.
377
+ - Every `@Inject(COMPONENT_LIBRARY)` constructor argument is gone from the library
378
+ (smart-icon, click/hover tiered menu, photo-capture / voice-record widgets,
379
+ smart-voice-recorder, smart-file-editor, smart-multi-file-editor, sortable
380
+ widget, smart-filter-editor-content, validation-result-page, message-dialog, the
381
+ dialog services `SmartdialogService` / `SmartViewContextDialogService` /
382
+ `SmartViewContextErrorDialogService` / the ui-action confirm/input/file-upload
383
+ dialog services, and `PdfViewerDialogService` in `@smartbit4all/document-explorer`).
384
+ The `SmartdialogService` base constructor is now `(dialog, injector)` — subclasses
385
+ that called `super(dialog, dialogService, injector, compLib)` keep the first and
386
+ third arguments only. See [Phase 1.1](#dialog-stack-primeng-dynamicdialog--matdialog-phase-11);
387
+ the codemod rewrites it.
388
+ - `SmartViewContextService` no longer exposes the public `componentLibrary` field
389
+ and no longer injects the token; it now writes the literal `'material'` into
390
+ `deviceInfo.componentLibrary` on every view-context update (the backend keys its
391
+ icon set on this string — `images-playground.properties` has `.material=` vs
392
+ `.primeng=` variants — so the value keeps flowing; the DTO field is unchanged).
393
+ - All dead `ComponentLibrary.PRIMENG` branches and their PrimeNG icon names
394
+ (`chevron-right`, `pi pi-*`, `power-off`, `trash`, `file`, `exclamation-circle`,
395
+ `play-circle`/`stop-circle`, etc.) are removed; the Material icon path is now
396
+ unconditional. Dead `.p-*` / `.pi` CSS rules were purged from the library
397
+ (smart-icon, smartform, smartformwidget, the file editors/uploader,
398
+ ui-action-file-upload-dialog) and from the app-playground `custom-theme.scss`;
399
+ the app-playground `index.html` no longer loads the `primeicons` CDN stylesheet.
400
+ - After 1.10, `grep -rin primeng projects` reports a single residual: the
401
+ auto-generated `deviceInfo.ts` DTO doc comment (`(Material, PrimeNg)`), which
402
+ mirrors the backend OpenAPI field description and is regenerated from it — left
403
+ untouched by design (the field itself is intentionally kept).
404
+
405
+ ### Angular 18 hop (Phase 2, 17 → 18)
406
+
407
+ The library line now builds against Angular 18. Host-relevant changes:
408
+
409
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
410
+ `@angular/*` `^18.0.0`, `@angular/youtube-player` `^18.2.14` (was lagging on
411
+ `^16` — its `^16 || ^17` peer range breaks a strict `npm install` on Angular
412
+ 18, so it can no longer lag), `ngx-quill` `^26.0.0` (the Angular 18 line;
413
+ quill stays `^2.0.3`), `ngx-mask` `^18.0.0`, and — for
414
+ `@smartbit4all/document-explorer` — `ngx-extended-pdf-viewer` `^21.0.0`
415
+ (the 19.x/20.x lines cap their Angular peer below 18).
416
+ - **`parchment` hoisting quirk (ngx-quill 26):** the ngx-quill 26 typings
417
+ reference `import("parchment")`, but npm may leave `parchment` nested under
418
+ `node_modules/quill/` (even after `npm dedupe`), which fails the host build
419
+ with `TS2307: Cannot find module 'parchment'`. Fix: add
420
+ `"parchment": "^3.0.0"` to the host devDependencies so it is hoisted to the
421
+ top level (quill 2 depends on parchment 3, so versions cannot conflict).
422
+ - **Material theming (M2 compat APIs):** the `ng update @angular/material@18`
423
+ schematic rewrites M2 theme SCSS to the prefixed compat API
424
+ (`mat.define-palette` → `mat.m2-define-palette`,
425
+ `mat.$indigo-palette` → `mat.$m2-indigo-palette`,
426
+ `mat.define-light-theme` → `mat.m2-define-light-theme`, …). The theme stays
427
+ visually **M2** — no M3 switch happens at this hop; hosts running the
428
+ schematic get this rewrite automatically.
429
+ - **`HttpClientModule` deprecation:** the `ng update @angular/core@18`
430
+ migration replaces `HttpClientModule` imports with
431
+ `provideHttpClient(withInterceptorsFromDi())`. Not a library breaking
432
+ change (the lib's generated API services were migrated internally), but
433
+ hosts will get the same automatic migration in their own modules.
434
+ - TypeScript requirement raised to `>=5.4` (the repo builds on 5.5).
435
+
436
+ ### Angular 19 hop (Phase 2, 18 → 19)
437
+
438
+ The library line now builds against Angular 19. Host-relevant changes:
439
+
440
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
441
+ `@angular/*` `^19.0.0`, `@angular/youtube-player` `^19.0.0`,
442
+ `ngx-quill` `^27.0.0` (the Angular 19 line; quill stays `^2.0.3`),
443
+ `ngx-mask` `^19.0.0`, and — for `@smartbit4all/document-explorer` —
444
+ `ngx-extended-pdf-viewer` `^22.0.0` (the 21.x line caps its Angular peer
445
+ below 19).
446
+ - **ngx-extended-pdf-viewer 22 input rename:** the `[showScrollingButton]`
447
+ input no longer exists — it is `[showScrollingButtons]` (plural,
448
+ `ResponsiveVisibility`). Hosts that use `<ngx-extended-pdf-viewer>` directly
449
+ must rename the binding, otherwise the template fails with NG8002.
450
+ - **Standalone-by-default:** in Angular 19 components/directives/pipes are
451
+ standalone unless declared otherwise. The `ng update @angular/core@19`
452
+ migration automatically adds `standalone: false` to every NgModule-declared
453
+ declarable in the host codebase — run it and review the (large, mechanical)
454
+ diff. Declarables not referenced by any NgModule are skipped by the
455
+ schematic.
456
+ - **Material theming:** the `ng update @angular/material@19` schematic splits
457
+ `@include mat.core()` into `@include mat.elevation-classes()` +
458
+ `@include mat.app-background()`. No visual change; hosts running the
459
+ schematic get this rewrite automatically.
460
+ - **New NG8111 extended diagnostic** (warning): "Function in event binding
461
+ should be invoked". Uninvoked event bindings like
462
+ `(click)="(someCallback)"` — previously silently dead — are now flagged at
463
+ build time. Warning only, the build stays green.
464
+ - TypeScript requirement stays satisfied by 5.5 (Angular 19 supports
465
+ TS 5.5–5.8). `ng update @angular/core@19` can install **TS 5.9** all the same, and the
466
+ build then stops with "requires TypeScript >=5.5.0 and <5.9.0"; pin `typescript ~5.8.3`
467
+ (p009-angular, 2026-10-27).
468
+ - TS 5.8 reports two dead constructs as errors: **TS2869** on a `` `${x}` ?? '' `` (a template
469
+ literal is never nullish) and **TS2353** on a property the widget type does not have, e.g.
470
+ `direction` on a `SELECT` widget. Both never did anything; delete them (p034).
471
+
472
+ ### Angular 20 hop (Phase 2, 19 → 20)
473
+
474
+ The library line now builds against Angular 20. Host-relevant changes:
475
+
476
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
477
+ `@angular/*` `^20.0.0`, `@angular/youtube-player` `^20.0.0`,
478
+ `ngx-quill` `^28.0.0` (the Angular 20 line; quill stays `^2.0.3`),
479
+ `ngx-mask` `^20.0.0`, and — for `@smartbit4all/document-explorer` —
480
+ `ngx-extended-pdf-viewer` `^23.0.0` (the 22.x line caps its Angular peer
481
+ below 20). No pdf-viewer API rename at this bump (unlike 22.x).
482
+ - **TypeScript 5.9 required** (Angular 20 supports 5.8–5.9); `ng update`
483
+ bumps it automatically. TS 5.9 adds **TS2872 "This kind of expression is
484
+ always truthy"**, which turns previously-silent dead `||` alternatives into
485
+ build errors — expect one or two in any older codebase.
486
+ - **Material 20 button labels no longer set `white-space: nowrap`.** Material
487
+ 20 dropped the MDC stylesheet (`@material/button`), which used to carry it.
488
+ Multi-word button labels now wrap, and any rule that lets the button size to
489
+ its content (the lib's `.mdc-button { height: fit-content }`) makes the
490
+ button grow taller. The lib restores the pre-20 look inside
491
+ `ui-action-button` (`.mdc-button__label { white-space: nowrap }`); **hosts
492
+ that render Material buttons of their own must add the same rule** if they
493
+ relied on single-line labels (typical symptom: a navigation bar that
494
+ suddenly wraps and overflows into scroll arrows).
495
+ - **Material 20 renamed the MDC-era CSS custom properties**: e.g.
496
+ `--mdc-text-button-container-height` → `--mat-button-text-container-height`.
497
+ The `ng update @angular/material@20` schematic rewrites the ones it finds in
498
+ `.css`/`.scss` files; token names embedded in TS strings or host templates
499
+ must be renamed by hand.
500
+ - **`DOCUMENT` moved from `@angular/common` to `@angular/core`.** Automatic
501
+ migration; `@angular/common`'s re-export is deprecated.
502
+ - **`moduleResolution: "node"` → `"bundler"`** in `tsconfig.json` (and any
503
+ `tsconfig.lib.json` that sets it explicitly). Automatic migration; it changes
504
+ how sub-path exports resolve, so a host with hand-written `paths` entries
505
+ should re-verify its build. A deep import such as
506
+ `import { UiAction } from '@smartbit4all/ng-client/projects'` stops resolving
507
+ (TS2307); import from `@smartbit4all/ng-client` instead. A 4.5-era deep import of a
508
+ generated model (`@smartbit4all/document-explorer/lib/api/...`,
509
+ `@smartbit4all/ng-client/lib/smart-generic-pages/api/...`) breaks the same way and has no
510
+ root export: take the host's own generated model, or derive the type from the field it
511
+ copies (`AdminTemplate['file']`) (p034).
512
+ - **A host without `skipLibCheck` that still imports `primeng/dynamicdialog` stays red from
513
+ this hop**: PrimeNG 17's typings name `InjectFlags`, which Angular 20 removed (TS2305 inside
514
+ `node_modules/primeng`). The 6.x `SmartdialogService` subclasses need that import until the
515
+ codemod removes it, so this hop joins 21 and 22 as red until the 7.0 library is in (p034).
516
+ - **Workspace generation defaults**: the CLI adds a `schematics` block to
517
+ `angular.json` pinning the pre-20 file-naming style (`type: "component"`,
518
+ `typeSeparator: "."`). Cosmetic — it only affects newly generated files.
519
+ - Optional migrations **not** run here (deferred to the modernization phase):
520
+ `use-application-builder` (esbuild), `control-flow-migration`,
521
+ `router-current-navigation`.
522
+ - The `InjectFlags`, `TestBed.get` → `TestBed.inject` and
523
+ `provideServerRendering` migrations were no-ops for this codebase.
524
+ - The NG8111 warnings introduced at the 19 hop are no longer emitted by the
525
+ Angular 20 compiler (the dead bindings themselves are unchanged).
526
+
527
+ ### Angular 21 hop (Phase 2, 20 → 21)
528
+
529
+ The library line now builds against Angular 21. Host-relevant changes:
530
+
531
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
532
+ `@angular/*` `^21.0.0`, `@angular/youtube-player` `^21.0.0`,
533
+ `ngx-quill` `^29.0.0` (the Angular 21 line; quill stays `^2.0.3`),
534
+ `ngx-mask` `^21.0.0`, and — for `@smartbit4all/document-explorer` —
535
+ `ngx-extended-pdf-viewer` `^25.0.0` (the 24.x line still caps its Angular
536
+ peer below 21). No pdf-viewer API rename at this bump.
537
+ - **`ngx-quill` 30.x is intentionally *not* used.** 30.x is the "zoneless"
538
+ line and drops the `zone.js` peer dependency; the 7.0 lib is still
539
+ zone-based. Hosts that have already gone zoneless may use 30.x, but the
540
+ lib is only validated against 29.x.
541
+ - **TypeScript stays 5.9** and **zone.js stays 0.15** (Angular 21 accepts
542
+ `~0.15.0 || ~0.16.0`). **Node** must satisfy
543
+ `^20.19.0 || ^22.12.0 || >=24.0.0`.
544
+ - **`MatCommonModule` is removed from `@angular/material/core` (breaking).**
545
+ It was already deprecated in Material 20 (`@breaking-change 21.0.0`). Any
546
+ host NgModule that imports it will fail to compile with
547
+ `Unknown reference` — and because a broken NgModule cascades, the real
548
+ error usually shows up as a flood of `NG8001: '<some-component>' is not a
549
+ known element` / `NG6002: … does not appear to be an NgModule class`
550
+ further down the build log. **Fix: just delete the import and the array
551
+ entry.** `MatCommonModule` had no template surface; it only
552
+ (a) applied the `cdk-high-contrast-*` body classes — `A11yModule` still
553
+ does this, and Material components that need it import `A11yModule`
554
+ themselves — and (b) re-exported `BidiModule`. If your host actually uses
555
+ the `Dir` directive or injects `Directionality`, import
556
+ `BidiModule` from `@angular/cdk/bidi` explicitly.
557
+ The lib dropped it from all 10 of its own modules.
558
+ - **`provideZoneChangeDetection()` is now added at bootstrap** by the
559
+ `ng update` migration, e.g.
560
+ `bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()] })`.
561
+ This keeps zone-based change detection explicit; behaviour is unchanged.
562
+ - **The control-flow migration (`*ngIf` → `@if`) now runs as a *mandatory*
563
+ `ng update` migration**, not an optional one. It rewrites every structural
564
+ directive in the workspace. The lib **reverted** it to keep the version hop
565
+ reviewable — `NgIf`/`NgForOf`/`NgSwitch` still ship in Angular 21, so
566
+ `*ngIf`/`*ngFor` keep working. Hosts may keep or revert it as they prefer;
567
+ if you want it as a separate change, run
568
+ `npx ng generate @angular/core:control-flow` on its own commit.
569
+ - **New `NG8107` extended diagnostic** (warning): an optional chain `?.`
570
+ whose left side can no longer be `null`/`undefined` under Angular 21's
571
+ sharpened template inference. Warning only — expect a batch of them in any
572
+ older template set.
573
+ - **`tsconfig.json` loses its explicit `lib` array** (the CLI derives it from
574
+ `target`). Automatic migration.
575
+ - The Material and CDK v21 migration schematics made no source changes, and
576
+ the `Router.lastSuccessfulNavigation` migration was a no-op. The optional
577
+ `router-current-navigation` migration was not run.
578
+ - The `.mdc-button__label { white-space: nowrap }` workaround introduced at
579
+ the Angular 20 hop is **still required on Material 21** — the class still
580
+ exists in the button template and Material still ships no `white-space`
581
+ rule for it.
582
+
583
+ ### Angular 22 hop (Phase 2, 21 → 22) — final hop
584
+
585
+ The library line now builds against **Angular 22**. This is the biggest
586
+ host-facing hop of the series: it changes the **change-detection default**, the
587
+ **HTTP backend default** and the **meaning of `?.` in templates**. All three are
588
+ handled by `ng update` migrations, but you must let them run.
589
+
590
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
591
+ `@angular/*` `^22.0.0`, `@angular/youtube-player` `^22.0.0`,
592
+ `ngx-quill` `^31.0.0` (the Angular 22 line; quill stays `^2.0.3`),
593
+ `ngx-mask` `^22.0.0`, and — for `@smartbit4all/document-explorer` —
594
+ `ngx-extended-pdf-viewer` `^28.0.0` (25/26/27 all cap their Angular peer
595
+ below 22). **No pdf-viewer API rename at this bump.**
596
+ `@ngx-translate/core` `^14` still works (its peer is `>=13`).
597
+ - **TypeScript must be 6.0** (`@angular/compiler-cli` peers `>=6.0 <6.1`);
598
+ **zone.js stays 0.15** (`~0.15.0 || ~0.16.0`) and **rxjs is unchanged**.
599
+ **Node** must satisfy `^22.22.3 || ^24.15.0 || >=26.0.0` — note the
600
+ **22.22.3** floor, which is stricter than Angular 21's `^22.12.0`.
601
+
602
+ #### 1. `ChangeDetectionStrategy.OnPush` is the new default
603
+
604
+ In Angular 22 the enum is redefined: `OnPush = 0` (default), `Eager = 1`, and
605
+ `Default = 1` is kept as a **deprecated alias of `Eager`**. Any component
606
+ without an explicit `changeDetection` therefore switches from CheckAlways to
607
+ OnPush — a real behaviour change for components that mutate their own state
608
+ without signals or `markForCheck()`.
609
+
610
+ An `ng update` schematic **automatically adds
611
+ `changeDetection: ChangeDetectionStrategy.Eager` to every component that had
612
+ none**, which keeps behaviour bit-identical. The lib took exactly this route
613
+ (114 components) and did **not** opt into OnPush at this hop. Recommended host
614
+ approach: let the schematic run, verify your app, and move components to OnPush
615
+ deliberately afterwards.
616
+
617
+ Watch out for components the schematic can skip — it only touches components it
618
+ can resolve. After updating, grep for `@Component` files with no
619
+ `changeDetection` and decide about each one; a component that is dynamically
620
+ instantiated but never statically declared can slip through.
621
+
622
+ #### 2. `provideHttpClient` now defaults to the fetch backend
623
+
624
+ Angular 22 switches `HttpClient` from `XMLHttpRequest` to `fetch`. An
625
+ `ng update` migration inserts **`withXhr()`** into your `provideHttpClient(...)`
626
+ calls to preserve the old backend:
627
+
628
+ ```ts
629
+ provideHttpClient(withXhr(), withInterceptorsFromDi())
630
+ ```
631
+
632
+ The lib does this in `SmartViewContextModule`. **Hosts that call
633
+ `provideHttpClient` themselves must do the same** (or consciously move to
634
+ fetch). This matters most for **upload progress**: `reportProgress` events
635
+ behave differently on the fetch backend, so if your host reports upload
636
+ progress, keep `withXhr()` until you have retested it.
637
+
638
+ #### 3. `?.` in templates now yields `undefined`, not `null`
639
+
640
+ Angular 22's safe-navigation operator returns `undefined` where it used to
641
+ return `null`. Where that difference is observable, an `ng update` migration
642
+ wraps the expression in the **`$safeNavigationMigration()` compiler builtin**:
643
+
644
+ ```html
645
+ [ngStyle]="calcStyle($safeNavigationMigration(diagramModel?.descriptor?.style))"
646
+ ```
647
+
648
+ This is an official builtin (handled like `$any()` — the compiler unwraps it and
649
+ the type-checker sees straight through it), not a temporary marker, so wrapped
650
+ templates are safe to keep. The same `ng update` run also sets the
651
+ `nullishCoalescingNotNullable` and `optionalChainNotNullable` extended
652
+ diagnostics to `suppress` in your tsconfigs, which silences the `NG8107`
653
+ warnings the Angular 21 hop introduced.
654
+
655
+ #### 4. `ComponentFactoryResolver` is removed (breaking)
656
+
657
+ `ComponentFactoryResolver` is gone and `ComponentFactory` is no longer public
658
+ (it only survives as `ɵRender3ComponentFactory`). Code like this stops
659
+ compiling with `TS2305` / `NG2003`:
660
+
661
+ ```ts
662
+ constructor(private resolver: ComponentFactoryResolver) {}
663
+ ngAfterViewInit() {
664
+ const factory = this.resolver.resolveComponentFactory(MyComponent);
665
+ this.vcRef.createComponent(factory);
666
+ }
667
+ ```
668
+
669
+ Replace it with the type-based overload — behaviour is identical (v22 builds the
670
+ same factory internally and resolves the injector from the parent):
671
+
672
+ ```ts
673
+ ngAfterViewInit() {
674
+ this.vcRef.createComponent(MyComponent);
675
+ }
676
+ ```
677
+
678
+ **Public-API change in the lib:** `ComponentFactoryService` no longer exposes
679
+ its `factory` field. `createComponent()` / `destroyComponent()` keep their
680
+ signatures, so hosts that only call those need no change.
681
+
682
+ #### 5. TypeScript 6.0 deprecates `baseUrl` and `downlevelIteration`
683
+
684
+ TS 6.0 raises **`TS5101`** for both options ("deprecated and will stop
685
+ functioning in TypeScript 7.0"). Two ways out:
686
+
687
+ - Remove them. `downlevelIteration` is a no-op at `target: ES2022`. `baseUrl`
688
+ can only go once every non-relative first-party import (`from 'projects/…'`,
689
+ `from 'src/…'`) is relative or covered by a `paths` entry.
690
+ - Or silence them for now with `"ignoreDeprecations": "6.0"` in
691
+ `compilerOptions`. This is what the lib workspace does, because `baseUrl` is
692
+ still load-bearing there. It only buys time until TS 7.
693
+
694
+ #### 6. Other notes
695
+
696
+ - **Material 22 removed no further modules or tokens** — the CDK and Material
697
+ v22 migration schematics made no source changes. CDK 22 moves to a
698
+ package `exports` map (the per-entry-point sub-directories are gone from
699
+ `node_modules/@angular/cdk`), but every `@angular/cdk/*` import still
700
+ resolves; this only matters if you referenced those paths on disk.
701
+ - The `.mdc-button__label { white-space: nowrap }` workaround introduced at the
702
+ Angular 20 hop is **still required on Material 22** — the class still appears
703
+ in Material's button template and Material still ships no `white-space` rule
704
+ for it.
705
+ - **The control-flow migration did not re-run at this hop** (it was mandatory at
706
+ 21). The lib still ships `*ngIf`/`*ngFor` templates; `NgIf`/`NgForOf`/
707
+ `NgSwitch` continue to work in Angular 22.
708
+ - **The webpack-based builders are deprecated.** `@angular-devkit/build-angular`
709
+ now prints a notice recommending `@angular/build`. Nothing breaks yet, but
710
+ plan the switch.
711
+ - `strictTemplates` is on by default in v22; the lib workspace was already
712
+ strict, so nothing changed. If your host was not strict, the `ng update`
713
+ migration adds `"strictTemplates": false` to keep it that way.
714
+ - A `@HostListener('document:keydown.enter', ['$event'])` handler typed `KeyboardEvent` fails
715
+ with TS2345 (`Event` is not assignable); type the parameter as `Event` when the handler only
716
+ reads `target` (p034).
717
+ - **An Angular CLI 1x `src/test.ts` breaks `ng test`**: `require.context` is a webpack API the
718
+ 22 karma builder no longer provides (`__webpack_require__(...).context is not a function`,
719
+ 0 specs run). Delete the `require.context` block; the builder collects the specs from
720
+ `tsconfig.spec.json` itself (p034).
721
+
722
+ ### The third-party packages the library does *not* own (Phase 2)
723
+
724
+ Every hop section above lists the library's own peer dependencies. Your host almost
725
+ certainly carries packages the library never sees, and those have their own Angular
726
+ peer ceilings — each one can stop a hop dead. Measured on p014 (2026-07-29), which
727
+ went 17 → 22 with the library still installed:
728
+
729
+ - **`@swimlane/ngx-charts` needs a ladder of its own**: `20 → 22 → 22 → 23 → 24 → 25`
730
+ across the five hops. At the Angular 21 hop it peer-caps the CDK, and because
731
+ `ng update`'s own `npm` step is **not** `--force`d (even when you passed `--force`
732
+ to `ng update`), the install fails there while `package.json` has already been
733
+ written correctly. The fix both times was to redo the install by hand, not to
734
+ re-run the update. `p043` and `app-tournament` both carry ngx-charts `^20.4.1`
735
+ and will hit this; `app-fitnessmirror` and `app-finance-ai` do not.
736
+ - **`ngx-extended-pdf-viewer` moved to `.mjs`** at the 21 hop and stopped shipping
737
+ `pdf-3.10.560-es5.min.js`. If your `angular.json` names that file under `scripts`
738
+ or `assets`, the build fails on a missing file rather than on anything Angular.
739
+ - **`karma` and `@angular/build`**: `@angular/build` 22 declares
740
+ `peerOptional karma@^6.4.0`. A host pinned to `~6.3.0` gets a wall of
741
+ `npm warn ERESOLVE overriding peer dependency` — **warnings, exit code 0**, so it
742
+ is not blocking. It becomes a *hard* install failure the moment `@angular/build`
743
+ is a direct devDependency rather than a nested one, which is what happened in the
744
+ library workspace. `karma@~6.4.4` clears it.
745
+ - **`ngx-markdown` 22 lazily imports `marked-katex-extension`**, an optional peer. The
746
+ application builder leaves that import alone; the webpack `browser` builder every measured
747
+ host still runs resolves it, and the build fails with `Can't resolve
748
+ 'marked-katex-extension'` against ngx-markdown's own bundle. Declare
749
+ `katex@^0.16` and `marked-katex-extension@^5`: webpack only puts them in a lazy chunk that
750
+ loads when KaTeX is switched on. ngx-markdown 22 also wants `marked@^17 || ^18` declared
751
+ (app-formmate, 2026-09-22).
752
+
753
+ The general shape: **`ng update` needs `--force` at every hop** (the library and
754
+ PrimeNG both peer-cap at Angular 17 until the very end), and `--force` covers the
755
+ Angular schematics but not the `npm install` they trigger. When that install fails,
756
+ check `package.json` before assuming the hop did nothing — it is usually already
757
+ correct.
758
+
759
+ ### Housekeeping (Phase 3.0)
760
+
761
+ #### The dialog's built-in Ok/Cancel buttons are gone
762
+
763
+ `smartdialog.component.html` no longer renders an `Ok` and a `Cancel` button.
764
+ They were dead: their `(click)` handlers were the no-op expressions
765
+ `(click)="(data.okCallback)"` / `(click)="(data.cancelCallback)"` — a bare
766
+ property read, not a call — so pressing them did nothing, and the whole block
767
+ was behind `*ngIf="!data.customComponent"`, which is never true for a host
768
+ dialog (`SmartDialog.ngAfterViewInit` unconditionally instantiates
769
+ `data.customComponent`).
770
+
771
+ **The `okCallback` / `cancelCallback` model fields stay.** Hosts set and invoke
772
+ them from their own `customComponent` dialogs, and that keeps working
773
+ unchanged. Only the library-rendered buttons are gone. The `actionCallback` /
774
+ `actionLabel` button — the one with a real handler (`onActionClick()`) — is
775
+ untouched.
776
+
777
+ Action needed: none, unless you relied on the library rendering those two
778
+ buttons — in which case they never worked, and you should render them in your
779
+ own dialog component.
780
+
781
+ ### Full standalone + `provideSmartNgClient()` (Phase 3.2) — **the big one**
782
+
783
+ This is the change that rewrites your `app.module.ts`. Everything else in this
784
+ guide is small next to it.
785
+
786
+ #### Every NgModule is gone
787
+
788
+ The library no longer ships a single `@NgModule`. All 58 are deleted —
789
+ `SmartNgClientModule`, `SmartViewContextModule`, `SmartSessionModule`,
790
+ `SmartGridModule`, `SmarttableModule`, `SmarttreeModule`, `SmartdialogModule`,
791
+ `SmartIconModule`, `SmartNavbarModule`, `SmartFilterModule`,
792
+ `SmartFilterEditorModule`, `SmartComponentLayoutModule`,
793
+ `SmartExpandableSectionModule`, `SmartMapModule`, `SmartDiagramModule`,
794
+ `SmartNavigationModule`, `SmartValidationModule`, `SmartGenericPagesModule`,
795
+ `ComponentFactoryServiceModule`, `SharedModule`, `SmartTabGroupModule`,
796
+ `SmartDocuStoreExplorerModule` and the generated OpenAPI `ApiModule`s.
797
+
798
+ Every component, directive and pipe is now **standalone**. Import the ones your
799
+ templates use directly, in the component that uses them:
800
+
801
+ ```ts
802
+ @Component({
803
+ selector: 'app-my-page',
804
+ templateUrl: './my-page.component.html',
805
+ imports: [SmartGridComponent, UiActionToolbarComponent, SmartEmbeddedSlotDirective],
806
+ })
807
+ export class MyPageComponent extends SmartComponent<MyModel> { … }
808
+ ```
809
+
810
+ **The compiler tells you what is missing** — one `NG8001: '<smart-grid>' is not
811
+ a known element` per unresolved element, per template. Work through them; there
812
+ is no guessing involved. In practice ~16 module imports become ~40 component
813
+ imports spread across the components that actually use them.
814
+
815
+ **The compiler does NOT tell you about the DI changes below.** Nothing fails at
816
+ build time if you get those wrong; you find out on the first page load. Smoke
817
+ your app.
818
+
819
+ #### `provideSmartNgClient(config, ...features)` replaces the module imports
820
+
821
+ ```ts
822
+ bootstrapApplication(AppComponent, {
823
+ providers: [
824
+ provideAnimations(),
825
+ provideRouter(ROUTES),
826
+ provideSmartNgClient(
827
+ {
828
+ gridMenuIcon: 'more_horiz',
829
+ treeMenuIcon: 'more_horiz',
830
+ aclEditingViewName: Pages.ACL_MATRIX_PAGE,
831
+ invalidSmartlinkPageName: Pages.INVALID_SMARTLINK_PAGE_NAME,
832
+ namedValidators: [MY_VALIDATOR_FACTORY],
833
+ },
834
+ withSmartMap({ engine: MapEngine.LEAFLET }),
835
+ withSmartDiagram({ customOptions: [MY_CHART] })
836
+ ),
837
+ { provide: HTTP_INTERCEPTORS, useClass: MyLoadingInterceptor, multi: true },
838
+ ],
839
+ });
840
+ ```
841
+
842
+ The core is always wired and cannot be forgotten: session, view context, the
843
+ three BFF interceptors, layout, form, grid, table, tree, dialog, icon, navbar,
844
+ filter, filter editor, validation, expandable section, navigation, shared and
845
+ the generic pages. That is the point — the session and header interceptors used
846
+ to be easy to leave out, and leaving them out fails *silently* (no
847
+ `Authorization` header).
848
+
849
+ Only two features are opt-in, because they pull heavy third-party code that not
850
+ every host renders: `withSmartMap()` (leaflet / Google Maps) and
851
+ `withSmartDiagram()` (chart.js). There is **no `withSmartTabGroup()`** —
852
+ `smart-tab-group` had no component left, only an empty module, and is deleted.
853
+
854
+ `provideSmartNgClient()` calls `provideHttpClient()` itself, with
855
+ `withInterceptorsFromDi()` and its own three functional interceptors, and leaves the backend
856
+ on the framework default (fetch). Your own class-based `HTTP_INTERCEPTORS` keep working and
857
+ still run after the library's three. Pass extra `HttpClient` features in
858
+ `config.httpFeatures` rather than making a second `provideHttpClient()` call — see
859
+ [HTTP](#http-the-library-no-longer-forces-the-xhr-backend-phase-38), which is also where the
860
+ 6.x "do not call it twice" trap went.
861
+
862
+ #### String-keyed providers become typed config fields
863
+
864
+ Delete these from your providers array and pass them to `provideSmartNgClient`:
865
+
866
+ | 6.x provider | 7.0 config field |
867
+ |---|---|
868
+ | `{ provide: 'gridMenuIcon', useValue: … }` | `gridMenuIcon` |
869
+ | `{ provide: 'treeMenuIcon', useValue: … }` | `treeMenuIcon` |
870
+ | `{ provide: 'searchPageName' / 'searchComponentName', … }` | `searchPageName` / `searchComponentName` |
871
+ | `{ provide: 'genericPageName' / 'genericComponentName', … }` | `genericPageName` / `genericComponentName` |
872
+ | `{ provide: 'subjectSelectorPageName' / 'subjectSelectorComponentName', … }` | `subjectSelectorPageName` / `subjectSelectorComponentName` |
873
+ | `{ provide: 'validationResultPageName' / 'validationResultComponentName', … }` | `validationResultPageName` / `validationResultComponentName` |
874
+ | `{ provide: 'noPermissionPageName' / 'noPermissionComponentName', … }` | `noPermissionPageName` / `noPermissionComponentName` |
875
+ | `{ provide: 'invalidSmartlinkPageName' / 'invalidSmartlinkComponentName', … }` | `invalidSmartlinkPageName` / `invalidSmartlinkComponentName` |
876
+ | `{ provide: 'aclEditingViewName', useValue: … }` | `aclEditingViewName` |
877
+ | `{ provide: DIALOG_DISABLE_CLOSE, useValue: … }` | `dialogDisableClose` |
878
+ | `{ provide: MAT_DATE_LOCALE, useValue: 'hu-HU' }` | `dateLocale` — **or keep the provider as it is**, which is what the codemod does; see [Dates §2](#2-leave-your-mat_date_locale-provider-alone) |
879
+ | `SmartWidgetSettings.useUtc = true` (a static, not a provider) | **delete it** — the flag is gone with the moment adapter; see [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) |
880
+ | `{ provide: NAMED_VALIDATOR, useValue: F, multi: true }` | `namedValidators: [F]` |
881
+ | `{ provide: SMART_DEFAULT_VIEW_COMPONENTS, useValue: E, multi: true }` | `defaultViewComponents: [E]` |
882
+ | `SmartValidationModule.forRoot([...])` | `namedValidators` — **the element type changes**, see below |
883
+ | `SmartDiagramModule.forRoot([...])` | `withSmartDiagram({ customOptions })` |
884
+ | `{ provide: MAP_ENGINE, useValue: … }` | `withSmartMap({ engine })` |
885
+
886
+ `'pageName'`, `'componentName'` and `'treeId'` stay as string tokens on purpose:
887
+ they identify one component instance rather than configuring the library, and
888
+ hosts also provide them at component level.
889
+
890
+ You can also delete these from your **root** providers array — the core provides them:
891
+ `SmartSessionService`, `SmartViewContextService`, `SmartNavigationService`,
892
+ `SmartIconService`, `SmartFilterEditorService`, `SmartFormService`,
893
+ `SmartCookieService`, `NamedValidatorService`, `ComponentFactoryService`, and the date
894
+ adapter wiring (`MAT_DATE_FORMATS`, `DateAdapter`).
895
+
896
+ ⚠️ **`namedValidators` takes factories, not providers.** `SmartValidationModule.forRoot()`
897
+ took `Provider[]`, so hosts wrote the wrapper by hand:
898
+
899
+ ```ts
900
+ // 6.x — validator.factories.ts
901
+ export const VALIDATOR_PROVIDER_EMPTY_INPUT: Provider = {
902
+ provide: NAMED_VALIDATOR, useValue: VALIDATOR_FACTORY_EMPTY_INPUT, multi: true,
903
+ };
904
+ // app.module.ts
905
+ imports: [SmartValidationModule.forRoot([VALIDATOR_PROVIDER_EMPTY_INPUT])]
906
+ ```
907
+
908
+ `provideSmartNgClient()` adds that wrapper itself, so the config takes the bare
909
+ `ValidatorFactory[]`. Export the factories and pass those:
910
+
911
+ ```ts
912
+ // 7.0
913
+ export const VALIDATOR_FACTORY_EMPTY_INPUT: ValidatorFactory = { … };
914
+ provideSmartNgClient({ namedValidators: [VALIDATOR_FACTORY_EMPTY_INPUT] })
915
+ ```
916
+
917
+ The codemod moves the argument into the config and **reports it**, because the entries are
918
+ normally named constants declared in another file and only you know what each one wraps. If
919
+ you miss the note, `TS2322: Type 'Provider' is not assignable to type 'ValidatorFactory'`
920
+ lands on every element.
921
+
922
+ ⚠️ **The same line on a `@Component` is not the same thing.**
923
+ `providers: [SmartFilterEditorService]` on a page component asks for one instance *per page*,
924
+ which is usually deliberate — p014 does it in five components. Leave those alone; the codemod
925
+ does.
926
+
927
+ #### The module → standalone mapping
928
+
929
+ What each `Smart*Module` used to export, reduced to what 7.0 still exports publicly. An
930
+ NgModule's `imports:` array accepts standalone components, so this substitution is all a host
931
+ that is still NgModule-based needs to keep compiling — the codemod performs it.
932
+
933
+ | 6.x module | 7.0 imports |
934
+ |---|---|
935
+ | `SmartNgClientModule` | the union of the fourteen it re-exported (everything below except the generic pages, the filter editor, the map and the diagram) — trim it to what your templates use |
936
+ | `SmartComponentLayoutModule` | `SmartComponentLayoutComponent` |
937
+ | `SmartViewContextModule` | `UiActionToolbarComponent`, `UiActionButtonComponent`, `UiActionDialogButtonComponent`, `SmartformComponent`, `SmartfileuploaderComponent`, `SmartFileEditorComponent`, `SmartMultiFileEditorComponent`, `SmartVoiceRecorderComponent`, `SmartEmbeddedSlotDirective`, `HighlightPipe` |
938
+ | `SmartGridModule` | `SmartGridComponent` |
939
+ | `SmarttreeModule` | `SmartTreeComponent` |
940
+ | `SmartdialogModule` | `SmartDialog` |
941
+ | `SmartIconModule` | `SmartIconComponent`, `UiBadgeComponent`, `UiBadgeDirective` |
942
+ | `SmartNavbarModule` | `SmartNavbarComponent` |
943
+ | `SmartFilterModule` | `SmartFilterComponent` |
944
+ | `SmartFilterEditorModule` | `SmartFilterEditorContentComponent` |
945
+ | `SmartExpandableSectionModule` | `ExpandableSectionComponent` |
946
+ | `SmartSessionModule` | `SmartSessionTimerComponent` |
947
+ | `SmartGenericPagesModule` | `SearchPageComponent`, `GenericPageComponent`, `SubjectSelectorComponent`, `InvalidSmartlinkComponent` |
948
+ | `SmartMapModule` | `SmartMapComponent` + `withSmartMap({ engine })` |
949
+ | `SmartDiagramModule` | `SmartDiagramComponent` + `withSmartDiagram({ customOptions })` |
950
+ | `SharedModule` | `SmartTooltipDirective`, `SmartDatePipe`, `SmartDateTimePipe`, `SmartTimePipe` |
951
+ | `SmartValidationModule` | nothing — `forRoot()`'s validators become `namedValidators` |
952
+ | `SmarttableModule` | nothing — `SmarttableComponent` is no longer public; render a `<smart-grid>` |
953
+ | `SmartNavigationModule`, `ComponentFactoryServiceModule` | nothing — they only carried services, which the core provides |
954
+ | `SmartTabGroupModule` | nothing — deleted; it had no component left |
955
+ | `SmartDocuStoreExplorerModule` (`@smartbit4all/document-explorer`) | `SmartDocuStoreExplorerComponent`, `FolderContentComponent`, `PdfViewerDialogPageComponent` |
956
+
957
+ Two things this table will not tell you, and the compiler will not either: an import you no
958
+ longer need is invisible (unused entries in an NgModule `imports:` array are not reported),
959
+ and a name you already bind from elsewhere — a `SmartDatePipe` of your own — must not be
960
+ imported twice. The codemod reports both cases rather than guessing.
961
+
962
+ #### Your `SmartComponent` subclasses silently become OnPush
963
+
964
+ This is an **Angular 22 fact rather than something 7.0.0 introduces**, but it
965
+ lands on you at the same moment, so it is worth stating plainly: `SmartComponent`
966
+ is an abstract `@Component`, and each of your subclasses carries its own
967
+ `@Component` decorator. On Angular 22 a component without an explicit
968
+ `changeDetection` gets the new `OnPush` default. p014 has 75 such subclasses,
969
+ p043 has 70.
970
+
971
+ Let the `ng update` schematic add `changeDetection: ChangeDetectionStrategy.Eager`
972
+ to all of them (that is what the library did), verify your app, and move to
973
+ OnPush deliberately afterwards. A subclass that mutates its own state outside a
974
+ signal and without `markForCheck()` will stop repainting under OnPush.
975
+
976
+ #### Removed from the public API
977
+
978
+ A major is the only chance to narrow the surface, so these are no longer
979
+ exported. All of them are library internals; none is used by p014, p043,
980
+ app-formmate or `@smartbit4all/document-explorer`:
981
+
982
+ - `SmarttableComponent`, `SmarttableService` — use `<smart-grid>`, which renders
983
+ the table internally. **All `SmartTable*` model types stay** (`SmartTableType`,
984
+ `SmartTableInterfaceTypeEnum`, `SmartTableHeader`, …). p043 imports
985
+ `SmarttableComponent` in `ad-groups.component.ts` but never uses it — delete
986
+ that one import line. An import is not the only way in: p009-angular created it
987
+ through `ComponentFactoryService` (`recommendations-page`), so grep for the class
988
+ name, not only for the import, and rebuild such a page on `<smart-grid>`. p034 used it
989
+ three times for a client-side check box table (a `<smarttable>` in a template and two
990
+ `ComponentFactoryService` sites) whose rows come from the host, not from a backend grid;
991
+ there a small host component rendering the still public `SmartTable` model with
992
+ `mat-table` keeps the pages, because `<smart-grid>` needs a backend grid model.
993
+ - `GoogleMap`, `LeafletMap`, `AbstractMap` — select an engine with
994
+ `withSmartMap({ engine })`.
995
+ - `SmartFilterParamComponent`, `SmartFilterParamsComponent`,
996
+ `SmartFilterExpressionItemComponent`, `SmartFilterExpressionItemsComponent` —
997
+ internals of `SmartFilterEditorContentComponent`, which stays public.
998
+ - `HoverMenuComponent`, `ClickMenuComponent` — rendered by the ui-action toolbar.
999
+ - `UiActionConfirmDialogComponent`, `UiActionInputDialogComponent` — opened by
1000
+ their services, which stay public.
1001
+ - `UploadWidgetComponent`, `PhotoCaptureWidgetComponent`,
1002
+ `VoiceRecordWidgetComponent` — contents of the ui-action upload dialog.
1003
+ - `SmartNgClientService` — deleted; it was an empty stub with no members.
1004
+ - The generated `ApiModule`s — they had no usages and their `forRoot()` told you
1005
+ to import `HttpClientModule`, which no longer applies.
1006
+
1007
+ Two internal string tokens are gone as well, replaced by real `InjectionToken`s
1008
+ (`'confirmDialogService'`, `'textFieldDialogService'`, `'fileUploadDialogService'`).
1009
+ No host provided them, so no action is needed.
1010
+
1011
+ #### Behaviour notes
1012
+
1013
+ - `<mat-slide-toggle>` in the form widget no longer carries a `value` binding,
1014
+ `<mat-nested-tree-node>` no longer carries an interpolated `matTreeNodeToggle`,
1015
+ and the ui-action menus no longer carry `[subActions]`. All four were dead
1016
+ bindings that the NgModules' `CUSTOM_ELEMENTS_SCHEMA` had been hiding — they
1017
+ wrote a DOM property nobody read. Rendering is unchanged.
1018
+ - The same schema on a **host** module hides library selectors that 7.0 no longer
1019
+ exports: the template compiles and the element renders nothing.
1020
+ `<smart-filter-group-params>` (p009-angular, two admin pages) is one; grep the
1021
+ templates for `smart-` selectors that the build does not resolve.
1022
+ - The dead `ExpandableGridComponent` is deleted (it was declared in
1023
+ `SmartGridModule` but its selector was used nowhere).
1024
+
1025
+ ### Widget ↔ SmartComponent: the model routing is inverted (Phase 3.4)
1026
+
1027
+ A widget no longer waits to be collected by the screen component it lives in: it finds its
1028
+ `SmartComponentApiClient` through DI and registers itself. `SmartComponent` carries the
1029
+ channel as an inherited host directive, so **no host component has to be changed for the
1030
+ routing itself** — everything below is the *legacy surface* that went with the old
1031
+ collection path. The protocol is written up in `WIDGETS.md` next to this file; the decision
1032
+ record behind it is ADR-0005 in the `platform-angular2` working copy.
1033
+
1034
+ #### What a host must delete (all of it mechanical, all reported by the compiler)
1035
+
1036
+ | Delete | Why |
1037
+ |---|---|
1038
+ | `this.useQueryLists = true;` | The property is gone. Its setter rejected anything but `true`, so every branch behind it was unreachable — this was a no-op that logged a warning. |
1039
+ | `[parentSmartComponent]="this"` on `<smart-component-layout>` | The input is gone; the layout injects the client. `strictTemplates` reports leftovers as **NG8002**. |
1040
+ | `this.addForm(id, smartForm, formComponent)` | Gone. A `<smartform>` that displays the client's model registers itself. Two call styles existed; where the return value was used, it was the `smartForm` argument — use that (which makes those lines self-assignments, so they just go). **Delete the scaffolding around the call too**: in p014 the 21 calls left behind 9 empty `if (this.formX) { }` guards and 3 `new Promise((r) => setTimeout(r, 500)).then(() => { })` wrappers whose only content had been the registration. The compiler does not complain about either. |
1041
+ | `this.widgets.delete(…)` / `this.widgets.set(…)` / `this.formWidgets…` | The maps are gone (they were only written by the legacy path and read by nothing). |
1042
+ | the 4th argument of `addGrid(smartGrid, options, useAsDefaultGrid, componentReference)` | `addGrid()` keeps the options and the default-grid role; the services it used to copy into the grid model (`uiActionService`, `uiActionDescriptorService`, `serviceToUse`, `viewContextService`) are the grid's own DI job now. |
1043
+ | `override getSmartXxxQL()` (e.g. the two `throw new Error` overrides in a `FilterPageService`) | The eight abstract getters are gone from `SmartComponentApiClient`. |
1044
+ | `this.getSmartGridsQL()` and friends | If a component really wants its own widgets, it declares the view query itself: `@ViewChildren(SmartGridComponent) grids!: QueryList<SmartGridComponent>` — same reach as the removed getter had. |
1045
+ | `handleDataChangeSubscriptions()` | Gone. A form publishes its own value-change keys when it constructs. |
1046
+
1047
+ Removed from the public API in the same step, in case a host imported them directly:
1048
+ `SmartComponentLayoutUtility` and its `EmbeddedSlotInfo` interface (the whole file — its
1049
+ collectors walked the layout tree for widgets, which nothing does now), and the `[parent]`
1050
+ inputs of `<smart-map>` / `<smart-diagram>` (they were bound to `parentSmartComponent` and read
1051
+ by nothing; both widgets resolve the client themselves). `initActions()` also runs less often:
1052
+ only when the model or its action list changes, no longer after every layout render, grid view
1053
+ init and filter model change — a host override that relied on one of those extra calls has to
1054
+ key off `dataChanged` instead.
1055
+
1056
+ The [codemod](#migrating-a-host-the-codemod) deletes the first two rows and the last one, and
1057
+ lists the rest — `addForm()`, the 4th `addGrid()` argument, the getter overrides — with their
1058
+ line numbers, because each of those is a small decision rather than a substitution.
1059
+
1060
+ #### What stays
1061
+
1062
+ - `SmartService` — a view-less client. Its `@ViewChildren` never populated anyway; they are
1063
+ simply gone now.
1064
+ - `initActions()` — still the hook a host overrides to build its own action list. The base
1065
+ implementation only asks for a render; toolbars pull their actions themselves.
1066
+ - `setUpDefaultTree()` / `createTreeService()`, `addGrid()`, `submitForms()`,
1067
+ `getInvalidFields()`, `uiActionModels` (now a getter over the model's actions).
1068
+
1069
+ #### `uiActionModels` is read-only now — assigning to it no longer compiles
1070
+
1071
+ It became a getter over the `actionModels` computed, so a host that *replaced* the list in
1072
+ place gets `TS2540: Cannot assign to 'uiActionModels' because it is a read-only property`.
1073
+ Measured in **p014 and p043**, `document-storage-editor.component.ts`:
1074
+
1075
+ ```ts
1076
+ // before — no longer compiles
1077
+ this.uiActionModels = this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL');
1078
+
1079
+ // after — filter into a field of your own and bind it
1080
+ protected readonly visibleActions = signal<UiActionModel[]>([]);
1081
+ // …
1082
+ this.visibleActions.set(this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL'));
1083
+ ```
1084
+
1085
+ ```html
1086
+ <smart-ui-action-toolbar [uiActionModels]="visibleActions()"></smart-ui-action-toolbar>
1087
+ ```
1088
+
1089
+ A plain field works just as well; what matters is that the **array reference changes**, which
1090
+ is what tells the toolbar to re-render. See also the entry-freeze note in the Phase 3.7
1091
+ section.
1092
+
1093
+ #### Behavioural deltas, accepted
1094
+
1095
+ - **Wider reach.** A widget in a host's *own* sub-component now participates, where a view
1096
+ query could not see it (measured on p014/p043: 5 toolbars and 3–4 forms per host). A form
1097
+ takes part only if it displays the client's component model, which is what the forms the
1098
+ queries used to find have in common; a form that renders something else of its own (a
1099
+ search box, a field editor) does not.
1100
+ - Two widgets sharing one identifier both reload; the old map kept only the last.
1101
+ - `getInvalidFields()` collects in subscription order rather than view order — visible only
1102
+ in the order of the names in the validation dialog.
1103
+ - A toolbar entry is tracked by a synthesized key (action code + ordinal among entries with
1104
+ the same code), so a button's DOM node — with its focus and ripple — follows its action
1105
+ across reordering. Rendering is otherwise identical.
1106
+
1107
+ #### `[smartComponentDetached]`
1108
+
1109
+ The one brake on the automatic registration. Put it on an element whose subtree deliberately
1110
+ renders something other than the client's model; widgets below it resolve an empty slot and
1111
+ neither reload nor take part in submit/validation.
1112
+
1113
+ ```html
1114
+ <div smartComponentDetached>
1115
+ <smart-grid [smartGrid]="ownGrid" [uuid]="ownUuid"></smart-grid>
1116
+ </div>
1117
+ ```
1118
+
1119
+ #### `SmartSubject` is deleted
1120
+
1121
+ It was a no-op wrapper over `Subject`: `subscription.add(this.unsubscribe$.subscribe())`
1122
+ registers the teardown in the wrong direction, so after `destroy$.next()`, `.complete()` or
1123
+ `.unsubscribe()` subscribers still received values. Replace `new SmartSubject(destroy$)`
1124
+ with `new Subject()`; where a subscription really must die with a component, use
1125
+ `takeUntil(this._destroy$)` (which is what actually stopped delivery all along) or
1126
+ `takeUntilDestroyed()`. No host used it.
1127
+
1128
+ ### Toolbars resolve their own actions and own the execution context (Phase 3.5)
1129
+
1130
+ One rule decides which actions a toolbar shows, and the toolbar — not each list entry —
1131
+ says who performs them. `WIDGETS.md`, *Toolbars*, is the short version; the decision record
1132
+ behind it is ADR-0006 in the `platform-angular2` working copy.
1133
+
1134
+ > A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The
1135
+ > list comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the
1136
+ > screen component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is
1137
+ > not an address, so a toolbar with neither an id nor a binding shows nothing.
1138
+
1139
+ `[uiActionModels]="uiActionModels"` therefore keeps meaning "put my page's unaddressed
1140
+ actions here" and needs no change.
1141
+
1142
+ #### `UseUiAction` is deleted; `UseUiAction2` is now `UiActionExecutor`
1143
+
1144
+ There is one executor interface. If a host class implements `UseUiAction` — a `submit` /
1145
+ `reSubscribeToChange` subject handshake — replace the two subjects with the two methods:
1146
+
1147
+ ```ts
1148
+ // before
1149
+ export class DelegateInboxDialogService implements UseUiAction {
1150
+ submit: Subject<void> = new Subject();
1151
+ reSubscribeToChange: Subject<void> = new Subject();
1152
+ …
1153
+ }
1154
+
1155
+ // after
1156
+ export class DelegateInboxDialogService implements UiActionExecutor {
1157
+ submitForm(validate: boolean): void {
1158
+ this.submit.next(); // keep the subject if a component subscribes to it
1159
+ }
1160
+ getInvalidFields(): SmartFormInvalidFields {
1161
+ return { invalidFieldKeys: [], invalidFieldNames: [] };
1162
+ }
1163
+ …
1164
+ }
1165
+ ```
1166
+
1167
+ `Subject.next()` is synchronous, so a component that subscribed to `submit` has already run
1168
+ by the time `submitForm()` returns — which is exactly what the old handshake tried to
1169
+ express. Keep `reSubscribeToChange` only if something subscribes to it; the library no longer
1170
+ fires it.
1171
+
1172
+ This also **fixes a silent hang**: the old branch `await`ed `submit.toPromise()`, which only
1173
+ settles when somebody `complete()`s the subject. Three of the five implementors never did, so
1174
+ a `submit`/`model` action on those services waited forever.
1175
+
1176
+ #### `UiActionService.execute()` lost its first argument
1177
+
1178
+ It was always `uiActionModel.uiAction`:
1179
+
1180
+ ```ts
1181
+ - this.uiActionService.execute(uiActionModel.uiAction, uiActionModel);
1182
+ + this.uiActionService.execute(uiActionModel);
1183
+ ```
1184
+
1185
+ #### The execution context moved from the entry to the toolbar
1186
+
1187
+ | what | bind on the toolbar | entry field (still honoured) |
1188
+ |---|---|---|
1189
+ | who performs the action | `[executor]` | `serviceToUse` — **deprecated** |
1190
+ | which widget it runs against | `[widgetId]` | `widgetId` — **deprecated**, wins if set |
1191
+ | which row inside it | `[nodeId]` | `nodeId` — **deprecated**, wins if set |
1192
+ | extra params for every action | `[actionParams]` | (clone the `UiAction` — no longer necessary) |
1193
+
1194
+ `[executor]` defaults to the screen component the toolbar sits under, which is what almost
1195
+ every entry named. Bind it only when the actions belong to another API — a tree service, a
1196
+ filter editor, a dialog service of your own:
1197
+
1198
+ ```html
1199
+ <smart-ui-action-toolbar [uiActionModels]="actions" [executor]="myService"></smart-ui-action-toolbar>
1200
+ ```
1201
+
1202
+ `[actionParams]` replaces cloning a `UiAction` per row just to inject a model:
1203
+
1204
+ ```html
1205
+ <smart-ui-action-toolbar
1206
+ [uiActionModels]="rowActions"
1207
+ [widgetId]="gridId"
1208
+ [nodeId]="row.id"
1209
+ [actionParams]="{ model: row }"
1210
+ ></smart-ui-action-toolbar>
1211
+ ```
1212
+
1213
+ #### Removed from the public API
1214
+
1215
+ | Removed | Replacement |
1216
+ |---|---|
1217
+ | `UseUiAction` | `UiActionExecutor` (see above) |
1218
+ | the third `SmartTable` constructor parameter and `getServiceToUse()` | nothing — the executor no longer travels through the table (0 host call sites) |
1219
+ | `SmartformComponent.getToolbars()` | nothing collects toolbars any more; each one resolves its own actions |
1220
+ | `ISmartFilterEditorService.get/setComplexToolbars()` and the `submit` / `reSubscribeToChange` members | as above |
1221
+ | `FileEditorToolbarComponent` | a plain `<smart-ui-action-toolbar>` with `[actionParams]` |
1222
+ | `SmartGridComponent.setupToolbar()` / `.toolbar` / `.headerToolbar` | the id is a binding; there is nothing to reach into |
1223
+
1224
+ #### Behavioural deltas, accepted
1225
+
1226
+ - A widget renders a toolbar exactly when its model carries a `toolbarId`, with no type list.
1227
+ A `toolbarId` on a type whose template has no place for a toolbar (`RECORDING_UPLOADER`)
1228
+ now logs a warning instead of being silently dropped.
1229
+ - Only the actions a toolbar actually shows are scheduled. Since 3.4 a toolbar armed every
1230
+ scheduled action of the view, so one action could fire from several toolbars at once.
1231
+ - A grid row's actions are no longer cloned per row; the row model travels as
1232
+ `[actionParams]`. The multi-file editor's per-file toolbars appear when their own list
1233
+ resolves rather than when a hidden sibling's did, and the tree's `doAction` clones instead
1234
+ of mutating the caller's `UiAction`.
1235
+
1236
+ ### The form widget's rendering decisions (Phase 3.6)
1237
+
1238
+ Nothing here needs a code change in a host — there is no API to adapt to. What
1239
+ changes is what the widget puts on the page, so read this before you compare
1240
+ screenshots.
1241
+
1242
+ #### Every widget carried a spare empty wrapper; it is gone
1243
+
1244
+ The `COMPONENT` branch's wrapper had no type guard, so
1245
+ `<div class="widgetContainer"><div class="widgetContent"><ng-template
1246
+ #customComponent>` rendered after **every** widget of every type. Measured on the
1247
+ playground: 14 widgets, 28 `.widgetContainer` — one spare, empty, per widget.
1248
+
1249
+ The library's own CSS puts no box properties on `.widgetContainer`, so most pages
1250
+ will look identical. **Yours may not**, if you style that class with anything that
1251
+ occupies space. Both in-house hosts do, on one page:
1252
+
1253
+ ```css
1254
+ /* p014 + p043, dossier-publication.component.css */
1255
+ :host ::ng-deep .widgetContainer { margin-bottom: 0.5rem !important; }
1256
+ ```
1257
+
1258
+ There, every widget loses half a rem of trailing space. Grep your stylesheets for
1259
+ `.widgetContainer` and check the pages the matching selectors reach. Rules that
1260
+ need content still match what they always matched — an empty wrapper never
1261
+ satisfied `.label.widgetContainer` or `.widgetContainer:has(…)`.
1262
+
1263
+ #### `MATRIX` and `YOUTUBE_PLAYER` now obey `isVisible` and get their `cssClass`
1264
+
1265
+ Those two branches sat outside the `@if (isVisible)` guard and outside the
1266
+ `<div [ngClass]="cssClass" class="container">` every other type lives in, so
1267
+ `isVisible: false` did not hide them and `cssClass` never reached them. Both are
1268
+ inside now. If a backend view relies on a matrix rendering while marked invisible,
1269
+ it will stop rendering.
1270
+
1271
+ #### A container keeps its invisible children
1272
+
1273
+ A `CONTAINER` widget's `ngOnInit` used to overwrite `widgetInstance.valueList`
1274
+ with the visible subset, destroying the invisible children in the model the
1275
+ backend sent. The visible subset is derived per check now, so the model stays
1276
+ whole and a later `applyConstraints()` that flips a child's `isVisible` brings it
1277
+ back — which it could not before. The DOM is unchanged: the same children render.
1278
+
1279
+ Side effect worth knowing: the model walks that recurse into a container's
1280
+ `valueList` (`applyComponentConstraints`, `copyValueFromFormToWidgetBeans`,
1281
+ `translateWidgets`) now see the invisible children too. Their form controls always
1282
+ existed — `createFormControls` runs over the whole model before any widget renders
1283
+ — so this only makes the widget beans agree with the controls.
1284
+
1285
+ #### A YouTube shorts link resolves
1286
+
1287
+ `parseYoutubeUrl` threw away its own `replace('/shorts/', '/watch?v=')`, so a
1288
+ shorts link produced no player. It plays now.
1289
+
1290
+ #### Removed from the public API
1291
+
1292
+ | Removed | Replacement |
1293
+ |---|---|
1294
+ | `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.3 splits it into per-type components |
1295
+ | `SmartWidgetSettings` (its only member was `static useUtc`) | nothing — **delete the assignment** |
1296
+
1297
+ 3.6 moved the `useUtc` static into `SmartNgClientConfig.useUtcDates`; **3.8 then removed the
1298
+ field altogether**, together with the moment adapter it fed. If you set
1299
+ `SmartWidgetSettings.useUtc = true` before bootstrap, delete that line and read
1300
+ [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) — the timezone contract is fixed now,
1301
+ and the flag defaulted to `false` everywhere anyway.
1302
+
1303
+ #### What deliberately did not change
1304
+
1305
+ A required field is marked in both label positions, by two different mechanisms:
1306
+ a shown label (`showLabel: true`) is the widget's own `<h4>` and carries a literal
1307
+ ` *`; a `mat-label` sits inside a `mat-form-field`, which renders
1308
+ `.mat-mdc-form-field-required-marker` from the control's own validators. That is
1309
+ why `getWidgetLabel` ties its asterisk to `showLabel` — dropping the condition
1310
+ would mark the floating label twice.
1311
+
1312
+ ### Change detection: frozen action entries, no zone.js (Phase 3.7)
1313
+
1314
+ 7.0 runs without zone.js. Every component of the library is `OnPush` — nothing is checked
1315
+ just because something, somewhere, ticked — and the library says when its own state changed.
1316
+ Two consequences reach a host.
1317
+
1318
+ #### A `UiActionModel` is frozen: build an entry, never edit one
1319
+
1320
+ Every field is `readonly`, and `[uiActionModels]` takes `readonly UiActionModel[]`.
1321
+
1322
+ ```ts
1323
+ // before — compiles on 6.x, and on 7.0 changes nothing on screen
1324
+ entry.cssClass = 'active-nav-action';
1325
+
1326
+ // after
1327
+ this.actions = this.actions.map((a) =>
1328
+ a.uiAction.code === code ? { ...a, cssClass: 'active-nav-action' } : a
1329
+ );
1330
+ ```
1331
+
1332
+ Measured across the 12 hosts in scope (2026-07-28): **72 sites in 7 hosts**, all of them
1333
+ `cssClass` — p009-angular alone has 38, p014 and p043 12 each. The codemod prints every one
1334
+ of them with its line number; it cannot rewrite them, because only you know which array the
1335
+ entry belongs to.
1336
+
1337
+ The compile error and the fix are the same thing. Writing into an entry that a toolbar is
1338
+ already rendering never reached the screen under zone.js either — it worked only because
1339
+ some other event happened to tick the application. Rebuilding the array is what actually
1340
+ re-renders.
1341
+
1342
+ **The gap this does not close.** `this.actions[0] = { ...this.actions[0], cssClass: 'x' }`
1343
+ compiles: `actions` is your field, and the freeze only covers the entries. It keeps the same
1344
+ array reference, so the toolbar does not re-render. Reassign the array — or, if you hold it
1345
+ in a `signal`, `update()` it.
1346
+
1347
+ **A component's own toolbar entries come from its `actionModels` signal.** The 6.x pattern
1348
+ `this.uiActionModels.forEach((a) => (a.cssClass = 'eji-button'))` in `initActions` becomes an
1349
+ override of that signal, which bound, pulled and layout-embedded toolbars all read:
1350
+
1351
+ ```ts
1352
+ // the helper runs while this.actionModels is still the base class's signal
1353
+ function withCssClass(component: SmartComponent<any>, cssClass: string) {
1354
+ const source = component.actionModels;
1355
+ return computed(() => source().map((a) => ({ ...a, cssClass })));
1356
+ }
1357
+
1358
+ override readonly actionModels = withCssClass(this, 'eji-button');
1359
+ ```
1360
+
1361
+ Reading `this.actionModels` directly in its own initializer is `TS2729`; passing `this` to a
1362
+ function is what captures the base signal (p009-angular, `src/app/utils/action-css-class.ts`).
1363
+ A host field of its own named `actionModels` now collides with the base signal
1364
+ (`TS2416` + `TS4114`): rename it.
1365
+
1366
+ When the mapping reads the component's data (mark the entry of `data.currentPage`), read
1367
+ `component.modelSignal()` inside the `computed` as well: `data` is a plain field, so without it
1368
+ the class does not follow a data change that leaves the action list alone (p034,
1369
+ `src/app/utils/action-models.ts`).
1370
+
1371
+ The descriptor a toolbar resolves is no longer written back into the entry either. Nothing
1372
+ outside the toolbar read it; if you did, resolve it yourself through
1373
+ `UiActionDescriptorService.getActionDescriptor(uiAction)` — which is
1374
+ [synchronous in 7.0](#getactiondescriptor-is-synchronous-phase-39).
1375
+
1376
+ #### Client-side translation is gone
1377
+
1378
+ `SmartTranslateService`, `viewContext.translateService` and the `translateServiceChanged`
1379
+ subject are removed, together with `originalLabel` / `originalPlaceholder` on the widget
1380
+ interfaces, and the `translateService` field of `SmartForm`, `SmartGrid` and `SmartFilter`. The
1381
+ backend localizes server-side, and in p014/p043 the three references were unused imports.
1382
+
1383
+ **A host that sends translation keys still needs translating.** p009-angular keeps ngx-translate
1384
+ keys in its client-built forms, its client action descriptors and its titles; deleting the
1385
+ calls would put raw keys on screen. It now translates in the host: the widget labels,
1386
+ placeholders, validator messages and item labels when a form is built, and the descriptor map
1387
+ on every `onLangChange`, with `bootstrap.start()` waiting for the language to load. **Grid
1388
+ column headers and filter labels have no host-side replacement**: they show what the backend
1389
+ sends, so localize them on the backend. A page that read `this.translate` without assigning it
1390
+ got `undefined` already on 6.x (the base class never set it), so check what a title showed
1391
+ before calling a now-translated one a regression.
1392
+
1393
+ What the frontend authors itself is now configuration:
1394
+
1395
+ ```ts
1396
+ provideSmartNgClient({
1397
+ errorDialog: { title: 'Hiba', message: 'Váratlan hiba történt.', buttonLabel: 'Rendben' },
1398
+ });
1399
+ ```
1400
+
1401
+ `SmartViewContextService.getSmartViewContextApiErrorByCode()` is no longer `async`.
1402
+
1403
+ **`SmartComponent` no longer has a `translate` member**, and a host that injected
1404
+ ngx-translate's `TranslateService` under that name as `override translate: TranslateService`
1405
+ now gets `TS4113` on every such constructor (app-formmate had 22). Do not just delete
1406
+ `override`: it was also what made the parameter a property, so without an access modifier
1407
+ `this.translate` disappears. Write `public translate: TranslateService` — and keep `override`
1408
+ on subclasses of your own pages, which now override *your* base class instead of the library's.
1409
+
1410
+ #### If your host still bootstraps with zone.js
1411
+
1412
+ Nothing forces you to drop it — the library works either way, because it no longer depends
1413
+ on an application-wide tick. The playground runs `provideZonelessChangeDetection()` with no
1414
+ `zone.js` polyfill, and that is the configuration 7.0 is tested in.
1415
+
1416
+ If you do go zoneless, the same rule applies to your own components: state written from a
1417
+ subscription, a promise or a timer needs a signal or a `markForCheck()`; state written from a
1418
+ template event or an input does not.
1419
+
1420
+ ### Dates: moment → date-fns, and the timezone contract (Phase 3.8)
1421
+
1422
+ **This section needs action at install time.** The reasoning is ADR-0008 in the
1423
+ `platform-angular2` working copy; the contract itself is stated in §4 below.
1424
+
1425
+ #### 1. Swap the peer dependency
1426
+
1427
+ `@angular/material-moment-adapter` is out of the library's `peerDependencies`;
1428
+ `@angular/material-date-fns-adapter` and `date-fns` are in. `date-fns` is listed explicitly
1429
+ rather than left transitive, because the library imports `date-fns/locale` itself.
1430
+
1431
+ ```bash
1432
+ npm uninstall @angular/material-moment-adapter moment
1433
+ npm install @angular/material-date-fns-adapter@^22.0.0 date-fns@^4
1434
+ ```
1435
+
1436
+ If your own code imports moment for something unrelated, keep it — nothing here forces its
1437
+ removal, only the adapter's.
1438
+
1439
+ **But check *how* it imports moment.** `import * as moment from 'moment'` stops being callable
1440
+ under `moduleResolution: "bundler"`, which the Angular 20 hop turns on: moment is an
1441
+ `export =` module, and a namespace import of one is not a callable value. The symptom is
1442
+ **`TS2349: This expression is not callable`** on every `moment(...)` call, and it arrives at the
1443
+ Angular 20 hop rather than at this step. Two ways out:
1444
+
1445
+ - `esModuleInterop: true` plus `import moment from 'moment'` — correct, but it changes how every
1446
+ CommonJS import in the app resolves, which is a wide blast radius for one file.
1447
+ - Replace the calls. Worth checking first how much moment is actually doing: in p014 the three
1448
+ surviving calls were `moment(new Date()).toDate()` (i.e. `new Date()`) and two moments compared
1449
+ with `<=` (`Date` compares through `valueOf()` identically). All three became plain `Date`s, and
1450
+ **nothing under `src/` imported moment any more** — so the `npm uninstall … moment` above was
1451
+ right for that host after all, just for a reason nobody predicted.
1452
+
1453
+ #### 2. Leave your `MAT_DATE_LOCALE` provider alone
1454
+
1455
+ If you have this line — p014 and p043 both do — **keep it as it is**:
1456
+
1457
+ ```ts
1458
+ { provide: MAT_DATE_LOCALE, useValue: 'hu-HU' },
1459
+ ```
1460
+
1461
+ The strict reading of the date-fns adapter would require a `Locale` **object** here, and a bare
1462
+ string would throw inside date-fns the first time a user opened a date field. The library's
1463
+ `SmartDateFnsAdapter` accepts both: it resolves `'hu'` / `'hu-HU'` / `'en'` / `'en-US'`
1464
+ (case-insensitively) to a date-fns locale, passes a real `Locale` object through untouched, and
1465
+ falls back to Hungarian with a `console.warn` for anything else.
1466
+
1467
+ So: **hosts on Hungarian or English need no change.** For any other language, provide a real
1468
+ date-fns locale:
1469
+
1470
+ ```ts
1471
+ import { de } from 'date-fns/locale';
1472
+ …
1473
+ { provide: MAT_DATE_LOCALE, useValue: de },
1474
+ ```
1475
+
1476
+ `SmartNgClientConfig.dateLocale` is unchanged — still a `string`, still defaulting to `'hu-HU'`.
1477
+
1478
+ #### 3. If you override `MAT_DATE_FORMATS`, your format strings will throw — and you probably want to delete the override
1479
+
1480
+ Measured across 12 hosts on 2026-07-28: three of them (**p014**, **p043**, **p009-angular**)
1481
+ provide their own formats next to the locale, and two more (`app-vlab`, `app-tournament`) have the
1482
+ same line commented out:
1483
+
1484
+ ```ts
1485
+ { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
1486
+ ```
1487
+
1488
+ with
1489
+
1490
+ ```ts
1491
+ export const MY_FORMATS = {
1492
+ parse: { dateInput: 'YYYY.MM.DD' },
1493
+ display: { dateInput: 'YYYY.MM.DD', monthYearLabel: 'YYYY',
1494
+ dateA11yLabel: 'LL', monthYearA11yLabel: 'YYYY' },
1495
+ };
1496
+ ```
1497
+
1498
+ Those are **moment tokens**. date-fns treats `YYYY`, `DD`, `YY` and `D` as *protected* and throws a
1499
+ `RangeError` on them, and its `LL` means something else entirely (stand-alone month, not a
1500
+ localized long date). So this override **crashes at runtime, on the first screen with a date
1501
+ field** — the same failure shape as the string locale above, and just as invisible to the compiler.
1502
+
1503
+ **The fix is almost certainly deletion.** That `MY_FORMATS` exists to render `YYYY.MM.DD`, which is
1504
+ exactly what the library's own `SMART_DATE_FORMATS` now produces (`2026.07.28.`). Drop the
1505
+ provider and the constant and you get the format you wanted, plus the more forgiving parsing.
1506
+
1507
+ If you genuinely need a different format, translate the tokens rather than copying them:
1508
+
1509
+ | moment | date-fns |
1510
+ |---|---|
1511
+ | `YYYY` | `yyyy` |
1512
+ | `DD` | `dd` |
1513
+ | `LL` | `PP` (or spell it out: `yyyy. MMMM d.`) |
1514
+ | `MMM YYYY` | `yyyy. LLL` |
1515
+
1516
+ Note that the library's `parse.dateInput` is an **array** — `DateFnsAdapter._parse` tries each entry
1517
+ in turn (and ISO-8601 first), so you can list several accepted spellings instead of one.
1518
+
1519
+ #### 4. `SmartNgClientConfig.useUtcDates` is removed
1520
+
1521
+ Delete it if you set it. No known host does. It existed for a client-configurable timezone that
1522
+ was abandoned as a requirement; the contract is now fixed and explicit:
1523
+
1524
+ > **The browser's zone on screen. Zulu (`…Z`) on the wire. The server converts.**
1525
+
1526
+ This is unchanged behaviour in practice — `useUtcDates` defaulted to `false`, and the moment
1527
+ adapter with `useUtc: false` behaves exactly like a plain `Date`.
1528
+
1529
+ #### 5. What a date widget's value now *is*
1530
+
1531
+ Form control values from the date pickers are plain **`Date`** objects, where they were
1532
+ `Moment` objects. The values reach your backend through a `UiActionRequest` param, so the wire
1533
+ format is `JSON.stringify` of that value — and `Date.toJSON()` emits the same ISO-8601 UTC string
1534
+ `Moment.toJSON()` did. **Nothing changes on the wire.** Only host code that read a value out of a
1535
+ form control and called a moment method on it (`.format()`, `.add()`, `.startOf()`) needs
1536
+ rewriting; the platform APIs never exposed a `Moment` in a signature.
1537
+
1538
+ #### 6. The one visible difference
1539
+
1540
+ Date inputs render **`2026.07.28.`** where 6.x rendered `2026.7.28.` — the documented `YYYY.MM.DD`
1541
+ convention. The old rendering was not a decision: `MAT_MOMENT_DATE_FORMATS` used moment's `'l'`,
1542
+ which strips the zero padding off the Hungarian `L`. Typed input is **more** forgiving than 6.x,
1543
+ not less: `2026.07.28.`, `2026.07.28`, `2026.7.28.`, `2026.7.28` and ISO-8601 are all accepted.
1544
+
1545
+ The month picker is unchanged (`07/2026`), and grid/table date columns were never affected —
1546
+ `SmartDatePipe` and friends extend Angular's own `DatePipe` and never used moment.
1547
+
1548
+ **Verified on a running host** (p014, 2026-07-29, live backend, `Europe/Budapest`). Picking
1549
+ 2026-07-15 in a `Kezdődátum` field gave, end to end:
1550
+
1551
+ | | |
1552
+ |---|---|
1553
+ | input | `2026.07.15.` |
1554
+ | widget value | `instanceof Date` — not a moment, not a luxon `DateTime` |
1555
+ | `JSON.stringify` of that value | `"2026-07-14T22:00:00.000Z"` |
1556
+ | `getHours()` | `0` |
1557
+
1558
+ That third row is the whole contract in one line: the value's `toJSON()` **is** the wire format
1559
+ (see §4), and it is zulu, while the screen and `getHours()` stay local — 15 July 00:00 CEST is
1560
+ 14 July 22:00 UTC. A grid column on the same host rendered `2026.04.20 0:03`, confirming the
1561
+ untouched pipe path.
1562
+
1563
+ ### HTTP: the library no longer forces the XHR backend (Phase 3.8)
1564
+
1565
+ `provideSmartNgClient()` used to pass `withXhr()` to its internal `provideHttpClient()` call,
1566
+ overriding the Angular 22 default on your behalf. It no longer does — the application runs on the
1567
+ framework default **fetch** backend.
1568
+
1569
+ For almost every host this is invisible. The one behavioural difference that matters: **the fetch
1570
+ backend does not support upload progress.** A request with `reportProgress: true` and a body
1571
+ throws `NG2824` instead of emitting `HttpUploadProgress` events.
1572
+
1573
+ Measured 2026-07-28 across 12 hosts: **`reportProgress: true` and `observe: 'events'` appear
1574
+ nowhere** outside the generated API services' unused parameters. So this is a theoretical risk for
1575
+ the current host set, not a live one. If *your* code does ask for upload progress, pass the
1576
+ feature back in:
1577
+
1578
+ ```ts
1579
+ import { withXhr } from '@angular/common/http';
1580
+
1581
+ provideSmartNgClient({ httpFeatures: [withXhr()] })
1582
+ ```
1583
+
1584
+ The upside is that the "**do not call `provideHttpClient()` twice**" trap this guide warned about
1585
+ is gone rather than merely documented: a second call now re-provides the same backend the library
1586
+ already uses. The library's interceptors were never at risk from it — they are `multi` providers.
1587
+
1588
+ ### `getActionDescriptor()` is synchronous (Phase 3.9)
1589
+
1590
+ `UiActionDescriptorService.getActionDescriptor(uiAction)` returns a `UiActionDescriptor`.
1591
+ It used to return a `Promise<UiActionDescriptor>`, and it was `async` for exactly one
1592
+ reason: the client-side translation layer that [Phase 3.7](#client-side-translation-is-gone)
1593
+ retired. Nothing it reads has been asynchronous since, so every caller was awaiting a value
1594
+ that was already there.
1595
+
1596
+ **If you only `await` the call, you are already fine.** `await` on a non-promise is legal
1597
+ TypeScript, so this compiles and behaves identically before and after. The codemod drops the
1598
+ `await` anyway, so the code stops claiming an asynchrony that is not there:
1599
+
1600
+ ```ts
1601
+ const descriptor = await this.uiActionDescriptor.getActionDescriptor(uiAction); // 6.x
1602
+ const descriptor = this.uiActionDescriptor.getActionDescriptor(uiAction); // 7.0
1603
+ ```
1604
+
1605
+ Two shapes genuinely break. Measured 2026-07-28 across the 12 hosts in scope: **5 hosts,
1606
+ 7 sites** — and none of them in a host's own `getActionDescriptor**s**()` map helper, which
1607
+ is an unrelated API and is left alone.
1608
+
1609
+ **1. A subclass that overrides it** — `TS2416`, because `Promise<UiActionDescriptor>` is not
1610
+ assignable to `UiActionDescriptor`. Found in three hosts, as a byte-identical
1611
+ `MdmUiActionDescriptorService`. **The codemod rewrites this one.**
1612
+
1613
+ ```ts
1614
+ // 6.x
1615
+ override async getActionDescriptor(uiAction: UiAction): Promise<UiActionDescriptor> {
1616
+ let d: UiActionDescriptor = await super.getActionDescriptor(basicUiAction);
1617
+ …
1618
+ }
1619
+
1620
+ // 7.0
1621
+ override getActionDescriptor(uiAction: UiAction): UiActionDescriptor {
1622
+ let d: UiActionDescriptor = super.getActionDescriptor(basicUiAction);
1623
+ …
1624
+ }
1625
+ ```
1626
+
1627
+ This is also the service you hand to a toolbar through `[uiActionDescriptorService]`, so a
1628
+ host with a custom one hits this at compile time rather than on screen.
1629
+
1630
+ **2. `.then(…)` on the result** — `TS2339`, because a descriptor has no `.then`. Found in
1631
+ two hosts, four sites. **The codemod reports this one and does not rewrite it**: turning a
1632
+ callback into straight-line code means moving its body, which is a judgment call. The fix is
1633
+ mechanical anyway — assign, then inline:
1634
+
1635
+ ```ts
1636
+ // 6.x
1637
+ constructForm() {
1638
+ this.actionDescriptorService.getActionDescriptor(this.uiAction).then((d) => {
1639
+ this.buttonTitle = d.title;
1640
+ this.buttonColor = d.color;
1641
+ });
1642
+ …
1643
+ }
1644
+
1645
+ // 7.0
1646
+ constructForm() {
1647
+ const d = this.actionDescriptorService.getActionDescriptor(this.uiAction);
1648
+ this.buttonTitle = d.title;
1649
+ this.buttonColor = d.color;
1650
+ …
1651
+ }
1652
+ ```
1653
+
1654
+ If the callback was the reason a field lived in a `signal` — because the value arrived after
1655
+ the first render — it no longer needs to. The library made that same simplification in its
1656
+ own three action dialogs and its snack bar.
1657
+
1658
+ **What went synchronous with it, inside the library:** the toolbar's descriptor resolution,
1659
+ `SmartGridComponent`'s header construction, and `SmartTreeGenericService.cacheActionDesciptors()`
1660
+ and **`syncTree()`**. `syncTree()` now returns `void`, which is what
1661
+ `SmartTreeServiceInterface` always declared; a host that calls it and discards the result —
1662
+ the only shape measured — needs no change, and a host that `await`s it still compiles.
1663
+
1664
+ #### The same cleanup, elsewhere: `SmartdialogService.closeDialog()` returns `void`
1665
+
1666
+ A sweep for the same shape found eight more library functions declared `async` with nothing
1667
+ asynchronous in them. Only one of them is on a surface a host touches:
1668
+ **`SmartdialogService.closeDialog(stopPropagate?)` returns `void`** instead of
1669
+ `Promise<void>`.
1670
+
1671
+ **Nothing to do.** Both measured hosts that extend `SmartdialogService` override this method
1672
+ as `override async closeDialog(): Promise<void>`, and that **still compiles**: TypeScript
1673
+ accepts any return type where the base declares `void`. `await this.closeDialog()` keeps
1674
+ compiling too. This is the difference from `getActionDescriptor` above — that one returns a
1675
+ *value*, which is why the same override shape fails there with `TS2416` and not here.
1676
+
1677
+ You may of course drop the `async` from your own override; nothing requires it.
1678
+
1679
+ ### Upload actions now wait for the server, and report their failures (Phase 3.9)
1680
+
1681
+ **This one changes behaviour, on purpose.** `SmartViewContextService.performUploadAction()`
1682
+ and `performUploadMultipleAction()` used to start the upload and resolve immediately,
1683
+ without waiting for it and without passing on its failure. Every other action in the family
1684
+ (`performAction`, `performWidgetAction`, `performWidgetMainAction`, `dataChanged`) always
1685
+ waited; the two upload ones were an omission.
1686
+
1687
+ Two things follow, and both are visible in a running app:
1688
+
1689
+ 1. **An upload action now blocks until the server has answered.** Before, the action was
1690
+ "done" the moment the request went out — so `UiActionService` showed its success snackbar
1691
+ over an upload that was still in flight, and anything the action was sequenced before ran
1692
+ against a view the server had not updated yet. Now the snackbar appears when the upload
1693
+ really has succeeded. On a large file that is a longer wait than 6.x showed you.
1694
+
1695
+ 2. **A failed upload now reaches your error handling.** The rejection used to be dropped —
1696
+ it became an unhandled promise rejection, `UiActionService`'s `catch` never ran, and the
1697
+ user was told the upload had worked. It now propagates, which means
1698
+ `setActionErrorHandler()` (or the default error dialog) will fire on upload failures it
1699
+ never fired on before. **If your host reports "new" upload errors after upgrading, they
1700
+ are not new** — they were happening silently.
1701
+
1702
+ Multi-file uploads also go out **one batch at a time** now. The batches were already being
1703
+ computed from the descriptor's `maxSize` and `maxBatchSize`, but they were then all sent at
1704
+ once, which put the whole set on the server simultaneously and defeated the split. If your
1705
+ backend measured upload concurrency, this is why it drops.
1706
+
1707
+ Nothing to change in host code.
1708
+
1709
+ ### New in 7.0: a grid row can render a backend layout (#29717)
1710
+
1711
+ A card-mode grid row that carries `layoutDescriptor.componentLayouts['GRID_ROW_LAYOUT']`
1712
+ renders that layout instead of the card component the host registered under
1713
+ `'<GRID_ID>Card'`. Both branches stay: a row without the descriptor behaves exactly as
1714
+ before, and this needs no host change.
1715
+
1716
+ Two consequences worth knowing if you style or extend it:
1717
+
1718
+ - The rendered layout's root element gets the `gridCardLayout` class, applied by the card to
1719
+ its own child rather than pushed into the `style.classesToAdd` of the object the backend
1720
+ sent. Nothing mutates the server model.
1721
+ - **The row's actions reach the layout's toolbars by a pull, not a walk.** The card provides
1722
+ a `SmartActionHost` for its row subtree and writes the row's action list into it; a toolbar
1723
+ inside the layout resolves that host before falling back to the page client. If you build
1724
+ a component that owns such a subtree, do the same — `WIDGETS.md`, *Toolbars*.
1725
+
1726
+ This shipped in 7.0.0 rather than as a 6.0.3x patch because it is built on the inverted
1727
+ widget contract that 7.0 introduces.
1728
+
1729
+ ### Table detail rows: rendered on demand, `example-*` classes renamed (7.0.10)
1730
+
1731
+ The Material table used to define its expandable detail row the way the Angular Material
1732
+ example does: a second `matRowDef` rendered under **every** data row, kept at zero height
1733
+ with `tr.example-detail-row { height: 0 }` and an `@angular/animations` trigger. Any host
1734
+ rule that gave table cells vertical padding, or rows a height, made those empty rows visible
1735
+ — the p043 report — and every table carried twice its rows in the DOM.
1736
+
1737
+ Now the detail row exists only in an expandable table (`SmartTable.expandable`), and only
1738
+ under the row that is expanded. There is nothing a host stylesheet can inflate. Expanding
1739
+ and collapsing animate through the native `animate.enter` / `animate.leave` of Angular 20.2+
1740
+ (a `grid-template-rows: 0fr ↔ 1fr` keyframe, as `mat-expansion-panel` does it), so the table
1741
+ no longer uses `@angular/animations` at all.
1742
+
1743
+ The classes lost their `example-` prefix, taken over from the Material sample:
1744
+
1745
+ | 6.x | 7.0.10 |
1746
+ |---|---|
1747
+ | `example-element-row` | `smart-table-row` |
1748
+ | `example-expanded-row` | `smart-table-row-expanded` |
1749
+ | `example-detail-row` | `smart-table-detail-row` |
1750
+ | `example-element-detail` | `smart-table-detail` |
1751
+
1752
+ `.example-element-diagram`, `-symbol`, `-description` and `-description-attribution` — the
1753
+ sample's own styles, referenced by nothing — are gone.
1754
+
1755
+ What to do in a host:
1756
+
1757
+ - Rename the classes in any stylesheet that targets them. The usual suspects are a hover
1758
+ rule on `tr.example-element-row:not(.example-expanded-row)` and a
1759
+ `:host ::ng-deep tr.example-detail-row { display: table-row !important }` in a page that
1760
+ expands rows; the latter can simply be deleted, the row is rendered when it is needed.
1761
+ - A data row now carries its own bottom border. Before, the always-present detail row drew
1762
+ it, and the data row's border was turned off to avoid a double line. Nothing changes on
1763
+ screen unless you drew your own row separators to compensate.
1764
+
1765
+ ### Writing your own widget
1766
+
1767
+ `WIDGETS.md`, next to this file, is the protocol: how a widget finds its screen component
1768
+ through DI and registers itself, when to opt out with `[smartComponentDetached]`, what has to
1769
+ be a signal now that there is no zone, how a toolbar resolves its actions, and the three
1770
+ traps that have actually bitten (imperatively-fed inputs cannot be signal inputs; `@for`
1771
+ tracking over backend objects; clear-then-apply styling).
1772
+
1773
+ In 6.x this was not possible at all — `SmartComponentApiClient` collected its children with
1774
+ eight `@ViewChildren` over eight concrete widget classes.
1775
+
1776
+ ### Imperatively created components are given their inputs properly
1777
+
1778
+ `ComponentFactoryService` creates the components the library instantiates by hand — grid
1779
+ cards, expanded rows, table cell components, the expandable section's content, the form's
1780
+ `COMPONENT` widget, the filter editor's field editors — and most of those classes come from a
1781
+ host. It used to write their inputs by assigning the field. Two things follow, and both are
1782
+ fixed:
1783
+
1784
+ - **A declared input is now written with `ref.setInput()`**, i.e. the same way a template
1785
+ binding writes it. So a **signal input** works (assignment used to overwrite the input
1786
+ function with the value, and the component's next `this.x()` threw `x is not a function`),
1787
+ an **aliased** input is addressable under either name, and **`ngOnChanges` runs**.
1788
+ - **Falsy values arrive.** The old guard was `if (value)`, so `false`, `0` and `''` never
1789
+ reached the component. Only `undefined` is skipped now, which keeps meaning "not passed".
1790
+
1791
+ If your component takes something from the library this way and the key is **not** a declared
1792
+ input, the field is still assigned — nothing breaks — but you get one `console.warn` per
1793
+ component type and key. Declaring it with `@Input()` (or as a signal input) is the fix.
1794
+
1795
+ **Check your falsy defaults.** If a host component relied on the old guard — expecting to
1796
+ keep its own default when the library passed `false` or `''` — it now receives the value.
1797
+ This is the only part of 7.0 where a bugfix can change what you see on screen without any
1798
+ code of yours changing.
1799
+
1800
+ ### Model sync: the client's models travel with every call (7.1.0)
1801
+
1802
+ Whether an action carried the page model used to be a per-action decision of the backend
1803
+ (`UiAction.submit` / `model` / `composite`) plus one hidden rule (an action with no params sent
1804
+ it anyway), and the model travelled under three ad-hoc `params` keys (`model`, `clientPageModel`,
1805
+ `compositeModel`) that only one endpoint wrote into the backend view automatically. The bug that
1806
+ forced the change: an action on an embedded view sent only its own model, the handler changed the
1807
+ container's model, the container's whole component model came back, and whatever the user had
1808
+ typed into the container since the last round-trip was gone.
1809
+
1810
+ From 7.1.0 the client sends, with **every** action, widget action, upload and grid selection
1811
+ call, the data of **every open view that changed since it was last synced** — slot-hosted and
1812
+ dialog views alike — in a new `UiActionRequest.modelSync.models` map keyed by view uuid. The
1813
+ backend writes each one into the matching view **before the handler runs and before it takes the
1814
+ base of its change push**, so a handler always sees what is on screen and the client's own values
1815
+ are never echoed back. A view equal to its last synced copy is left out; a view without a copy is
1816
+ always sent.
1817
+
1818
+ **Backend minimum, and the server switch.** The platform build that ships
1819
+ `UiActionRequest.modelSync` (platform ADR-0011 on the server side), **with model sync switched
1820
+ on**: `view.model-sync.enabled=true` in the host's `application.properties`. The switch defaults
1821
+ to `false`, and in that mode the server behaves exactly as before model sync existed: nothing is
1822
+ written into the views before the handler, and a request that carries `modelSync` is **rejected**
1823
+ with an error naming the property, not ignored. A 7.1.0 client sends the envelope with every
1824
+ call, so against a server with the switch off every call fails loudly; against an older platform
1825
+ the field is silently ignored and every handler that read `params.model` sees nothing. **Order:
1826
+ upgrade the backend, set the flag, then upgrade the client.** Server code can read the mode as
1827
+ `viewApi.isModelSyncEnabled()`.
1828
+
1829
+ **A 6.x client against a backend with the switch on** is rejected too: with
1830
+ `view.model-sync.enabled=true` every request that can carry `modelSync` — an action, a widget or
1831
+ upload action, a tree action, a grid selection change — must carry it, and one without it (an
1832
+ old client, or host code that builds a `UiActionRequest` by hand and calls the view context
1833
+ service directly) gets an error saying so. A call that cannot carry it (loading a view, a
1834
+ message result, expanding a tree node, a map interaction) is neither synced nor refused. The old `params.model` / `clientPageModel` / `compositeModel`
1835
+ keys are never read as a sync source; they are only *filled* by the backend, see below. So the
1836
+ switch and the client upgrade go together per host: flip the flag in the same deploy that ships
1837
+ the 7.1.0 client. Internal Java callers with nothing to sync send an empty `new ModelSync()`.
1838
+
1839
+ #### What changed on the client
1840
+
1841
+ - `UiAction.model` and `UiAction.composite` are **deprecated no-ops** for a view's own actions.
1842
+ `submit` keeps one meaning: validate before sending, the embedded views' forms included. For an
1843
+ executor that is not a view (a tree, the filter editor) `model` still flushes its form and its
1844
+ model still rides on `params.model` as before, because it has no view to sync.
1845
+ - Forms are flushed into their data (`submitForm(false)`) on every open view before a call, so an
1846
+ unsaved field value is part of what is compared and sent. **Only the user's edits are
1847
+ flushed**: the form remembers, per control, the value it last exchanged with the data (built
1848
+ or refreshed from it, or written into it), and a control still equal to that writes nothing.
1849
+ This is what keeps an untouched `false` or `0` from turning into `''` on a round trip (the
1850
+ control is now built with `value ?? ''`, and a value written back takes the type the field has
1851
+ in the data: `'true'` → `true`, `'12'` → `12`, `''` → `null`), and a backend push from
1852
+ reaching the data only to be overwritten by the control's stale value on the next call. The
1853
+ same rule the other way: a data field the backend changed reaches its control even when the
1854
+ user edited it (the backend wins on a field it changed), a field it did not change keeps what
1855
+ the user typed. `SmartFormService.resetExchangedValues()` starts over when a form is rebuilt.
1856
+ The form's own dirty flag is not involved: it lives until the form is rebuilt and says nothing
1857
+ about what the data holds.
1858
+ - A push of some `data.*` paths marks only those paths as synced; whatever else the view holds
1859
+ keeps its standing, so a value written into the data outside a form while a call was in flight
1860
+ is still sent by the next one. A whole-model push and the initial load mark all of it.
1861
+ - Calls run concurrently, and an envelope built earlier may complete later: its copies are never
1862
+ written over the copies a later envelope or push already settled. The server-side ordering of
1863
+ rapid per-field actions stays out of scope (ADR-0010).
1864
+ - A dialog the backend closed (`ViewState.TO_CLOSE`) no longer fires a `DEFAULT_CLOSE` action of
1865
+ its own on top of the response that closed it; only the user's own close does.
1866
+ - The per-field actions a widget's `valueChangeMode` fires are ordinary actions and inherit the
1867
+ sync; they no longer set `model: true`, and still carry `params.item`.
1868
+
1869
+ #### Removed from the public API
1870
+
1871
+ | Gone | What to do |
1872
+ |---|---|
1873
+ | `SmartComponentApiClient.sendModelOnWidgetAction` | Delete the assignment. Every widget action carries the changed models now. Loud: `this.sendModelOnWidgetAction = true` no longer compiles. |
1874
+ | `SmartComponentApiClient.addModelToWidgetParams()` / `addModelToWidgetAction()` | Delete. The grid service builds its own envelope. |
1875
+ | `SmartViewContextService.getCompositeModel()` | Delete. The backend gets every changed view. |
1876
+ | `SmartViewContextService.submitEmbeddedForms()` | `validateEmbeddedForms(containerUuid)` — validation only; the flush is part of every call. |
1877
+ | `SmartViewContextService.dataChanged()` | Delete. It was never wired on the client (`handleValueChanges` had no caller); a field change is a uiAction. |
1878
+
1879
+ New on `SmartViewContextService`: `joinModelSync(uuid, participant)` / `leaveModelSync(uuid,
1880
+ participant)` (the client does this on its own uuid; leaving only takes effect for the participant
1881
+ that is registered, so a component destroyed after its successor joined under the same uuid does
1882
+ not take the successor with it), `markModelSynced(uuid, keys?)` (the client calls it whenever the
1883
+ backend's model reached it: without keys the whole data, with keys the pushed `data` paths), and `withModelSync(carrier, call)` for a host that issues its own backend call on a request
1884
+ that can carry the envelope (a `UiActionRequest`, a `GridSelectionChange`): it puts the envelope
1885
+ on the carrier, runs the call with it and settles the copies on success, and hands back the raw
1886
+ `ViewContextChange` — what to do with the response stays with the caller. The grid and tree
1887
+ services are the in-tree examples; `beginModelSync()` / `completeModelSync()` underneath are
1888
+ public too.
1889
+
1890
+ #### The Java side of the host: three kinds of handler
1891
+
1892
+ The backend **backfills** the old `params` keys from the synced model on every request (with the
1893
+ switch on, every request carries `modelSync`), so no handler has to change on the day the client
1894
+ is upgraded. Two kinds must be
1895
+ looked at anyway, and one of them breaks without a compiler error. Run
1896
+ `node tools/model-sync-audit/cli.mjs ../<host>` — it lists every read of the request model with
1897
+ one of these categories and a reason ([its README](../../tools/model-sync-audit/README.md)):
1898
+
1899
+ | | The handler | What to do |
1900
+ |---|---|---|
1901
+ | **A** | reads `params.model` (`extractClientModel(request)`, `actionRequestHelper(request).get(UiActions.MODEL, …)`) and only passes it to `setModel(viewUuid, …)` | Delete both statements. The sync already wrote the client's model into the view. |
1902
+ | **B** | reads it for something else: saves it, validates it, forwards it, mutates it before setting it back | Nothing required. `getModel(viewUuid)` is the honest read now; `extractClientModel` is `@Deprecated`. A grid row action's `params.model` is still the clicked row and is never synced. |
1903
+ | **C** | reads the view's model too (`getModel(viewUuid)`, `view.getModel()`) and uses the two together — a diff, a merge, "what changed" | **Breaks silently.** After the sync `getModel` *is* the client's model, so the comparison compares a thing with itself. The server-side read must become `viewApi.isModelSyncEnabled() ? getPreviousModel(viewUuid) : getModel(viewUuid)`: the model as the server knew it before this call, request-scoped (`viewApi.getPreviousModel(uuid, Class)` outside a `PageApiImpl`). The off branch is the old code. |
1904
+
1905
+ A handler that must work in both server modes — a shared module deployed to hosts with the
1906
+ switch off as well as on — branches inline on `viewApi.isModelSyncEnabled()` and keeps its old
1907
+ code verbatim in the off branch: an A site becomes
1908
+ `if (!viewApi.isModelSyncEnabled()) { setModel(viewUuid, extractClientModel(request)); }`, a
1909
+ client read `viewApi.isModelSyncEnabled() ? getModel(viewUuid) : extractClientModel(request)`, a
1910
+ C site's server read the ternary above. No wrapper helper on purpose: once every host runs with
1911
+ the switch on, the off branches are deleted line by line and what remains is the plain new code.
1912
+ The platform's own handlers are written this way. `getPreviousModel` with the switch off returns
1913
+ `getModel` and logs an ERROR: that code assumes a mode the server is not running in.
1914
+
1915
+ Two traps the audit cannot see:
1916
+
1917
+ - A handler that forwards the *request* to a callback which reads `params.model` (the platform's
1918
+ password editor does this) depends on `extractClientModel` writing the typed instance back into
1919
+ the params. Keep `extractClientModel` there, or hand the model over explicitly.
1920
+ - A handler that reads `compositeModel` by hand gets it backfilled only when its action still
1921
+ has `composite=true`. Read the children's models from `viewApi` instead; the flag goes away
1922
+ once no host reads the key.
1923
+
1924
+ The measured spread, over the 56 host repositories that were audited when this shipped: 1061 read
1925
+ sites, 301 A, 671 B, 77 C.
1926
+
1927
+ ### New in 7.2: HTML widget, row layouts, hydrated toolbars (#30388)
1928
+
1929
+ Additive, with **one behaviour change** and one removed field. The reference is
1930
+ `HTML-WIDGET.md`, shipped in the package next to this file.
1931
+
1932
+ - **`SmartFormWidgetType.HTML`**: server-sent html whose client placeholders (`{{…}}`) the client
1933
+ resolves against the model, with toolbars hydrated into its toolbar slots
1934
+ (`<smart-ui-action-toolbar data-sb4-toolbar="…">`) and action triggers (`data-sb4-action`) on
1935
+ any element. The `DIV` widget is untouched. A host that never sends the widget sees no change.
1936
+ The widget can bring its own style sheet (`HtmlProperties.css`), which the client adds to the
1937
+ document head once per distinct text; a host with a nonce-based `style-src` provides
1938
+ Angular's `CSP_NONCE`. A style sheet can animate new html coming in and, through the
1939
+ `sb4-html-leaving` class the client puts on the widget or cell before it replaces the html,
1940
+ going out.
1941
+ Inside a `{{#section}}` a placeholder reads the surrounding scope only when it says so,
1942
+ `{{../title}}`: one step per entering section, never above the widget's root.
1943
+ - **Icon slots (in the tree, released after 7.2.4)**: an element in the html marked with
1944
+ `data-sb4-icon` hydrates into a
1945
+ `smart-icon` — the same component a button or a cell uses. The marker addresses a name
1946
+ (`data-sb4-icon="mail"`, coloured by `data-sb4-icon-color`, a theme or host class name), an
1947
+ `ImageResource` (a placeholder that resolves to one, or the json `Tag.icon(resource)` writes
1948
+ in), or a key of the template's icon dictionary (`data-sb4-icon-key`,
1949
+ `HtmlProperties.icons`), which sends a finite type-to-icon mapping once with the template
1950
+ rather than once per row. An address that resolves to nothing renders no icon and says
1951
+ nothing; an unknown dictionary key warns. Nothing changes for a host that sends no icon slot.
1952
+ - **Row layouts**: a cards grid can carry its card layouts once per grid
1953
+ (`GridViewDescriptor.rowLayouts`, picked per row by `GridRow.rowLayout`) instead of once per
1954
+ row. The per-row `layoutDescriptor.GRID_ROW_LAYOUT` still works and wins over them; a
1955
+ host-registered `'<GRID_ID>Card'` component still renders the rows that get no layout.
1956
+ - **Clicking a card rendered from a layout — 7.2.0 changed it, 7.2.1 takes the change back.**
1957
+ 7.2.0 made a click select the row and moved the default row action to the double click. That
1958
+ broke `mod-contact`'s contact list, and a cards grid naming no `selectionMode` lost its click
1959
+ altogether. **From 7.2.1 a card behaves as in 7.1.x again unless the server says otherwise**: a
1960
+ click fires the row's first applicable default action, and nothing is selected by click while
1961
+ the click belongs to the default action. A host that put a root trigger on its cards for 7.2.0
1962
+ can keep it; it is no longer needed.
1963
+ - **`GridViewDescriptor.defaultRowActionTrigger` (7.2.1, `click` | `doubleClick`)** makes the
1964
+ gesture data, so a cards grid is programmed like a table: `selectionMode` decides whether rows
1965
+ are selectable, the trigger decides what fires the default row action. With `doubleClick` a
1966
+ click selects the card and a double click fires the default action, as on a `ROW_SELECT` table.
1967
+ With an explicit trigger several applicable default actions open a menu at the pointer, filtered
1968
+ to what the row offers. Without a value a card keeps the click and a table the double click; in
1969
+ 7.2 a table fires on double click whatever the value is (7.3 honours an explicit `click`, see
1970
+ below). The second click of a double click is not a
1971
+ click of its own on a card: it neither fires the default action again nor toggles the selection
1972
+ back, and further clicks are dropped while the default action runs. Needs a platform that knows
1973
+ the field; an older client ignores it.
1974
+ - **The double-click menu of a table works again.** A grid row offering several of the grid's
1975
+ `defaultRowActions` opens a menu at the pointer on double click. The position override behind it
1976
+ had been written for an older Material signature and threw on open, so until 7.2.1 such a double
1977
+ click did nothing.
1978
+ - **`SmartGrid.layoutComponent` is removed.** The card gets the layout class through the
1979
+ `SMART_ROW_LAYOUT_COMPONENT` token that `provideSmartNgClient()` provides, so a cards grid a
1980
+ host builds with `addGrid()` renders backend layouts too. Delete the field if a host grid model
1981
+ sets it; nothing else changes.
1982
+ - A toolbar no longer lets a `dblclick` on its buttons through to the element around it (it
1983
+ already kept the `click`).
1984
+ - The toolbar's re-entry guard is per action now: while one action runs, another button of the
1985
+ same toolbar can be fired. It is also released when the action fails.
1986
+ - **Trusted Types**: the HTML widget writes backend-authored html as it is, as the `DIV` widget
1987
+ always has. A host that enforces Trusted Types must allow that write.
1988
+ - **Table cells**: `GridColumnMeta.contentType` now reaches the table. A column that names none
1989
+ (or `html`) renders exactly as before, sanitized; `text` escapes the value. A column with
1990
+ `htmlProperties` renders a column template — the HTML widget's dialect, resolved per row, with
1991
+ toolbar slots and triggers offering the row's actions. A row action addressed to
1992
+ `UiActions.NO_TOOLBAR` no longer shows in the row menu; in a table with column templates no row
1993
+ action addressed to a toolbar does.
1994
+
1995
+ ### New in 7.3: object navigation and editing screens (#23616)
1996
+
1997
+ Additive for a host that uses the components as they are, with **visible style changes in every
1998
+ host**; a host that calls `TreeService.selectNode2` or extends `Table` has source changes (see
1999
+ "Source changes" at the end of this section). Needs the matching platform (`TreeSelectionRequest`,
2000
+ `ViewContext.sequence`); release the two together. With an older platform the edits a tree
2001
+ selection carried are not sent again and late responses are always applied.
2002
+
2003
+ - **Theme token defaults.** The first `ThemeService` (every host has one through
2004
+ `SmartViewContextService`) puts a `<style id="sb4-theme-defaults">` first in `<head>`: light
2005
+ values of the library's tokens on `:root`, dark values under `.theme-dark`. Tokens a host sets
2006
+ on `body` or `:root` win; tokens set on `html { }` do not, so move them to `body`. The defaults
2007
+ cover 14 tokens (`--color-surface(-alt)`, `--color-text(-muted)`, `--color-border`,
2008
+ `--primary / secondary / accent / warn / success / warning / info-color`,
2009
+ `--primary-text-color`, `--tree-active-color`). The optional tokens the library also reads
2010
+ (`--tree-active-background`, `--grid-open-row-background`, `--grid-open-row-bar`,
2011
+ `--primary-light-color` and the other old names below) get no default and fall back where they
2012
+ are read, for example `--tree-active-background` to `--accent-color`. A token the host never
2013
+ defined now has a value, for example `--tree-active-color: #ffffff`. A host with a
2014
+ nonce-based `style-src` provides Angular's `CSP_NONCE`, which the element then carries.
2015
+ - **Dark defaults** stand under `.theme-dark` only (with `color-scheme: dark`), so they apply
2016
+ only when the host offers a theme named `dark`. A host that sets its light tokens on a plain
2017
+ `body` hides them and ships its dark theme with the full token set.
2018
+ - **Components read the theme tokens.** The menus, action buttons, tables, the mobile table and
2019
+ the grid take their colours from `--color-surface`, `--color-surface-alt`, `--color-text`,
2020
+ `--color-text-muted`, `--color-border`, `--primary-color` and `--primary-text-color` instead of
2021
+ fixed values. A host without these tokens sees the defaults above, which are close to, but not
2022
+ exactly, the old colours (menu text `black` → `#132033`, disabled `#aaa` → `#68778e`, table
2023
+ hover `whitesmoke` → `#f4f6fb`). The old names `--primary-light-color` (selected table row),
2024
+ `--disabled` (disabled row), `--light-gray` (grid edit icon), `--mat-sys-primary` (selected
2025
+ card outline) and `--primary-color-text` (activated view button) are still honoured before the
2026
+ new ones. The grid overlay uses relative colour syntax (`rgb(from …)`, Chrome 119 / Safari
2027
+ 16.4 / Firefox 128), inside Angular 22's browser floor.
2028
+ - **Row states.** `GridRow.rowState` is rendered as the row class `sm-grid-row-<state>`
2029
+ (`new`, `modified`, `deleted`, `moved`, `contains-change`, or an application code in lower
2030
+ case) on table, mobile-table and card rows, with default styles: new green italic, modified
2031
+ accent bold, deleted muted with struck-through cell text (`.smart-table-cell-text`), moved
2032
+ italic, contains-change the accent colour only. A card only takes the colour: a deleted card is
2033
+ muted, nothing is struck through. A selected row keeps its normal text colour. Any grid whose
2034
+ server already fills `rowState` shows them.
2035
+ - **Row state column.** `GridViewDescriptor.rowStateDisplay` (`GridRowStateDisplay`) picks how a
2036
+ table and a mobile table show the states: `STYLE` (the default when absent) the row classes
2037
+ above, `COLUMN` a first column of state icons without the row classes, `BOTH` the two
2038
+ together. The column shows an icon for the platform states (an application state has none),
2039
+ with the tooltip and accessible name from `rowStateLabels` (by state code); the column itself is
2040
+ named by `rowStateColumnLabel` and has no header text. It is not a descriptor column: not
2041
+ editable, not in the column chooser. The platform fills both labels in the session's locale.
2042
+ A cards grid has no column and keeps the row classes whatever the value.
2043
+ - **Mark of the open row.** A grid whose `GridViewDescriptor.markOpenRow` is true gives the row
2044
+ of `GridPage.openRowId` the class `sm-grid-row-open` (table and mobile table): a light accent
2045
+ tint and a bar at the row's start, restyled by `--grid-open-row-background` and
2046
+ `--grid-open-row-bar`. A default row action, from a double click, a click or the row menu, moves
2047
+ the mark at once; a grid page requested before the action's answer keeps the new mark, the first
2048
+ one requested after it brings the server's `openRowId`.
2049
+ - **`defaultRowActionTrigger: click` on a table.** A table now honours an explicit `click`: a click
2050
+ on a row with an applicable default action fires it (several open the menu at the pointer), does
2051
+ not select the row, and a double click does nothing more. A click on a button, link or input in
2052
+ the row stays the control's own. Without a value or with `doubleClick` nothing changes. Table
2053
+ rows are tracked by their id, so a reloaded row keeps its elements.
2054
+ - **Tree grid controls.** The header of a TREE_GRID shows expand all / expand level / collapse
2055
+ level / collapse all as small icon buttons, disabled when there is nothing to do. The server
2056
+ picks and orders them with `GridViewDescriptor.treeGridControls` (`TreeGridControl`): absent or
2057
+ null shows all four, `[]` none (before: two mini-fab buttons).
2058
+ - **Trees.**
2059
+ - A tree reload keeps the node objects of the nodes it still has and writes the new data
2060
+ into them. Rows survive a reload, a click that started before it still lands, and
2061
+ `smartTreeModelChanged` fires once per reload (before: twice, first empty).
2062
+ - A node keeps the icon the backend sends; a node without one gets `Folders` if the host
2063
+ registered it, Material `folder` otherwise (before: always `Folders`).
2064
+ - A node action with its own `descriptor.title` is labelled by it.
2065
+ - The caption shows the short description as a tooltip.
2066
+ - Without a `[treeStyle]` a row is indented 15px per level and the selected row gets
2067
+ `sm-tree-node-selected` (`--tree-active-background`, else `--accent-color` /
2068
+ `--tree-active-color`); its caption and short description take the colour with
2069
+ `color: inherit`, so a host rule on every element (`* { color: … }`) does not reach them.
2070
+ With a `treeStyle` nothing changes.
2071
+ - Server classes `sm-tree-branch-new|modified|deleted|foreign|own-other-view-context` have
2072
+ default styles.
2073
+ - **Tree selection syncs the models.** Selecting a tree node sends the client's changed models
2074
+ like an action does (`TreeSelectionRequest.modelSync`). Needs the matching platform:
2075
+ an older one ignores the body and the edits it carried are not sent again.
2076
+ - **Tree widgets in a server layout** provision their own tree service from the widget
2077
+ identifier when the screen's default tree is a different one or there is none, so a generic
2078
+ page can show a tree and a layout can hold several.
2079
+ - **Late responses.** The server raises `ViewContext.sequence` on every save of the view
2080
+ context; the response of an action or of a tree selection whose view context is not newer
2081
+ than the one the client holds no longer overwrites it, its changes are still applied. Needs
2082
+ the matching platform: an older platform sends no sequence and its responses are always
2083
+ applied. Such a response first waits for the view-context updates the client still has in
2084
+ flight, and their answers count in the comparison (they are applied as before, without the
2085
+ guard). A response that is not applied still closes the views it marks `TO_CLOSE`.
2086
+ - **Views entering a slot.** When one response closes an embedded view and opens another in
2087
+ the same slot, the two are exchanged in one step: the old component stays (inert, cut off
2088
+ from the server) until the new one has its model, at most 3 s. Every view opened into a slot
2089
+ of a server layout likewise stays hidden until its model arrives, so the layout beside it
2090
+ changes once. Showing such a view fires one `window` `resize` event, so content that measured
2091
+ itself while hidden (textarea autosize, scrolling toolbars) measures again.
2092
+ - **Empty slots.** An embedded slot of a layout that shows no view carries `sm-slot-empty`; an
2093
+ empty `sm-nav-pane` takes no width, so the list beside it has the full width.
2094
+ - **Focus after a pane closes.** When the view of an `sm-nav-pane` closes and the focus went
2095
+ with it, the focus moves to the open row (`sm-grid-row-open`) of the `sm-nav-list` beside the
2096
+ pane, else to the row that was open while the pane showed the view, else to the list.
2097
+ - **Navigation frame styles.** Default layout for the server classes `sm-nav-view`,
2098
+ `sm-nav-pane`, `sm-nav-list`, `sm-nav-header`, `sm-nav-caption`, `sm-nav-section(-title)`,
2099
+ `sm-nav-tree`, `sm-nav-slot`, `sm-nav-crumbs`, `sm-crumb-ahead`, `sm-crumb-current`; a host may
2100
+ restyle them by the same names. `sm-nav-header` puts the caption of a pane view and the toolbar
2101
+ with the pane's close on one line. Inside `.sm-nav-list` a grid toolbar without a button is
2102
+ hidden and the grid has no top padding; the search page container has a 0.5rem gap there.
2103
+ - **Icon buttons** get an `aria-label`: the action's title, else its tooltip. A button that shows
2104
+ its title is named by it and gets none.
2105
+ - **Row actions in a table's action column** stand side by side; the toolbar no longer wraps
2106
+ in the narrow column (row actions the server shows as icons).
2107
+ - **Prerendered pages.** A page shown after browser prerendering keeps its view context uuid
2108
+ (sessionStorage written during prerendering is dropped; the client restores it from the request
2109
+ header), so a tree on it loads its nodes.
2110
+ - **Search page title**: `.search-page-title` is 1.35rem / 600 / no margin on every search
2111
+ page, a deliberate global change.
2112
+ - **`--primary-color` fallback.** Where the menus and the activated view button fall back
2113
+ without the token, the value is `#384372` instead of `#1976d2`; the tree grid header buttons
2114
+ are 28px squares.
2115
+ - New public API: `GridViewDescriptor.treeGridControls`, enum `TreeGridControl`,
2116
+ `SmartTableInterface.treeGridControls`, `SmartIconService.hasCustomIcon()`,
2117
+ `ViewContext.sequence`, `ViewContextData.sequence`, `SmartTreeComponent.trackByIdentifier`,
2118
+ `SmartGridComponent.calcRowClasses()`; `GridViewDescriptor.rowStateDisplay`, `rowStateLabels`,
2119
+ `rowStateColumnLabel`, `markOpenRow`, enum `GridRowStateDisplay`, `GridPage.openRowId`;
2120
+ `SmartTableInterface.defaultActionOnClick`, `markOpenRow`, `openRowId`, `rowStateDisplay`,
2121
+ `rowStateLabels`, `rowStateColumnLabel`, `onRowAction`; `UiActionToolbarComponent.actionFired`;
2122
+ `EmbeddedSlotProperties` (`singleView`: the server closes the view standing in the slot before
2123
+ it opens the next; generated for completeness, the client does not read it).
2124
+ - **Source changes** for a host that calls these members or extends the classes:
2125
+ - `TreeService.selectNode2(viewUuid, treeId, nodeId, treeSelectionRequest?, observe?, …)`: the
2126
+ optional `TreeSelectionRequest` stands before `observe` in every overload, and the call sends
2127
+ a JSON body. A call that passed `observe` in the fourth place does not compile; insert
2128
+ `undefined` or the request.
2129
+ - `Table` (the base of the table components): `handleOnRowClick(element)` became
2130
+ `handleOnRowClick(event: MouseEvent, element)`; the getters `expandTreeGridLevel` and
2131
+ `closeTreeGridLevel` are gone (the controls come from `treeGridControls`); the rows and the
2132
+ header render `displayedColumns` instead of `smartTable.tableHeaders`, and the expanded
2133
+ detail row spans `displayedColumns.length`. New members: `trackRow`, `rowStateColumn`,
2134
+ `onRowActionFired`. A host template copied from the library's must follow.
2135
+ - `SmartTreeComponent.treeControl`'s children accessor returns an `Observable` of the children,
2136
+ not the array. `SmarttreeGenericService` writes a reload into the node objects a host holds
2137
+ (same objects, new data) and sets `treeFromBackend` to the merged nodes.
2138
+
2139
+ ### New in 7.3.2: keybindings (#19122)
2140
+
2141
+ Additive: a host without a keymap gets one binding, Escape closes a dialog that offers
2142
+ `DEFAULT_CLOSE`. Needs the matching platform (`Keybinding`, `View.keybindings`,
2143
+ `ComponentModel.keybindings`); release the two together.
2144
+
2145
+ - **The keymap.** Properties files on the server, `CODE=chord[,chord]` binds every action of
2146
+ that code, `viewName.CODE=chord[,chord]` only in that view and wins over `CODE`; an empty
2147
+ `viewName.CODE=` disables the chord in that view. The host lists its own file before the
2148
+ platform's, the first file with the key wins:
2149
+ `spring.keybindings.basename=keybindings-<app>,keybindings-platform`. The platform's file binds
2150
+ Escape to `DEFAULT_CLOSE`, `CANCEL` and `CLOSE_SEARCH_PAGE`, which close the view on every
2151
+ platform page offering them; without the property only that file is read.
2152
+ - **Chord grammar.** Modifiers, then one key, joined by `+`: `Mod+Shift+S`. `Mod` is Cmd on a Mac
2153
+ and Ctrl elsewhere, so one chord serves both; `Ctrl`, `Alt`, `Shift`, `Meta` are the literal
2154
+ keys. The server writes every chord canonically (modifier order `Mod`, `Ctrl`, `Alt`, `Shift`,
2155
+ `Meta`, letters upper case), so the order and case in the file do not matter. The key is the
2156
+ browser's `KeyboardEvent.key` name (`Escape`, `Enter`, `ArrowUp`, `F2`, `Delete`), the space
2157
+ bar is `Space`. A letter or digit also matches by its physical key when a modifier changes the
2158
+ character (Option+N on a Mac). Chords the browser keeps for itself (`Mod`/`Ctrl` with `W`,
2159
+ `T`, `N`, `Q`, `R`, `Tab`, and `F5`) and unparsable ones are dropped with an ERROR log.
2160
+ - **Bindings in code.** A page binds its own actions on the view, typically in `initModel`:
2161
+ ```java
2162
+ view.addKeybindingsItem(new Keybinding()
2163
+ .chord(KeyChord.of(KeyboardKeys.MOD, KeyboardKeys.SHIFT, KeyboardKeys.ARROW_UP))
2164
+ .uiActionCode(MOVE_UP)
2165
+ .uiActionIdentifier(rowId)); // optional: without it every MOVE_UP action is bound
2166
+ ```
2167
+ Per action the code level wins over `viewName.CODE`, which wins over `CODE`; a code-level entry
2168
+ replaces the keymap of its action, they are not merged. Only the code level can name an
2169
+ identifier. When two actions of a view share a chord, the more specific source keeps it, then
2170
+ the earlier action, with a WARN log. The server resolves all this into
2171
+ `ComponentModel.keybindings`; a keymap entry of an action the view does not offer yields
2172
+ nothing.
2173
+ - **Which view gets the key.** One document listener serves every view. The key goes to the view
2174
+ holding the focus, the innermost one when views are nested; when nothing has the focus, to the
2175
+ view that had it last (the focus is lost when a pushed layout re-renders the field), else the
2176
+ top-most dialog, else the current page. If that view has no enabled binding for the chord, the key
2177
+ bubbles to the enclosing views, but never out of a dialog: the page behind it does not react
2178
+ while a dialog is open. A NORMAL view opened by the user's action takes the focus, a view
2179
+ restored on reload does not.
2180
+ - **What does not fire.** Unmodified typing in an editable element (text inputs, textareas,
2181
+ selects, contenteditable): letters, digits, editing and caret keys, and `Enter` in a multi-line
2182
+ field stay the element's; `Escape`, the function keys, chords with Ctrl, Alt or Cmd, and `Enter`
2183
+ in a single-line input reach the bindings. A disabled or not offered action does not fire: the
2184
+ key bubbles on and reaches the browser when nothing takes it. Auto-repeated keys, a key a panel
2185
+ already handled (`defaultPrevented`) and a key pressed in an overlay of no view (an open select
2186
+ or menu panel) fire nothing. An unmodified `Enter` or `Space` on a focused button, link or
2187
+ ARIA-interactive control stays with that control (the browser activates it), and the `Enter`
2188
+ that confirms an IME composition fires nothing. A fired chord is `preventDefault`ed.
2189
+ - **Tooltip and accessibility.** A toolbar button shows its chords in its tooltip, formatted for
2190
+ the platform (`⌘⇧S` on a Mac, `Ctrl+Shift+S` elsewhere), after the server tooltip or alone; the
2191
+ tooltip also opens on keyboard focus. The button names them in `aria-keyshortcuts`.
2192
+ - **Two host rules.** Do not bind `Enter` on a view whose form uses `SmartForm.submitOnEnter`:
2193
+ both would run. With `dialogDisableClose: false` MatDialog closes the dialog on Escape itself,
2194
+ as before; the `DEFAULT_CLOSE=Escape` binding is what closes it under the default `true`.
2195
+ - **Focus survives a push.** When a push re-renders the field being edited, the view focuses it
2196
+ again: the field with the same widget key (`data-testid`) at the same position within the view,
2197
+ with the caret or selection of a native input or textarea and of a Quill editor. Nothing
2198
+ happens when the field left the layout or the focus moved elsewhere meanwhile (a dialog, a click).
2199
+ - **Pending edits are saved first.** A fired action (a key, a toolbar click, an HTML trigger)
2200
+ waits until the view's widgets have saved what was typed into them — today the filter editor,
2201
+ which saves its fields on blur — so a query bound to `Enter` runs with the value just typed. If
2202
+ that save fails, the action is not sent.
2203
+ - **Messages.** A message with a single option focuses it and sends it on Escape. Otherwise the
2204
+ first `confirm`-typed option gets the initial focus (Enter presses it) and Escape sends the first
2205
+ `cancel`-typed option. A `reject` option is never chosen by a key, and untyped options (a YES/NO
2206
+ without types) get neither: Escape does nothing, the focus stays on the first button.
2207
+ - **Out of scope.** Row actions (grid, table, card rows) and actions inside widgets have no
2208
+ keybindings; only the view's own actions do.
2209
+ - **New in the public API:** model `Keybinding`, `View.keybindings`,
2210
+ `ComponentModel.keybindings`; Java `KeyChord`, `KeyboardKeys`, `KeybindingSettingApi`.
2211
+
2212
+ ### New in 7.3.5: a stored layout names its HTML template by key (#30388)
2213
+
2214
+ Additive, and needs the matching platform. Nothing changes for a layout that names no key.
2215
+
2216
+ - **`HtmlProperties.templateKey`.** An HTML widget of a stored layout (an
2217
+ `ObjectLayoutDescriptor`) names the template its html and style sheet come from:
2218
+ `h.templateKey("CONTACT_CARD")` in code, `html card { template: CONTACT_CARD }` in the layout
2219
+ DSL. The key stays in the widget and wins over a stored html.
2220
+ - **`resolvedCopy(ctx, viewName, layout)`** (`PageApiImpl`, `ObjectLayouts`) is how a stored
2221
+ layout goes onto a view: a deep copy with the keys loaded and the server placeholders resolved.
2222
+ The platform's own paths use it (`ObjectLayoutDescriptorUtilApi.initLayoutDescriptor`, the MDM
2223
+ entry editor, object navigation). **A host that puts `descriptor.getLayouts()` or the layout of
2224
+ `getObjectDisplay` onto a view itself has to call it** before it stores a key in such a layout.
2225
+ - **A widget whose key was never loaded renders nothing** and logs one console warning naming
2226
+ the key.
2227
+ - **Behaviour changes on the server.** `ObjectLayoutDescriptorUtilApi.initLayoutDescriptor` now
2228
+ puts a copy of the descriptor's layout on the view, not the descriptor's own instance, and
2229
+ fails on an html with a server placeholder (`${alias:/…}`) it has no context for — pass the
2230
+ context with the new `initLayoutDescriptor(view, descriptor, ctx)`. `NavigationNodeLayoutInternalApi
2231
+ .createBodyLayoutWithFieldEdges` takes the view name as its first argument.
2232
+ `PageApiImpl.gridBuilder(...)` hands the template resolver to the `GridBuilder`, so
2233
+ `htmlColumn(c -> c.template(ctx, KEY))` works.
2234
+ - **New in the public API:** `HtmlProperties.templateKey`; Java `HtmlWidgetBuilder.templateKey`,
2235
+ `ObjectLayouts.resolvedCopy` / `templateKeys` / `unloadedTemplateKeys`, the `viewName` variants
2236
+ of `resolveHtml`, `DataSheetPageApi.CONTEXT_DATA_SHEET`. See HTML-WIDGET.md, *Template-kulcs egy
2237
+ tárolt layoutban*.
2238
+
2239
+ ### Bugfixes shipped with 7.0
2240
+
2241
+ - (7.3.2) A view dialog no longer disappears on the browser's back button. MatDialog's default
2242
+ `closeOnNavigation` disposed every dialog on a Location change without any action, so the
2243
+ backend kept the views open and a reload brought them back; view dialogs now open with it off.
2244
+ - `SmartformwidgetComponent.ngAfterViewInit` no longer crashes with
2245
+ `Cannot read properties of undefined (reading 'valueChanges')` when the form
2246
+ contains a `MONTH_PICKER` widget (it wrongly subscribed to the non-existent
2247
+ `<key>-time` control; only `DATE_TIME_PICKER` has one).
2248
+ - A widget nested in a CONTAINER receives `blurSophisticatedValueChange`; the recursion never
2249
+ passed it down, so a BLUR-mode child would have thrown on blur.
2250
+ - `SmartformComponent` reaches widgets nested in a CONTAINER when it changes them in place
2251
+ (values, constraints, the touched state after a failed submit). The view query it used
2252
+ could not see them.
2253
+ - `UiActionDescriptorService` no longer shares one dialog object between actions. Any action
2254
+ whose descriptor had no `dialog` of its own was given the service's single placeholder — the
2255
+ same object every time — and had its title and button caption written into it, so resolving
2256
+ one action renamed the dialog every other one was holding. The service is
2257
+ `providedIn: 'root'`, so this outlasted the view.
2258
+
2259
+ What you saw on screen, measured on both versions:
2260
+
2261
+ | The action | 6.x, when the dialog opened | 7.0 |
2262
+ |---|---|---|
2263
+ | described by the backend, no dialog of its own | **another action's code** | its own code |
2264
+ | described by nothing at all | **another action's code**, or empty | empty |
2265
+ | registered client-side, no dialog of its own | its own code | unchanged |
2266
+
2267
+ The middle column is not a typo. An action the backend describes is resolved twice in the
2268
+ normal flow — once when the toolbar renders it, again when the dialog it opens resolves it
2269
+ for itself — and the first resolution wrote a dialog onto the action's own descriptor, so the
2270
+ second one skipped the step that fills in the title. What the dialog then showed was whatever
2271
+ code had last passed through the shared object. An **already open** dialog could change too,
2272
+ since its title is re-read on every change-detection pass.
2273
+
2274
+ An action nothing describes still gets an untitled placeholder dialog: its *button* carries
2275
+ the code, the dialog does not. That is unchanged and deliberate — such an action is a gap in
2276
+ the host's descriptor map, and this is the honest rendering of it.
2277
+
2278
+ A resolved descriptor is now the caller's own object throughout, which also means
2279
+ `getActionDescriptor()` no longer writes a placeholder dialog and a `SNACKBAR` feedbackType
2280
+ into the `descriptor` the backend put on the action itself.