@smartbit4all/ng-client 7.0.7 → 7.0.10

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,1722 +1,1758 @@
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).
466
-
467
- ### Angular 20 hop (Phase 2, 19 → 20)
468
-
469
- The library line now builds against Angular 20. Host-relevant changes:
470
-
471
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
472
- `@angular/*` `^20.0.0`, `@angular/youtube-player` `^20.0.0`,
473
- `ngx-quill` `^28.0.0` (the Angular 20 line; quill stays `^2.0.3`),
474
- `ngx-mask` `^20.0.0`, and — for `@smartbit4all/document-explorer` —
475
- `ngx-extended-pdf-viewer` `^23.0.0` (the 22.x line caps its Angular peer
476
- below 20). No pdf-viewer API rename at this bump (unlike 22.x).
477
- - **TypeScript 5.9 required** (Angular 20 supports 5.8–5.9); `ng update`
478
- bumps it automatically. TS 5.9 adds **TS2872 "This kind of expression is
479
- always truthy"**, which turns previously-silent dead `||` alternatives into
480
- build errors — expect one or two in any older codebase.
481
- - **Material 20 button labels no longer set `white-space: nowrap`.** Material
482
- 20 dropped the MDC stylesheet (`@material/button`), which used to carry it.
483
- Multi-word button labels now wrap, and any rule that lets the button size to
484
- its content (the lib's `.mdc-button { height: fit-content }`) makes the
485
- button grow taller. The lib restores the pre-20 look inside
486
- `ui-action-button` (`.mdc-button__label { white-space: nowrap }`); **hosts
487
- that render Material buttons of their own must add the same rule** if they
488
- relied on single-line labels (typical symptom: a navigation bar that
489
- suddenly wraps and overflows into scroll arrows).
490
- - **Material 20 renamed the MDC-era CSS custom properties**: e.g.
491
- `--mdc-text-button-container-height` → `--mat-button-text-container-height`.
492
- The `ng update @angular/material@20` schematic rewrites the ones it finds in
493
- `.css`/`.scss` files; token names embedded in TS strings or host templates
494
- must be renamed by hand.
495
- - **`DOCUMENT` moved from `@angular/common` to `@angular/core`.** Automatic
496
- migration; `@angular/common`'s re-export is deprecated.
497
- - **`moduleResolution: "node"` → `"bundler"`** in `tsconfig.json` (and any
498
- `tsconfig.lib.json` that sets it explicitly). Automatic migration; it changes
499
- how sub-path exports resolve, so a host with hand-written `paths` entries
500
- should re-verify its build.
501
- - **Workspace generation defaults**: the CLI adds a `schematics` block to
502
- `angular.json` pinning the pre-20 file-naming style (`type: "component"`,
503
- `typeSeparator: "."`). Cosmetic — it only affects newly generated files.
504
- - Optional migrations **not** run here (deferred to the modernization phase):
505
- `use-application-builder` (esbuild), `control-flow-migration`,
506
- `router-current-navigation`.
507
- - The `InjectFlags`, `TestBed.get` → `TestBed.inject` and
508
- `provideServerRendering` migrations were no-ops for this codebase.
509
- - The NG8111 warnings introduced at the 19 hop are no longer emitted by the
510
- Angular 20 compiler (the dead bindings themselves are unchanged).
511
-
512
- ### Angular 21 hop (Phase 2, 20 → 21)
513
-
514
- The library line now builds against Angular 21. Host-relevant changes:
515
-
516
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
517
- `@angular/*` `^21.0.0`, `@angular/youtube-player` `^21.0.0`,
518
- `ngx-quill` `^29.0.0` (the Angular 21 line; quill stays `^2.0.3`),
519
- `ngx-mask` `^21.0.0`, and — for `@smartbit4all/document-explorer` —
520
- `ngx-extended-pdf-viewer` `^25.0.0` (the 24.x line still caps its Angular
521
- peer below 21). No pdf-viewer API rename at this bump.
522
- - **`ngx-quill` 30.x is intentionally *not* used.** 30.x is the "zoneless"
523
- line and drops the `zone.js` peer dependency; the 7.0 lib is still
524
- zone-based. Hosts that have already gone zoneless may use 30.x, but the
525
- lib is only validated against 29.x.
526
- - **TypeScript stays 5.9** and **zone.js stays 0.15** (Angular 21 accepts
527
- `~0.15.0 || ~0.16.0`). **Node** must satisfy
528
- `^20.19.0 || ^22.12.0 || >=24.0.0`.
529
- - **`MatCommonModule` is removed from `@angular/material/core` (breaking).**
530
- It was already deprecated in Material 20 (`@breaking-change 21.0.0`). Any
531
- host NgModule that imports it will fail to compile with
532
- `Unknown reference` — and because a broken NgModule cascades, the real
533
- error usually shows up as a flood of `NG8001: '<some-component>' is not a
534
- known element` / `NG6002: … does not appear to be an NgModule class`
535
- further down the build log. **Fix: just delete the import and the array
536
- entry.** `MatCommonModule` had no template surface; it only
537
- (a) applied the `cdk-high-contrast-*` body classes — `A11yModule` still
538
- does this, and Material components that need it import `A11yModule`
539
- themselves — and (b) re-exported `BidiModule`. If your host actually uses
540
- the `Dir` directive or injects `Directionality`, import
541
- `BidiModule` from `@angular/cdk/bidi` explicitly.
542
- The lib dropped it from all 10 of its own modules.
543
- - **`provideZoneChangeDetection()` is now added at bootstrap** by the
544
- `ng update` migration, e.g.
545
- `bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()] })`.
546
- This keeps zone-based change detection explicit; behaviour is unchanged.
547
- - **The control-flow migration (`*ngIf` → `@if`) now runs as a *mandatory*
548
- `ng update` migration**, not an optional one. It rewrites every structural
549
- directive in the workspace. The lib **reverted** it to keep the version hop
550
- reviewable — `NgIf`/`NgForOf`/`NgSwitch` still ship in Angular 21, so
551
- `*ngIf`/`*ngFor` keep working. Hosts may keep or revert it as they prefer;
552
- if you want it as a separate change, run
553
- `npx ng generate @angular/core:control-flow` on its own commit.
554
- - **New `NG8107` extended diagnostic** (warning): an optional chain `?.`
555
- whose left side can no longer be `null`/`undefined` under Angular 21's
556
- sharpened template inference. Warning only — expect a batch of them in any
557
- older template set.
558
- - **`tsconfig.json` loses its explicit `lib` array** (the CLI derives it from
559
- `target`). Automatic migration.
560
- - The Material and CDK v21 migration schematics made no source changes, and
561
- the `Router.lastSuccessfulNavigation` migration was a no-op. The optional
562
- `router-current-navigation` migration was not run.
563
- - The `.mdc-button__label { white-space: nowrap }` workaround introduced at
564
- the Angular 20 hop is **still required on Material 21** — the class still
565
- exists in the button template and Material still ships no `white-space`
566
- rule for it.
567
-
568
- ### Angular 22 hop (Phase 2, 21 → 22) — final hop
569
-
570
- The library line now builds against **Angular 22**. This is the biggest
571
- host-facing hop of the series: it changes the **change-detection default**, the
572
- **HTTP backend default** and the **meaning of `?.` in templates**. All three are
573
- handled by `ng update` migrations, but you must let them run.
574
-
575
- - **Peer dependency bumps** (hosts must upgrade together with the lib):
576
- `@angular/*` `^22.0.0`, `@angular/youtube-player` `^22.0.0`,
577
- `ngx-quill` `^31.0.0` (the Angular 22 line; quill stays `^2.0.3`),
578
- `ngx-mask` `^22.0.0`, and — for `@smartbit4all/document-explorer` —
579
- `ngx-extended-pdf-viewer` `^28.0.0` (25/26/27 all cap their Angular peer
580
- below 22). **No pdf-viewer API rename at this bump.**
581
- `@ngx-translate/core` `^14` still works (its peer is `>=13`).
582
- - **TypeScript must be 6.0** (`@angular/compiler-cli` peers `>=6.0 <6.1`);
583
- **zone.js stays 0.15** (`~0.15.0 || ~0.16.0`) and **rxjs is unchanged**.
584
- **Node** must satisfy `^22.22.3 || ^24.15.0 || >=26.0.0` — note the
585
- **22.22.3** floor, which is stricter than Angular 21's `^22.12.0`.
586
-
587
- #### 1. `ChangeDetectionStrategy.OnPush` is the new default
588
-
589
- In Angular 22 the enum is redefined: `OnPush = 0` (default), `Eager = 1`, and
590
- `Default = 1` is kept as a **deprecated alias of `Eager`**. Any component
591
- without an explicit `changeDetection` therefore switches from CheckAlways to
592
- OnPush — a real behaviour change for components that mutate their own state
593
- without signals or `markForCheck()`.
594
-
595
- An `ng update` schematic **automatically adds
596
- `changeDetection: ChangeDetectionStrategy.Eager` to every component that had
597
- none**, which keeps behaviour bit-identical. The lib took exactly this route
598
- (114 components) and did **not** opt into OnPush at this hop. Recommended host
599
- approach: let the schematic run, verify your app, and move components to OnPush
600
- deliberately afterwards.
601
-
602
- Watch out for components the schematic can skip — it only touches components it
603
- can resolve. After updating, grep for `@Component` files with no
604
- `changeDetection` and decide about each one; a component that is dynamically
605
- instantiated but never statically declared can slip through.
606
-
607
- #### 2. `provideHttpClient` now defaults to the fetch backend
608
-
609
- Angular 22 switches `HttpClient` from `XMLHttpRequest` to `fetch`. An
610
- `ng update` migration inserts **`withXhr()`** into your `provideHttpClient(...)`
611
- calls to preserve the old backend:
612
-
613
- ```ts
614
- provideHttpClient(withXhr(), withInterceptorsFromDi())
615
- ```
616
-
617
- The lib does this in `SmartViewContextModule`. **Hosts that call
618
- `provideHttpClient` themselves must do the same** (or consciously move to
619
- fetch). This matters most for **upload progress**: `reportProgress` events
620
- behave differently on the fetch backend, so if your host reports upload
621
- progress, keep `withXhr()` until you have retested it.
622
-
623
- #### 3. `?.` in templates now yields `undefined`, not `null`
624
-
625
- Angular 22's safe-navigation operator returns `undefined` where it used to
626
- return `null`. Where that difference is observable, an `ng update` migration
627
- wraps the expression in the **`$safeNavigationMigration()` compiler builtin**:
628
-
629
- ```html
630
- [ngStyle]="calcStyle($safeNavigationMigration(diagramModel?.descriptor?.style))"
631
- ```
632
-
633
- This is an official builtin (handled like `$any()` — the compiler unwraps it and
634
- the type-checker sees straight through it), not a temporary marker, so wrapped
635
- templates are safe to keep. The same `ng update` run also sets the
636
- `nullishCoalescingNotNullable` and `optionalChainNotNullable` extended
637
- diagnostics to `suppress` in your tsconfigs, which silences the `NG8107`
638
- warnings the Angular 21 hop introduced.
639
-
640
- #### 4. `ComponentFactoryResolver` is removed (breaking)
641
-
642
- `ComponentFactoryResolver` is gone and `ComponentFactory` is no longer public
643
- (it only survives as `ɵRender3ComponentFactory`). Code like this stops
644
- compiling with `TS2305` / `NG2003`:
645
-
646
- ```ts
647
- constructor(private resolver: ComponentFactoryResolver) {}
648
- ngAfterViewInit() {
649
- const factory = this.resolver.resolveComponentFactory(MyComponent);
650
- this.vcRef.createComponent(factory);
651
- }
652
- ```
653
-
654
- Replace it with the type-based overload — behaviour is identical (v22 builds the
655
- same factory internally and resolves the injector from the parent):
656
-
657
- ```ts
658
- ngAfterViewInit() {
659
- this.vcRef.createComponent(MyComponent);
660
- }
661
- ```
662
-
663
- **Public-API change in the lib:** `ComponentFactoryService` no longer exposes
664
- its `factory` field. `createComponent()` / `destroyComponent()` keep their
665
- signatures, so hosts that only call those need no change.
666
-
667
- #### 5. TypeScript 6.0 deprecates `baseUrl` and `downlevelIteration`
668
-
669
- TS 6.0 raises **`TS5101`** for both options ("deprecated and will stop
670
- functioning in TypeScript 7.0"). Two ways out:
671
-
672
- - Remove them. `downlevelIteration` is a no-op at `target: ES2022`. `baseUrl`
673
- can only go once every non-relative first-party import (`from 'projects/…'`,
674
- `from 'src/…'`) is relative or covered by a `paths` entry.
675
- - Or silence them for now with `"ignoreDeprecations": "6.0"` in
676
- `compilerOptions`. This is what the lib workspace does, because `baseUrl` is
677
- still load-bearing there. It only buys time until TS 7.
678
-
679
- #### 6. Other notes
680
-
681
- - **Material 22 removed no further modules or tokens** — the CDK and Material
682
- v22 migration schematics made no source changes. CDK 22 moves to a
683
- package `exports` map (the per-entry-point sub-directories are gone from
684
- `node_modules/@angular/cdk`), but every `@angular/cdk/*` import still
685
- resolves; this only matters if you referenced those paths on disk.
686
- - The `.mdc-button__label { white-space: nowrap }` workaround introduced at the
687
- Angular 20 hop is **still required on Material 22** — the class still appears
688
- in Material's button template and Material still ships no `white-space` rule
689
- for it.
690
- - **The control-flow migration did not re-run at this hop** (it was mandatory at
691
- 21). The lib still ships `*ngIf`/`*ngFor` templates; `NgIf`/`NgForOf`/
692
- `NgSwitch` continue to work in Angular 22.
693
- - **The webpack-based builders are deprecated.** `@angular-devkit/build-angular`
694
- now prints a notice recommending `@angular/build`. Nothing breaks yet, but
695
- plan the switch.
696
- - `strictTemplates` is on by default in v22; the lib workspace was already
697
- strict, so nothing changed. If your host was not strict, the `ng update`
698
- migration adds `"strictTemplates": false` to keep it that way.
699
-
700
- ### The third-party packages the library does *not* own (Phase 2)
701
-
702
- Every hop section above lists the library's own peer dependencies. Your host almost
703
- certainly carries packages the library never sees, and those have their own Angular
704
- peer ceilings — each one can stop a hop dead. Measured on p014 (2026-07-29), which
705
- went 17 → 22 with the library still installed:
706
-
707
- - **`@swimlane/ngx-charts` needs a ladder of its own**: `20 → 22 → 22 → 23 → 24 → 25`
708
- across the five hops. At the Angular 21 hop it peer-caps the CDK, and because
709
- `ng update`'s own `npm` step is **not** `--force`d (even when you passed `--force`
710
- to `ng update`), the install fails there while `package.json` has already been
711
- written correctly. The fix both times was to redo the install by hand, not to
712
- re-run the update. `p043` and `app-tournament` both carry ngx-charts `^20.4.1`
713
- and will hit this; `app-fitnessmirror` and `app-finance-ai` do not.
714
- - **`ngx-extended-pdf-viewer` moved to `.mjs`** at the 21 hop and stopped shipping
715
- `pdf-3.10.560-es5.min.js`. If your `angular.json` names that file under `scripts`
716
- or `assets`, the build fails on a missing file rather than on anything Angular.
717
- - **`karma` and `@angular/build`**: `@angular/build` 22 declares
718
- `peerOptional karma@^6.4.0`. A host pinned to `~6.3.0` gets a wall of
719
- `npm warn ERESOLVE overriding peer dependency` — **warnings, exit code 0**, so it
720
- is not blocking. It becomes a *hard* install failure the moment `@angular/build`
721
- is a direct devDependency rather than a nested one, which is what happened in the
722
- library workspace. `karma@~6.4.4` clears it.
723
-
724
- The general shape: **`ng update` needs `--force` at every hop** (the library and
725
- PrimeNG both peer-cap at Angular 17 until the very end), and `--force` covers the
726
- Angular schematics but not the `npm install` they trigger. When that install fails,
727
- check `package.json` before assuming the hop did nothing — it is usually already
728
- correct.
729
-
730
- ### Housekeeping (Phase 3.0)
731
-
732
- #### The dialog's built-in Ok/Cancel buttons are gone
733
-
734
- `smartdialog.component.html` no longer renders an `Ok` and a `Cancel` button.
735
- They were dead: their `(click)` handlers were the no-op expressions
736
- `(click)="(data.okCallback)"` / `(click)="(data.cancelCallback)"` — a bare
737
- property read, not a call — so pressing them did nothing, and the whole block
738
- was behind `*ngIf="!data.customComponent"`, which is never true for a host
739
- dialog (`SmartDialog.ngAfterViewInit` unconditionally instantiates
740
- `data.customComponent`).
741
-
742
- **The `okCallback` / `cancelCallback` model fields stay.** Hosts set and invoke
743
- them from their own `customComponent` dialogs, and that keeps working
744
- unchanged. Only the library-rendered buttons are gone. The `actionCallback` /
745
- `actionLabel` button — the one with a real handler (`onActionClick()`) — is
746
- untouched.
747
-
748
- Action needed: none, unless you relied on the library rendering those two
749
- buttons — in which case they never worked, and you should render them in your
750
- own dialog component.
751
-
752
- ### Full standalone + `provideSmartNgClient()` (Phase 3.2) — **the big one**
753
-
754
- This is the change that rewrites your `app.module.ts`. Everything else in this
755
- guide is small next to it.
756
-
757
- #### Every NgModule is gone
758
-
759
- The library no longer ships a single `@NgModule`. All 58 are deleted —
760
- `SmartNgClientModule`, `SmartViewContextModule`, `SmartSessionModule`,
761
- `SmartGridModule`, `SmarttableModule`, `SmarttreeModule`, `SmartdialogModule`,
762
- `SmartIconModule`, `SmartNavbarModule`, `SmartFilterModule`,
763
- `SmartFilterEditorModule`, `SmartComponentLayoutModule`,
764
- `SmartExpandableSectionModule`, `SmartMapModule`, `SmartDiagramModule`,
765
- `SmartNavigationModule`, `SmartValidationModule`, `SmartGenericPagesModule`,
766
- `ComponentFactoryServiceModule`, `SharedModule`, `SmartTabGroupModule`,
767
- `SmartDocuStoreExplorerModule` and the generated OpenAPI `ApiModule`s.
768
-
769
- Every component, directive and pipe is now **standalone**. Import the ones your
770
- templates use directly, in the component that uses them:
771
-
772
- ```ts
773
- @Component({
774
- selector: 'app-my-page',
775
- templateUrl: './my-page.component.html',
776
- imports: [SmartGridComponent, UiActionToolbarComponent, SmartEmbeddedSlotDirective],
777
- })
778
- export class MyPageComponent extends SmartComponent<MyModel> { … }
779
- ```
780
-
781
- **The compiler tells you what is missing** — one `NG8001: '<smart-grid>' is not
782
- a known element` per unresolved element, per template. Work through them; there
783
- is no guessing involved. In practice ~16 module imports become ~40 component
784
- imports spread across the components that actually use them.
785
-
786
- **The compiler does NOT tell you about the DI changes below.** Nothing fails at
787
- build time if you get those wrong; you find out on the first page load. Smoke
788
- your app.
789
-
790
- #### `provideSmartNgClient(config, ...features)` replaces the module imports
791
-
792
- ```ts
793
- bootstrapApplication(AppComponent, {
794
- providers: [
795
- provideAnimations(),
796
- provideRouter(ROUTES),
797
- provideSmartNgClient(
798
- {
799
- gridMenuIcon: 'more_horiz',
800
- treeMenuIcon: 'more_horiz',
801
- aclEditingViewName: Pages.ACL_MATRIX_PAGE,
802
- invalidSmartlinkPageName: Pages.INVALID_SMARTLINK_PAGE_NAME,
803
- namedValidators: [MY_VALIDATOR_FACTORY],
804
- },
805
- withSmartMap({ engine: MapEngine.LEAFLET }),
806
- withSmartDiagram({ customOptions: [MY_CHART] })
807
- ),
808
- { provide: HTTP_INTERCEPTORS, useClass: MyLoadingInterceptor, multi: true },
809
- ],
810
- });
811
- ```
812
-
813
- The core is always wired and cannot be forgotten: session, view context, the
814
- three BFF interceptors, layout, form, grid, table, tree, dialog, icon, navbar,
815
- filter, filter editor, validation, expandable section, navigation, shared and
816
- the generic pages. That is the point — the session and header interceptors used
817
- to be easy to leave out, and leaving them out fails *silently* (no
818
- `Authorization` header).
819
-
820
- Only two features are opt-in, because they pull heavy third-party code that not
821
- every host renders: `withSmartMap()` (leaflet / Google Maps) and
822
- `withSmartDiagram()` (chart.js). There is **no `withSmartTabGroup()`** —
823
- `smart-tab-group` had no component left, only an empty module, and is deleted.
824
-
825
- `provideSmartNgClient()` calls `provideHttpClient()` itself, with
826
- `withInterceptorsFromDi()` and its own three functional interceptors, and leaves the backend
827
- on the framework default (fetch). Your own class-based `HTTP_INTERCEPTORS` keep working and
828
- still run after the library's three. Pass extra `HttpClient` features in
829
- `config.httpFeatures` rather than making a second `provideHttpClient()` call — see
830
- [HTTP](#http-the-library-no-longer-forces-the-xhr-backend-phase-38), which is also where the
831
- 6.x "do not call it twice" trap went.
832
-
833
- #### String-keyed providers become typed config fields
834
-
835
- Delete these from your providers array and pass them to `provideSmartNgClient`:
836
-
837
- | 6.x provider | 7.0 config field |
838
- |---|---|
839
- | `{ provide: 'gridMenuIcon', useValue: … }` | `gridMenuIcon` |
840
- | `{ provide: 'treeMenuIcon', useValue: … }` | `treeMenuIcon` |
841
- | `{ provide: 'searchPageName' / 'searchComponentName', … }` | `searchPageName` / `searchComponentName` |
842
- | `{ provide: 'genericPageName' / 'genericComponentName', … }` | `genericPageName` / `genericComponentName` |
843
- | `{ provide: 'subjectSelectorPageName' / 'subjectSelectorComponentName', … }` | `subjectSelectorPageName` / `subjectSelectorComponentName` |
844
- | `{ provide: 'validationResultPageName' / 'validationResultComponentName', … }` | `validationResultPageName` / `validationResultComponentName` |
845
- | `{ provide: 'noPermissionPageName' / 'noPermissionComponentName', … }` | `noPermissionPageName` / `noPermissionComponentName` |
846
- | `{ provide: 'invalidSmartlinkPageName' / 'invalidSmartlinkComponentName', … }` | `invalidSmartlinkPageName` / `invalidSmartlinkComponentName` |
847
- | `{ provide: 'aclEditingViewName', useValue: … }` | `aclEditingViewName` |
848
- | `{ provide: DIALOG_DISABLE_CLOSE, useValue: … }` | `dialogDisableClose` |
849
- | `{ 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) |
850
- | `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) |
851
- | `{ provide: NAMED_VALIDATOR, useValue: F, multi: true }` | `namedValidators: [F]` |
852
- | `{ provide: SMART_DEFAULT_VIEW_COMPONENTS, useValue: E, multi: true }` | `defaultViewComponents: [E]` |
853
- | `SmartValidationModule.forRoot([...])` | `namedValidators` — **the element type changes**, see below |
854
- | `SmartDiagramModule.forRoot([...])` | `withSmartDiagram({ customOptions })` |
855
- | `{ provide: MAP_ENGINE, useValue: … }` | `withSmartMap({ engine })` |
856
-
857
- `'pageName'`, `'componentName'` and `'treeId'` stay as string tokens on purpose:
858
- they identify one component instance rather than configuring the library, and
859
- hosts also provide them at component level.
860
-
861
- You can also delete these from your **root** providers array — the core provides them:
862
- `SmartSessionService`, `SmartViewContextService`, `SmartNavigationService`,
863
- `SmartIconService`, `SmartFilterEditorService`, `SmartFormService`,
864
- `SmartCookieService`, `NamedValidatorService`, `ComponentFactoryService`, and the date
865
- adapter wiring (`MAT_DATE_FORMATS`, `DateAdapter`).
866
-
867
- ⚠️ **`namedValidators` takes factories, not providers.** `SmartValidationModule.forRoot()`
868
- took `Provider[]`, so hosts wrote the wrapper by hand:
869
-
870
- ```ts
871
- // 6.x — validator.factories.ts
872
- export const VALIDATOR_PROVIDER_EMPTY_INPUT: Provider = {
873
- provide: NAMED_VALIDATOR, useValue: VALIDATOR_FACTORY_EMPTY_INPUT, multi: true,
874
- };
875
- // app.module.ts
876
- imports: [SmartValidationModule.forRoot([VALIDATOR_PROVIDER_EMPTY_INPUT])]
877
- ```
878
-
879
- `provideSmartNgClient()` adds that wrapper itself, so the config takes the bare
880
- `ValidatorFactory[]`. Export the factories and pass those:
881
-
882
- ```ts
883
- // 7.0
884
- export const VALIDATOR_FACTORY_EMPTY_INPUT: ValidatorFactory = { … };
885
- provideSmartNgClient({ namedValidators: [VALIDATOR_FACTORY_EMPTY_INPUT] })
886
- ```
887
-
888
- The codemod moves the argument into the config and **reports it**, because the entries are
889
- normally named constants declared in another file and only you know what each one wraps. If
890
- you miss the note, `TS2322: Type 'Provider' is not assignable to type 'ValidatorFactory'`
891
- lands on every element.
892
-
893
- ⚠️ **The same line on a `@Component` is not the same thing.**
894
- `providers: [SmartFilterEditorService]` on a page component asks for one instance *per page*,
895
- which is usually deliberate — p014 does it in five components. Leave those alone; the codemod
896
- does.
897
-
898
- #### The module → standalone mapping
899
-
900
- What each `Smart*Module` used to export, reduced to what 7.0 still exports publicly. An
901
- NgModule's `imports:` array accepts standalone components, so this substitution is all a host
902
- that is still NgModule-based needs to keep compiling — the codemod performs it.
903
-
904
- | 6.x module | 7.0 imports |
905
- |---|---|
906
- | `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 |
907
- | `SmartComponentLayoutModule` | `SmartComponentLayoutComponent` |
908
- | `SmartViewContextModule` | `UiActionToolbarComponent`, `UiActionButtonComponent`, `UiActionDialogButtonComponent`, `SmartformComponent`, `SmartfileuploaderComponent`, `SmartFileEditorComponent`, `SmartMultiFileEditorComponent`, `SmartVoiceRecorderComponent`, `SmartEmbeddedSlotDirective`, `HighlightPipe` |
909
- | `SmartGridModule` | `SmartGridComponent` |
910
- | `SmarttreeModule` | `SmartTreeComponent` |
911
- | `SmartdialogModule` | `SmartDialog` |
912
- | `SmartIconModule` | `SmartIconComponent`, `UiBadgeComponent`, `UiBadgeDirective` |
913
- | `SmartNavbarModule` | `SmartNavbarComponent` |
914
- | `SmartFilterModule` | `SmartFilterComponent` |
915
- | `SmartFilterEditorModule` | `SmartFilterEditorContentComponent` |
916
- | `SmartExpandableSectionModule` | `ExpandableSectionComponent` |
917
- | `SmartSessionModule` | `SmartSessionTimerComponent` |
918
- | `SmartGenericPagesModule` | `SearchPageComponent`, `GenericPageComponent`, `SubjectSelectorComponent`, `InvalidSmartlinkComponent` |
919
- | `SmartMapModule` | `SmartMapComponent` + `withSmartMap({ engine })` |
920
- | `SmartDiagramModule` | `SmartDiagramComponent` + `withSmartDiagram({ customOptions })` |
921
- | `SharedModule` | `SmartTooltipDirective`, `SmartDatePipe`, `SmartDateTimePipe`, `SmartTimePipe` |
922
- | `SmartValidationModule` | nothing — `forRoot()`'s validators become `namedValidators` |
923
- | `SmarttableModule` | nothing — `SmarttableComponent` is no longer public; render a `<smart-grid>` |
924
- | `SmartNavigationModule`, `ComponentFactoryServiceModule` | nothing — they only carried services, which the core provides |
925
- | `SmartTabGroupModule` | nothing — deleted; it had no component left |
926
- | `SmartDocuStoreExplorerModule` (`@smartbit4all/document-explorer`) | `SmartDocuStoreExplorerComponent`, `FolderContentComponent`, `PdfViewerDialogPageComponent` |
927
-
928
- Two things this table will not tell you, and the compiler will not either: an import you no
929
- longer need is invisible (unused entries in an NgModule `imports:` array are not reported),
930
- and a name you already bind from elsewhere — a `SmartDatePipe` of your own — must not be
931
- imported twice. The codemod reports both cases rather than guessing.
932
-
933
- #### Your `SmartComponent` subclasses silently become OnPush
934
-
935
- This is an **Angular 22 fact rather than something 7.0.0 introduces**, but it
936
- lands on you at the same moment, so it is worth stating plainly: `SmartComponent`
937
- is an abstract `@Component`, and each of your subclasses carries its own
938
- `@Component` decorator. On Angular 22 a component without an explicit
939
- `changeDetection` gets the new `OnPush` default. p014 has 75 such subclasses,
940
- p043 has 70.
941
-
942
- Let the `ng update` schematic add `changeDetection: ChangeDetectionStrategy.Eager`
943
- to all of them (that is what the library did), verify your app, and move to
944
- OnPush deliberately afterwards. A subclass that mutates its own state outside a
945
- signal and without `markForCheck()` will stop repainting under OnPush.
946
-
947
- #### Removed from the public API
948
-
949
- A major is the only chance to narrow the surface, so these are no longer
950
- exported. All of them are library internals; none is used by p014, p043,
951
- app-formmate or `@smartbit4all/document-explorer`:
952
-
953
- - `SmarttableComponent`, `SmarttableService` — use `<smart-grid>`, which renders
954
- the table internally. **All `SmartTable*` model types stay** (`SmartTableType`,
955
- `SmartTableInterfaceTypeEnum`, `SmartTableHeader`, …). p043 imports
956
- `SmarttableComponent` in `ad-groups.component.ts` but never uses it — delete
957
- that one import line.
958
- - `GoogleMap`, `LeafletMap`, `AbstractMap` — select an engine with
959
- `withSmartMap({ engine })`.
960
- - `SmartFilterParamComponent`, `SmartFilterParamsComponent`,
961
- `SmartFilterExpressionItemComponent`, `SmartFilterExpressionItemsComponent` —
962
- internals of `SmartFilterEditorContentComponent`, which stays public.
963
- - `HoverMenuComponent`, `ClickMenuComponent` — rendered by the ui-action toolbar.
964
- - `UiActionConfirmDialogComponent`, `UiActionInputDialogComponent` — opened by
965
- their services, which stay public.
966
- - `UploadWidgetComponent`, `PhotoCaptureWidgetComponent`,
967
- `VoiceRecordWidgetComponent` — contents of the ui-action upload dialog.
968
- - `SmartNgClientService` — deleted; it was an empty stub with no members.
969
- - The generated `ApiModule`s — they had no usages and their `forRoot()` told you
970
- to import `HttpClientModule`, which no longer applies.
971
-
972
- Two internal string tokens are gone as well, replaced by real `InjectionToken`s
973
- (`'confirmDialogService'`, `'textFieldDialogService'`, `'fileUploadDialogService'`).
974
- No host provided them, so no action is needed.
975
-
976
- #### Behaviour notes
977
-
978
- - `<mat-slide-toggle>` in the form widget no longer carries a `value` binding,
979
- `<mat-nested-tree-node>` no longer carries an interpolated `matTreeNodeToggle`,
980
- and the ui-action menus no longer carry `[subActions]`. All four were dead
981
- bindings that the NgModules' `CUSTOM_ELEMENTS_SCHEMA` had been hiding — they
982
- wrote a DOM property nobody read. Rendering is unchanged.
983
- - The dead `ExpandableGridComponent` is deleted (it was declared in
984
- `SmartGridModule` but its selector was used nowhere).
985
-
986
- ### Widget ↔ SmartComponent: the model routing is inverted (Phase 3.4)
987
-
988
- A widget no longer waits to be collected by the screen component it lives in: it finds its
989
- `SmartComponentApiClient` through DI and registers itself. `SmartComponent` carries the
990
- channel as an inherited host directive, so **no host component has to be changed for the
991
- routing itself** — everything below is the *legacy surface* that went with the old
992
- collection path. The protocol is written up in `WIDGETS.md` next to this file; the decision
993
- record behind it is ADR-0005 in the `platform-angular2` working copy.
994
-
995
- #### What a host must delete (all of it mechanical, all reported by the compiler)
996
-
997
- | Delete | Why |
998
- |---|---|
999
- | `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. |
1000
- | `[parentSmartComponent]="this"` on `<smart-component-layout>` | The input is gone; the layout injects the client. `strictTemplates` reports leftovers as **NG8002**. |
1001
- | `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. |
1002
- | `this.widgets.delete(…)` / `this.widgets.set(…)` / `this.formWidgets…` | The maps are gone (they were only written by the legacy path and read by nothing). |
1003
- | 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. |
1004
- | `override getSmartXxxQL()` (e.g. the two `throw new Error` overrides in a `FilterPageService`) | The eight abstract getters are gone from `SmartComponentApiClient`. |
1005
- | `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. |
1006
- | `handleDataChangeSubscriptions()` | Gone. A form publishes its own value-change keys when it constructs. |
1007
-
1008
- Removed from the public API in the same step, in case a host imported them directly:
1009
- `SmartComponentLayoutUtility` and its `EmbeddedSlotInfo` interface (the whole file — its
1010
- collectors walked the layout tree for widgets, which nothing does now), and the `[parent]`
1011
- inputs of `<smart-map>` / `<smart-diagram>` (they were bound to `parentSmartComponent` and read
1012
- by nothing; both widgets resolve the client themselves). `initActions()` also runs less often:
1013
- only when the model or its action list changes, no longer after every layout render, grid view
1014
- init and filter model change — a host override that relied on one of those extra calls has to
1015
- key off `dataChanged` instead.
1016
-
1017
- The [codemod](#migrating-a-host-the-codemod) deletes the first two rows and the last one, and
1018
- lists the rest — `addForm()`, the 4th `addGrid()` argument, the getter overrides — with their
1019
- line numbers, because each of those is a small decision rather than a substitution.
1020
-
1021
- #### What stays
1022
-
1023
- - `SmartService` — a view-less client. Its `@ViewChildren` never populated anyway; they are
1024
- simply gone now.
1025
- - `initActions()` — still the hook a host overrides to build its own action list. The base
1026
- implementation only asks for a render; toolbars pull their actions themselves.
1027
- - `setUpDefaultTree()` / `createTreeService()`, `addGrid()`, `submitForms()`,
1028
- `getInvalidFields()`, `uiActionModels` (now a getter over the model's actions).
1029
-
1030
- #### `uiActionModels` is read-only now — assigning to it no longer compiles
1031
-
1032
- It became a getter over the `actionModels` computed, so a host that *replaced* the list in
1033
- place gets `TS2540: Cannot assign to 'uiActionModels' because it is a read-only property`.
1034
- Measured in **p014 and p043**, `document-storage-editor.component.ts`:
1035
-
1036
- ```ts
1037
- // before — no longer compiles
1038
- this.uiActionModels = this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL');
1039
-
1040
- // after — filter into a field of your own and bind it
1041
- protected readonly visibleActions = signal<UiActionModel[]>([]);
1042
- // …
1043
- this.visibleActions.set(this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL'));
1044
- ```
1045
-
1046
- ```html
1047
- <smart-ui-action-toolbar [uiActionModels]="visibleActions()"></smart-ui-action-toolbar>
1048
- ```
1049
-
1050
- A plain field works just as well; what matters is that the **array reference changes**, which
1051
- is what tells the toolbar to re-render. See also the entry-freeze note in the Phase 3.7
1052
- section.
1053
-
1054
- #### Behavioural deltas, accepted
1055
-
1056
- - **Wider reach.** A widget in a host's *own* sub-component now participates, where a view
1057
- query could not see it (measured on p014/p043: 5 toolbars and 3–4 forms per host). A form
1058
- takes part only if it displays the client's component model, which is what the forms the
1059
- queries used to find have in common; a form that renders something else of its own (a
1060
- search box, a field editor) does not.
1061
- - Two widgets sharing one identifier both reload; the old map kept only the last.
1062
- - `getInvalidFields()` collects in subscription order rather than view order — visible only
1063
- in the order of the names in the validation dialog.
1064
- - A toolbar entry is tracked by a synthesized key (action code + ordinal among entries with
1065
- the same code), so a button's DOM node — with its focus and ripple — follows its action
1066
- across reordering. Rendering is otherwise identical.
1067
-
1068
- #### `[smartComponentDetached]`
1069
-
1070
- The one brake on the automatic registration. Put it on an element whose subtree deliberately
1071
- renders something other than the client's model; widgets below it resolve an empty slot and
1072
- neither reload nor take part in submit/validation.
1073
-
1074
- ```html
1075
- <div smartComponentDetached>
1076
- <smart-grid [smartGrid]="ownGrid" [uuid]="ownUuid"></smart-grid>
1077
- </div>
1078
- ```
1079
-
1080
- #### `SmartSubject` is deleted
1081
-
1082
- It was a no-op wrapper over `Subject`: `subscription.add(this.unsubscribe$.subscribe())`
1083
- registers the teardown in the wrong direction, so after `destroy$.next()`, `.complete()` or
1084
- `.unsubscribe()` subscribers still received values. Replace `new SmartSubject(destroy$)`
1085
- with `new Subject()`; where a subscription really must die with a component, use
1086
- `takeUntil(this._destroy$)` (which is what actually stopped delivery all along) or
1087
- `takeUntilDestroyed()`. No host used it.
1088
-
1089
- ### Toolbars resolve their own actions and own the execution context (Phase 3.5)
1090
-
1091
- One rule decides which actions a toolbar shows, and the toolbar — not each list entry —
1092
- says who performs them. `WIDGETS.md`, *Toolbars*, is the short version; the decision record
1093
- behind it is ADR-0006 in the `platform-angular2` working copy.
1094
-
1095
- > A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The
1096
- > list comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the
1097
- > screen component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is
1098
- > not an address, so a toolbar with neither an id nor a binding shows nothing.
1099
-
1100
- `[uiActionModels]="uiActionModels"` therefore keeps meaning "put my page's unaddressed
1101
- actions here" and needs no change.
1102
-
1103
- #### `UseUiAction` is deleted; `UseUiAction2` is now `UiActionExecutor`
1104
-
1105
- There is one executor interface. If a host class implements `UseUiAction` — a `submit` /
1106
- `reSubscribeToChange` subject handshake — replace the two subjects with the two methods:
1107
-
1108
- ```ts
1109
- // before
1110
- export class DelegateInboxDialogService implements UseUiAction {
1111
- submit: Subject<void> = new Subject();
1112
- reSubscribeToChange: Subject<void> = new Subject();
1113
-
1114
- }
1115
-
1116
- // after
1117
- export class DelegateInboxDialogService implements UiActionExecutor {
1118
- submitForm(validate: boolean): void {
1119
- this.submit.next(); // keep the subject if a component subscribes to it
1120
- }
1121
- getInvalidFields(): SmartFormInvalidFields {
1122
- return { invalidFieldKeys: [], invalidFieldNames: [] };
1123
- }
1124
-
1125
- }
1126
- ```
1127
-
1128
- `Subject.next()` is synchronous, so a component that subscribed to `submit` has already run
1129
- by the time `submitForm()` returns — which is exactly what the old handshake tried to
1130
- express. Keep `reSubscribeToChange` only if something subscribes to it; the library no longer
1131
- fires it.
1132
-
1133
- This also **fixes a silent hang**: the old branch `await`ed `submit.toPromise()`, which only
1134
- settles when somebody `complete()`s the subject. Three of the five implementors never did, so
1135
- a `submit`/`model` action on those services waited forever.
1136
-
1137
- #### `UiActionService.execute()` lost its first argument
1138
-
1139
- It was always `uiActionModel.uiAction`:
1140
-
1141
- ```ts
1142
- - this.uiActionService.execute(uiActionModel.uiAction, uiActionModel);
1143
- + this.uiActionService.execute(uiActionModel);
1144
- ```
1145
-
1146
- #### The execution context moved from the entry to the toolbar
1147
-
1148
- | what | bind on the toolbar | entry field (still honoured) |
1149
- |---|---|---|
1150
- | who performs the action | `[executor]` | `serviceToUse` — **deprecated** |
1151
- | which widget it runs against | `[widgetId]` | `widgetId` — **deprecated**, wins if set |
1152
- | which row inside it | `[nodeId]` | `nodeId` — **deprecated**, wins if set |
1153
- | extra params for every action | `[actionParams]` | (clone the `UiAction` — no longer necessary) |
1154
-
1155
- `[executor]` defaults to the screen component the toolbar sits under, which is what almost
1156
- every entry named. Bind it only when the actions belong to another API — a tree service, a
1157
- filter editor, a dialog service of your own:
1158
-
1159
- ```html
1160
- <smart-ui-action-toolbar [uiActionModels]="actions" [executor]="myService"></smart-ui-action-toolbar>
1161
- ```
1162
-
1163
- `[actionParams]` replaces cloning a `UiAction` per row just to inject a model:
1164
-
1165
- ```html
1166
- <smart-ui-action-toolbar
1167
- [uiActionModels]="rowActions"
1168
- [widgetId]="gridId"
1169
- [nodeId]="row.id"
1170
- [actionParams]="{ model: row }"
1171
- ></smart-ui-action-toolbar>
1172
- ```
1173
-
1174
- #### Removed from the public API
1175
-
1176
- | Removed | Replacement |
1177
- |---|---|
1178
- | `UseUiAction` | `UiActionExecutor` (see above) |
1179
- | the third `SmartTable` constructor parameter and `getServiceToUse()` | nothing — the executor no longer travels through the table (0 host call sites) |
1180
- | `SmartformComponent.getToolbars()` | nothing collects toolbars any more; each one resolves its own actions |
1181
- | `ISmartFilterEditorService.get/setComplexToolbars()` and the `submit` / `reSubscribeToChange` members | as above |
1182
- | `FileEditorToolbarComponent` | a plain `<smart-ui-action-toolbar>` with `[actionParams]` |
1183
- | `SmartGridComponent.setupToolbar()` / `.toolbar` / `.headerToolbar` | the id is a binding; there is nothing to reach into |
1184
-
1185
- #### Behavioural deltas, accepted
1186
-
1187
- - A widget renders a toolbar exactly when its model carries a `toolbarId`, with no type list.
1188
- A `toolbarId` on a type whose template has no place for a toolbar (`RECORDING_UPLOADER`)
1189
- now logs a warning instead of being silently dropped.
1190
- - Only the actions a toolbar actually shows are scheduled. Since 3.4 a toolbar armed every
1191
- scheduled action of the view, so one action could fire from several toolbars at once.
1192
- - A grid row's actions are no longer cloned per row; the row model travels as
1193
- `[actionParams]`. The multi-file editor's per-file toolbars appear when their own list
1194
- resolves rather than when a hidden sibling's did, and the tree's `doAction` clones instead
1195
- of mutating the caller's `UiAction`.
1196
-
1197
- ### The form widget's rendering decisions (Phase 3.6)
1198
-
1199
- Nothing here needs a code change in a host — there is no API to adapt to. What
1200
- changes is what the widget puts on the page, so read this before you compare
1201
- screenshots.
1202
-
1203
- #### Every widget carried a spare empty wrapper; it is gone
1204
-
1205
- The `COMPONENT` branch's wrapper had no type guard, so
1206
- `<div class="widgetContainer"><div class="widgetContent"><ng-template
1207
- #customComponent>` rendered after **every** widget of every type. Measured on the
1208
- playground: 14 widgets, 28 `.widgetContainer` — one spare, empty, per widget.
1209
-
1210
- The library's own CSS puts no box properties on `.widgetContainer`, so most pages
1211
- will look identical. **Yours may not**, if you style that class with anything that
1212
- occupies space. Both in-house hosts do, on one page:
1213
-
1214
- ```css
1215
- /* p014 + p043, dossier-publication.component.css */
1216
- :host ::ng-deep .widgetContainer { margin-bottom: 0.5rem !important; }
1217
- ```
1218
-
1219
- There, every widget loses half a rem of trailing space. Grep your stylesheets for
1220
- `.widgetContainer` and check the pages the matching selectors reach. Rules that
1221
- need content still match what they always matched — an empty wrapper never
1222
- satisfied `.label.widgetContainer` or `.widgetContainer:has(…)`.
1223
-
1224
- #### `MATRIX` and `YOUTUBE_PLAYER` now obey `isVisible` and get their `cssClass`
1225
-
1226
- Those two branches sat outside the `@if (isVisible)` guard and outside the
1227
- `<div [ngClass]="cssClass" class="container">` every other type lives in, so
1228
- `isVisible: false` did not hide them and `cssClass` never reached them. Both are
1229
- inside now. If a backend view relies on a matrix rendering while marked invisible,
1230
- it will stop rendering.
1231
-
1232
- #### A container keeps its invisible children
1233
-
1234
- A `CONTAINER` widget's `ngOnInit` used to overwrite `widgetInstance.valueList`
1235
- with the visible subset, destroying the invisible children in the model the
1236
- backend sent. The visible subset is derived per check now, so the model stays
1237
- whole and a later `applyConstraints()` that flips a child's `isVisible` brings it
1238
- back — which it could not before. The DOM is unchanged: the same children render.
1239
-
1240
- Side effect worth knowing: the model walks that recurse into a container's
1241
- `valueList` (`applyComponentConstraints`, `copyValueFromFormToWidgetBeans`,
1242
- `translateWidgets`) now see the invisible children too. Their form controls always
1243
- existed — `createFormControls` runs over the whole model before any widget renders
1244
- — so this only makes the widget beans agree with the controls.
1245
-
1246
- #### A YouTube shorts link resolves
1247
-
1248
- `parseYoutubeUrl` threw away its own `replace('/shorts/', '/watch?v=')`, so a
1249
- shorts link produced no player. It plays now.
1250
-
1251
- #### Removed from the public API
1252
-
1253
- | Removed | Replacement |
1254
- |---|---|
1255
- | `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.1 splits it into per-type components |
1256
- | `SmartWidgetSettings` (its only member was `static useUtc`) | nothing — **delete the assignment** |
1257
-
1258
- 3.6 moved the `useUtc` static into `SmartNgClientConfig.useUtcDates`; **3.8 then removed the
1259
- field altogether**, together with the moment adapter it fed. If you set
1260
- `SmartWidgetSettings.useUtc = true` before bootstrap, delete that line and read
1261
- [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) — the timezone contract is fixed now,
1262
- and the flag defaulted to `false` everywhere anyway.
1263
-
1264
- #### What deliberately did not change
1265
-
1266
- A required field is marked in both label positions, by two different mechanisms:
1267
- a shown label (`showLabel: true`) is the widget's own `<h4>` and carries a literal
1268
- ` *`; a `mat-label` sits inside a `mat-form-field`, which renders
1269
- `.mat-mdc-form-field-required-marker` from the control's own validators. That is
1270
- why `getWidgetLabel` ties its asterisk to `showLabel` — dropping the condition
1271
- would mark the floating label twice.
1272
-
1273
- ### Change detection: frozen action entries, no zone.js (Phase 3.7)
1274
-
1275
- 7.0 runs without zone.js. Every component of the library is `OnPush` — nothing is checked
1276
- just because something, somewhere, ticked — and the library says when its own state changed.
1277
- Two consequences reach a host.
1278
-
1279
- #### A `UiActionModel` is frozen: build an entry, never edit one
1280
-
1281
- Every field is `readonly`, and `[uiActionModels]` takes `readonly UiActionModel[]`.
1282
-
1283
- ```ts
1284
- // before — compiles on 6.x, and on 7.0 changes nothing on screen
1285
- entry.cssClass = 'active-nav-action';
1286
-
1287
- // after
1288
- this.actions = this.actions.map((a) =>
1289
- a.uiAction.code === code ? { ...a, cssClass: 'active-nav-action' } : a
1290
- );
1291
- ```
1292
-
1293
- Measured across the 12 hosts in scope (2026-07-28): **72 sites in 7 hosts**, all of them
1294
- `cssClass` — p009-angular alone has 38, p014 and p043 12 each. The codemod prints every one
1295
- of them with its line number; it cannot rewrite them, because only you know which array the
1296
- entry belongs to.
1297
-
1298
- The compile error and the fix are the same thing. Writing into an entry that a toolbar is
1299
- already rendering never reached the screen under zone.js either — it worked only because
1300
- some other event happened to tick the application. Rebuilding the array is what actually
1301
- re-renders.
1302
-
1303
- **The gap this does not close.** `this.actions[0] = { ...this.actions[0], cssClass: 'x' }`
1304
- compiles: `actions` is your field, and the freeze only covers the entries. It keeps the same
1305
- array reference, so the toolbar does not re-render. Reassign the array — or, if you hold it
1306
- in a `signal`, `update()` it.
1307
-
1308
- The descriptor a toolbar resolves is no longer written back into the entry either. Nothing
1309
- outside the toolbar read it; if you did, resolve it yourself through
1310
- `UiActionDescriptorService.getActionDescriptor(uiAction)` — which is
1311
- [synchronous in 7.0](#getactiondescriptor-is-synchronous-phase-39).
1312
-
1313
- #### Client-side translation is gone
1314
-
1315
- `SmartTranslateService`, `viewContext.translateService` and the `translateServiceChanged`
1316
- subject are removed, together with `originalLabel` / `originalPlaceholder` on the widget
1317
- interfaces. No measured host assigned any of them — the three references in p014/p043 were
1318
- unused imports — and the backend localizes server-side.
1319
-
1320
- What the frontend authors itself is now configuration:
1321
-
1322
- ```ts
1323
- provideSmartNgClient({
1324
- errorDialog: { title: 'Hiba', message: 'Váratlan hiba történt.', buttonLabel: 'Rendben' },
1325
- });
1326
- ```
1327
-
1328
- `SmartViewContextService.getSmartViewContextApiErrorByCode()` is no longer `async`.
1329
-
1330
- #### If your host still bootstraps with zone.js
1331
-
1332
- Nothing forces you to drop it — the library works either way, because it no longer depends
1333
- on an application-wide tick. The playground runs `provideZonelessChangeDetection()` with no
1334
- `zone.js` polyfill, and that is the configuration 7.0 is tested in.
1335
-
1336
- If you do go zoneless, the same rule applies to your own components: state written from a
1337
- subscription, a promise or a timer needs a signal or a `markForCheck()`; state written from a
1338
- template event or an input does not.
1339
-
1340
- ### Dates: moment → date-fns, and the timezone contract (Phase 3.8)
1341
-
1342
- **This section needs action at install time.** The reasoning is ADR-0008 in the
1343
- `platform-angular2` working copy; the contract itself is stated in §4 below.
1344
-
1345
- #### 1. Swap the peer dependency
1346
-
1347
- `@angular/material-moment-adapter` is out of the library's `peerDependencies`;
1348
- `@angular/material-date-fns-adapter` and `date-fns` are in. `date-fns` is listed explicitly
1349
- rather than left transitive, because the library imports `date-fns/locale` itself.
1350
-
1351
- ```bash
1352
- npm uninstall @angular/material-moment-adapter moment
1353
- npm install @angular/material-date-fns-adapter@^22.0.0 date-fns@^4
1354
- ```
1355
-
1356
- If your own code imports moment for something unrelated, keep it — nothing here forces its
1357
- removal, only the adapter's.
1358
-
1359
- **But check *how* it imports moment.** `import * as moment from 'moment'` stops being callable
1360
- under `moduleResolution: "bundler"`, which the Angular 20 hop turns on: moment is an
1361
- `export =` module, and a namespace import of one is not a callable value. The symptom is
1362
- **`TS2349: This expression is not callable`** on every `moment(...)` call, and it arrives at the
1363
- Angular 20 hop rather than at this step. Two ways out:
1364
-
1365
- - `esModuleInterop: true` plus `import moment from 'moment'` — correct, but it changes how every
1366
- CommonJS import in the app resolves, which is a wide blast radius for one file.
1367
- - Replace the calls. Worth checking first how much moment is actually doing: in p014 the three
1368
- surviving calls were `moment(new Date()).toDate()` (i.e. `new Date()`) and two moments compared
1369
- with `<=` (`Date` compares through `valueOf()` identically). All three became plain `Date`s, and
1370
- **nothing under `src/` imported moment any more** — so the `npm uninstall … moment` above was
1371
- right for that host after all, just for a reason nobody predicted.
1372
-
1373
- #### 2. Leave your `MAT_DATE_LOCALE` provider alone
1374
-
1375
- If you have this line — p014 and p043 both do — **keep it as it is**:
1376
-
1377
- ```ts
1378
- { provide: MAT_DATE_LOCALE, useValue: 'hu-HU' },
1379
- ```
1380
-
1381
- The strict reading of the date-fns adapter would require a `Locale` **object** here, and a bare
1382
- string would throw inside date-fns the first time a user opened a date field. The library's
1383
- `SmartDateFnsAdapter` accepts both: it resolves `'hu'` / `'hu-HU'` / `'en'` / `'en-US'`
1384
- (case-insensitively) to a date-fns locale, passes a real `Locale` object through untouched, and
1385
- falls back to Hungarian with a `console.warn` for anything else.
1386
-
1387
- So: **hosts on Hungarian or English need no change.** For any other language, provide a real
1388
- date-fns locale:
1389
-
1390
- ```ts
1391
- import { de } from 'date-fns/locale';
1392
-
1393
- { provide: MAT_DATE_LOCALE, useValue: de },
1394
- ```
1395
-
1396
- `SmartNgClientConfig.dateLocale` is unchanged — still a `string`, still defaulting to `'hu-HU'`.
1397
-
1398
- #### 3. If you override `MAT_DATE_FORMATS`, your format strings will throw — and you probably want to delete the override
1399
-
1400
- Measured across 12 hosts on 2026-07-28: three of them (**p014**, **p043**, **p009-angular**)
1401
- provide their own formats next to the locale, and two more (`app-vlab`, `app-tournament`) have the
1402
- same line commented out:
1403
-
1404
- ```ts
1405
- { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
1406
- ```
1407
-
1408
- with
1409
-
1410
- ```ts
1411
- export const MY_FORMATS = {
1412
- parse: { dateInput: 'YYYY.MM.DD' },
1413
- display: { dateInput: 'YYYY.MM.DD', monthYearLabel: 'YYYY',
1414
- dateA11yLabel: 'LL', monthYearA11yLabel: 'YYYY' },
1415
- };
1416
- ```
1417
-
1418
- Those are **moment tokens**. date-fns treats `YYYY`, `DD`, `YY` and `D` as *protected* and throws a
1419
- `RangeError` on them, and its `LL` means something else entirely (stand-alone month, not a
1420
- localized long date). So this override **crashes at runtime, on the first screen with a date
1421
- field** — the same failure shape as the string locale above, and just as invisible to the compiler.
1422
-
1423
- **The fix is almost certainly deletion.** That `MY_FORMATS` exists to render `YYYY.MM.DD`, which is
1424
- exactly what the library's own `SMART_DATE_FORMATS` now produces (`2026.07.28.`). Drop the
1425
- provider and the constant and you get the format you wanted, plus the more forgiving parsing.
1426
-
1427
- If you genuinely need a different format, translate the tokens rather than copying them:
1428
-
1429
- | moment | date-fns |
1430
- |---|---|
1431
- | `YYYY` | `yyyy` |
1432
- | `DD` | `dd` |
1433
- | `LL` | `PP` (or spell it out: `yyyy. MMMM d.`) |
1434
- | `MMM YYYY` | `yyyy. LLL` |
1435
-
1436
- Note that the library's `parse.dateInput` is an **array** — `DateFnsAdapter._parse` tries each entry
1437
- in turn (and ISO-8601 first), so you can list several accepted spellings instead of one.
1438
-
1439
- #### 4. `SmartNgClientConfig.useUtcDates` is removed
1440
-
1441
- Delete it if you set it. No known host does. It existed for a client-configurable timezone that
1442
- was abandoned as a requirement; the contract is now fixed and explicit:
1443
-
1444
- > **The browser's zone on screen. Zulu (`…Z`) on the wire. The server converts.**
1445
-
1446
- This is unchanged behaviour in practice — `useUtcDates` defaulted to `false`, and the moment
1447
- adapter with `useUtc: false` behaves exactly like a plain `Date`.
1448
-
1449
- #### 5. What a date widget's value now *is*
1450
-
1451
- Form control values from the date pickers are plain **`Date`** objects, where they were
1452
- `Moment` objects. The values reach your backend through a `UiActionRequest` param, so the wire
1453
- format is `JSON.stringify` of that value — and `Date.toJSON()` emits the same ISO-8601 UTC string
1454
- `Moment.toJSON()` did. **Nothing changes on the wire.** Only host code that read a value out of a
1455
- form control and called a moment method on it (`.format()`, `.add()`, `.startOf()`) needs
1456
- rewriting; the platform APIs never exposed a `Moment` in a signature.
1457
-
1458
- #### 6. The one visible difference
1459
-
1460
- Date inputs render **`2026.07.28.`** where 6.x rendered `2026.7.28.` — the documented `YYYY.MM.DD`
1461
- convention. The old rendering was not a decision: `MAT_MOMENT_DATE_FORMATS` used moment's `'l'`,
1462
- which strips the zero padding off the Hungarian `L`. Typed input is **more** forgiving than 6.x,
1463
- not less: `2026.07.28.`, `2026.07.28`, `2026.7.28.`, `2026.7.28` and ISO-8601 are all accepted.
1464
-
1465
- The month picker is unchanged (`07/2026`), and grid/table date columns were never affected —
1466
- `SmartDatePipe` and friends extend Angular's own `DatePipe` and never used moment.
1467
-
1468
- **Verified on a running host** (p014, 2026-07-29, live backend, `Europe/Budapest`). Picking
1469
- 2026-07-15 in a `Kezdődátum` field gave, end to end:
1470
-
1471
- | | |
1472
- |---|---|
1473
- | input | `2026.07.15.` |
1474
- | widget value | `instanceof Date` — not a moment, not a luxon `DateTime` |
1475
- | `JSON.stringify` of that value | `"2026-07-14T22:00:00.000Z"` |
1476
- | `getHours()` | `0` |
1477
-
1478
- That third row is the whole contract in one line: the value's `toJSON()` **is** the wire format
1479
- (see §4), and it is zulu, while the screen and `getHours()` stay local — 15 July 00:00 CEST is
1480
- 14 July 22:00 UTC. A grid column on the same host rendered `2026.04.20 0:03`, confirming the
1481
- untouched pipe path.
1482
-
1483
- ### HTTP: the library no longer forces the XHR backend (Phase 3.8)
1484
-
1485
- `provideSmartNgClient()` used to pass `withXhr()` to its internal `provideHttpClient()` call,
1486
- overriding the Angular 22 default on your behalf. It no longer does — the application runs on the
1487
- framework default **fetch** backend.
1488
-
1489
- For almost every host this is invisible. The one behavioural difference that matters: **the fetch
1490
- backend does not support upload progress.** A request with `reportProgress: true` and a body
1491
- throws `NG2824` instead of emitting `HttpUploadProgress` events.
1492
-
1493
- Measured 2026-07-28 across 12 hosts: **`reportProgress: true` and `observe: 'events'` appear
1494
- nowhere** outside the generated API services' unused parameters. So this is a theoretical risk for
1495
- the current host set, not a live one. If *your* code does ask for upload progress, pass the
1496
- feature back in:
1497
-
1498
- ```ts
1499
- import { withXhr } from '@angular/common/http';
1500
-
1501
- provideSmartNgClient({ httpFeatures: [withXhr()] })
1502
- ```
1503
-
1504
- The upside is that the "**do not call `provideHttpClient()` twice**" trap this guide warned about
1505
- is gone rather than merely documented: a second call now re-provides the same backend the library
1506
- already uses. The library's interceptors were never at risk from it — they are `multi` providers.
1507
-
1508
- ### `getActionDescriptor()` is synchronous (Phase 3.9)
1509
-
1510
- `UiActionDescriptorService.getActionDescriptor(uiAction)` returns a `UiActionDescriptor`.
1511
- It used to return a `Promise<UiActionDescriptor>`, and it was `async` for exactly one
1512
- reason: the client-side translation layer that [Phase 3.7](#client-side-translation-is-gone)
1513
- retired. Nothing it reads has been asynchronous since, so every caller was awaiting a value
1514
- that was already there.
1515
-
1516
- **If you only `await` the call, you are already fine.** `await` on a non-promise is legal
1517
- TypeScript, so this compiles and behaves identically before and after. The codemod drops the
1518
- `await` anyway, so the code stops claiming an asynchrony that is not there:
1519
-
1520
- ```ts
1521
- const descriptor = await this.uiActionDescriptor.getActionDescriptor(uiAction); // 6.x
1522
- const descriptor = this.uiActionDescriptor.getActionDescriptor(uiAction); // 7.0
1523
- ```
1524
-
1525
- Two shapes genuinely break. Measured 2026-07-28 across the 12 hosts in scope: **5 hosts,
1526
- 7 sites** — and none of them in a host's own `getActionDescriptor**s**()` map helper, which
1527
- is an unrelated API and is left alone.
1528
-
1529
- **1. A subclass that overrides it** — `TS2416`, because `Promise<UiActionDescriptor>` is not
1530
- assignable to `UiActionDescriptor`. Found in three hosts, as a byte-identical
1531
- `MdmUiActionDescriptorService`. **The codemod rewrites this one.**
1532
-
1533
- ```ts
1534
- // 6.x
1535
- override async getActionDescriptor(uiAction: UiAction): Promise<UiActionDescriptor> {
1536
- let d: UiActionDescriptor = await super.getActionDescriptor(basicUiAction);
1537
-
1538
- }
1539
-
1540
- // 7.0
1541
- override getActionDescriptor(uiAction: UiAction): UiActionDescriptor {
1542
- let d: UiActionDescriptor = super.getActionDescriptor(basicUiAction);
1543
-
1544
- }
1545
- ```
1546
-
1547
- This is also the service you hand to a toolbar through `[uiActionDescriptorService]`, so a
1548
- host with a custom one hits this at compile time rather than on screen.
1549
-
1550
- **2. `.then(…)` on the result** — `TS2339`, because a descriptor has no `.then`. Found in
1551
- two hosts, four sites. **The codemod reports this one and does not rewrite it**: turning a
1552
- callback into straight-line code means moving its body, which is a judgment call. The fix is
1553
- mechanical anyway — assign, then inline:
1554
-
1555
- ```ts
1556
- // 6.x
1557
- constructForm() {
1558
- this.actionDescriptorService.getActionDescriptor(this.uiAction).then((d) => {
1559
- this.buttonTitle = d.title;
1560
- this.buttonColor = d.color;
1561
- });
1562
-
1563
- }
1564
-
1565
- // 7.0
1566
- constructForm() {
1567
- const d = this.actionDescriptorService.getActionDescriptor(this.uiAction);
1568
- this.buttonTitle = d.title;
1569
- this.buttonColor = d.color;
1570
-
1571
- }
1572
- ```
1573
-
1574
- If the callback was the reason a field lived in a `signal` — because the value arrived after
1575
- the first render — it no longer needs to. The library made that same simplification in its
1576
- own three action dialogs and its snack bar.
1577
-
1578
- **What went synchronous with it, inside the library:** the toolbar's descriptor resolution,
1579
- `SmartGridComponent`'s header construction, and `SmartTreeGenericService.cacheActionDesciptors()`
1580
- and **`syncTree()`**. `syncTree()` now returns `void`, which is what
1581
- `SmartTreeServiceInterface` always declared; a host that calls it and discards the result —
1582
- the only shape measured — needs no change, and a host that `await`s it still compiles.
1583
-
1584
- #### The same cleanup, elsewhere: `SmartdialogService.closeDialog()` returns `void`
1585
-
1586
- A sweep for the same shape found eight more library functions declared `async` with nothing
1587
- asynchronous in them. Only one of them is on a surface a host touches:
1588
- **`SmartdialogService.closeDialog(stopPropagate?)` returns `void`** instead of
1589
- `Promise<void>`.
1590
-
1591
- **Nothing to do.** Both measured hosts that extend `SmartdialogService` override this method
1592
- as `override async closeDialog(): Promise<void>`, and that **still compiles**: TypeScript
1593
- accepts any return type where the base declares `void`. `await this.closeDialog()` keeps
1594
- compiling too. This is the difference from `getActionDescriptor` above — that one returns a
1595
- *value*, which is why the same override shape fails there with `TS2416` and not here.
1596
-
1597
- You may of course drop the `async` from your own override; nothing requires it.
1598
-
1599
- ### Upload actions now wait for the server, and report their failures (Phase 3.9)
1600
-
1601
- **This one changes behaviour, on purpose.** `SmartViewContextService.performUploadAction()`
1602
- and `performUploadMultipleAction()` used to start the upload and resolve immediately,
1603
- without waiting for it and without passing on its failure. Every other action in the family
1604
- (`performAction`, `performWidgetAction`, `performWidgetMainAction`, `dataChanged`) always
1605
- waited; the two upload ones were an omission.
1606
-
1607
- Two things follow, and both are visible in a running app:
1608
-
1609
- 1. **An upload action now blocks until the server has answered.** Before, the action was
1610
- "done" the moment the request went out — so `UiActionService` showed its success snackbar
1611
- over an upload that was still in flight, and anything the action was sequenced before ran
1612
- against a view the server had not updated yet. Now the snackbar appears when the upload
1613
- really has succeeded. On a large file that is a longer wait than 6.x showed you.
1614
-
1615
- 2. **A failed upload now reaches your error handling.** The rejection used to be dropped —
1616
- it became an unhandled promise rejection, `UiActionService`'s `catch` never ran, and the
1617
- user was told the upload had worked. It now propagates, which means
1618
- `setActionErrorHandler()` (or the default error dialog) will fire on upload failures it
1619
- never fired on before. **If your host reports "new" upload errors after upgrading, they
1620
- are not new** — they were happening silently.
1621
-
1622
- Multi-file uploads also go out **one batch at a time** now. The batches were already being
1623
- computed from the descriptor's `maxSize` and `maxBatchSize`, but they were then all sent at
1624
- once, which put the whole set on the server simultaneously and defeated the split. If your
1625
- backend measured upload concurrency, this is why it drops.
1626
-
1627
- Nothing to change in host code.
1628
-
1629
- ### New in 7.0: a grid row can render a backend layout (#29717)
1630
-
1631
- A card-mode grid row that carries `layoutDescriptor.componentLayouts['GRID_ROW_LAYOUT']`
1632
- renders that layout instead of the card component the host registered under
1633
- `'<GRID_ID>Card'`. Both branches stay: a row without the descriptor behaves exactly as
1634
- before, and this needs no host change.
1635
-
1636
- Two consequences worth knowing if you style or extend it:
1637
-
1638
- - The rendered layout's root element gets the `gridCardLayout` class, applied by the card to
1639
- its own child rather than pushed into the `style.classesToAdd` of the object the backend
1640
- sent. Nothing mutates the server model.
1641
- - **The row's actions reach the layout's toolbars by a pull, not a walk.** The card provides
1642
- a `SmartActionHost` for its row subtree and writes the row's action list into it; a toolbar
1643
- inside the layout resolves that host before falling back to the page client. If you build
1644
- a component that owns such a subtree, do the same — `WIDGETS.md`, *Toolbars*.
1645
-
1646
- This shipped in 7.0.0 rather than as a 6.0.3x patch because it is built on the inverted
1647
- widget contract that 7.0 introduces.
1648
-
1649
- ### Writing your own widget
1650
-
1651
- `WIDGETS.md`, next to this file, is the protocol: how a widget finds its screen component
1652
- through DI and registers itself, when to opt out with `[smartComponentDetached]`, what has to
1653
- be a signal now that there is no zone, how a toolbar resolves its actions, and the three
1654
- traps that have actually bitten (imperatively-fed inputs cannot be signal inputs; `@for`
1655
- tracking over backend objects; clear-then-apply styling).
1656
-
1657
- In 6.x this was not possible at all `SmartComponentApiClient` collected its children with
1658
- eight `@ViewChildren` over eight concrete widget classes.
1659
-
1660
- ### Imperatively created components are given their inputs properly
1661
-
1662
- `ComponentFactoryService` creates the components the library instantiates by hand — grid
1663
- cards, expanded rows, table cell components, the expandable section's content, the form's
1664
- `COMPONENT` widget, the filter editor's field editors — and most of those classes come from a
1665
- host. It used to write their inputs by assigning the field. Two things follow, and both are
1666
- fixed:
1667
-
1668
- - **A declared input is now written with `ref.setInput()`**, i.e. the same way a template
1669
- binding writes it. So a **signal input** works (assignment used to overwrite the input
1670
- function with the value, and the component's next `this.x()` threw `x is not a function`),
1671
- an **aliased** input is addressable under either name, and **`ngOnChanges` runs**.
1672
- - **Falsy values arrive.** The old guard was `if (value)`, so `false`, `0` and `''` never
1673
- reached the component. Only `undefined` is skipped now, which keeps meaning "not passed".
1674
-
1675
- If your component takes something from the library this way and the key is **not** a declared
1676
- input, the field is still assigned — nothing breaks — but you get one `console.warn` per
1677
- component type and key. Declaring it with `@Input()` (or as a signal input) is the fix.
1678
-
1679
- **Check your falsy defaults.** If a host component relied on the old guard — expecting to
1680
- keep its own default when the library passed `false` or `''` it now receives the value.
1681
- This is the only part of 7.0 where a bugfix can change what you see on screen without any
1682
- code of yours changing.
1683
-
1684
- ### Bugfixes shipped with 7.0
1685
-
1686
- - `SmartformwidgetComponent.ngAfterViewInit` no longer crashes with
1687
- `Cannot read properties of undefined (reading 'valueChanges')` when the form
1688
- contains a `MONTH_PICKER` widget (it wrongly subscribed to the non-existent
1689
- `<key>-time` control; only `DATE_TIME_PICKER` has one).
1690
- - A widget nested in a CONTAINER receives `blurSophisticatedValueChange`; the recursion never
1691
- passed it down, so a BLUR-mode child would have thrown on blur.
1692
- - `SmartformComponent` reaches widgets nested in a CONTAINER when it changes them in place
1693
- (values, constraints, the touched state after a failed submit). The view query it used
1694
- could not see them.
1695
- - `UiActionDescriptorService` no longer shares one dialog object between actions. Any action
1696
- whose descriptor had no `dialog` of its own was given the service's single placeholder — the
1697
- same object every time — and had its title and button caption written into it, so resolving
1698
- one action renamed the dialog every other one was holding. The service is
1699
- `providedIn: 'root'`, so this outlasted the view.
1700
-
1701
- What you saw on screen, measured on both versions:
1702
-
1703
- | The action | 6.x, when the dialog opened | 7.0 |
1704
- |---|---|---|
1705
- | described by the backend, no dialog of its own | **another action's code** | its own code |
1706
- | described by nothing at all | **another action's code**, or empty | empty |
1707
- | registered client-side, no dialog of its own | its own code | unchanged |
1708
-
1709
- The middle column is not a typo. An action the backend describes is resolved twice in the
1710
- normal flow — once when the toolbar renders it, again when the dialog it opens resolves it
1711
- for itself and the first resolution wrote a dialog onto the action's own descriptor, so the
1712
- second one skipped the step that fills in the title. What the dialog then showed was whatever
1713
- code had last passed through the shared object. An **already open** dialog could change too,
1714
- since its title is re-read on every change-detection pass.
1715
-
1716
- An action nothing describes still gets an untitled placeholder dialog: its *button* carries
1717
- the code, the dialog does not. That is unchanged and deliberate such an action is a gap in
1718
- the host's descriptor map, and this is the honest rendering of it.
1719
-
1720
- A resolved descriptor is now the caller's own object throughout, which also means
1721
- `getActionDescriptor()` no longer writes a placeholder dialog and a `SNACKBAR` feedbackType
1722
- 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).
466
+
467
+ ### Angular 20 hop (Phase 2, 19 → 20)
468
+
469
+ The library line now builds against Angular 20. Host-relevant changes:
470
+
471
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
472
+ `@angular/*` `^20.0.0`, `@angular/youtube-player` `^20.0.0`,
473
+ `ngx-quill` `^28.0.0` (the Angular 20 line; quill stays `^2.0.3`),
474
+ `ngx-mask` `^20.0.0`, and — for `@smartbit4all/document-explorer` —
475
+ `ngx-extended-pdf-viewer` `^23.0.0` (the 22.x line caps its Angular peer
476
+ below 20). No pdf-viewer API rename at this bump (unlike 22.x).
477
+ - **TypeScript 5.9 required** (Angular 20 supports 5.8–5.9); `ng update`
478
+ bumps it automatically. TS 5.9 adds **TS2872 "This kind of expression is
479
+ always truthy"**, which turns previously-silent dead `||` alternatives into
480
+ build errors — expect one or two in any older codebase.
481
+ - **Material 20 button labels no longer set `white-space: nowrap`.** Material
482
+ 20 dropped the MDC stylesheet (`@material/button`), which used to carry it.
483
+ Multi-word button labels now wrap, and any rule that lets the button size to
484
+ its content (the lib's `.mdc-button { height: fit-content }`) makes the
485
+ button grow taller. The lib restores the pre-20 look inside
486
+ `ui-action-button` (`.mdc-button__label { white-space: nowrap }`); **hosts
487
+ that render Material buttons of their own must add the same rule** if they
488
+ relied on single-line labels (typical symptom: a navigation bar that
489
+ suddenly wraps and overflows into scroll arrows).
490
+ - **Material 20 renamed the MDC-era CSS custom properties**: e.g.
491
+ `--mdc-text-button-container-height` → `--mat-button-text-container-height`.
492
+ The `ng update @angular/material@20` schematic rewrites the ones it finds in
493
+ `.css`/`.scss` files; token names embedded in TS strings or host templates
494
+ must be renamed by hand.
495
+ - **`DOCUMENT` moved from `@angular/common` to `@angular/core`.** Automatic
496
+ migration; `@angular/common`'s re-export is deprecated.
497
+ - **`moduleResolution: "node"` → `"bundler"`** in `tsconfig.json` (and any
498
+ `tsconfig.lib.json` that sets it explicitly). Automatic migration; it changes
499
+ how sub-path exports resolve, so a host with hand-written `paths` entries
500
+ should re-verify its build.
501
+ - **Workspace generation defaults**: the CLI adds a `schematics` block to
502
+ `angular.json` pinning the pre-20 file-naming style (`type: "component"`,
503
+ `typeSeparator: "."`). Cosmetic — it only affects newly generated files.
504
+ - Optional migrations **not** run here (deferred to the modernization phase):
505
+ `use-application-builder` (esbuild), `control-flow-migration`,
506
+ `router-current-navigation`.
507
+ - The `InjectFlags`, `TestBed.get` → `TestBed.inject` and
508
+ `provideServerRendering` migrations were no-ops for this codebase.
509
+ - The NG8111 warnings introduced at the 19 hop are no longer emitted by the
510
+ Angular 20 compiler (the dead bindings themselves are unchanged).
511
+
512
+ ### Angular 21 hop (Phase 2, 20 → 21)
513
+
514
+ The library line now builds against Angular 21. Host-relevant changes:
515
+
516
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
517
+ `@angular/*` `^21.0.0`, `@angular/youtube-player` `^21.0.0`,
518
+ `ngx-quill` `^29.0.0` (the Angular 21 line; quill stays `^2.0.3`),
519
+ `ngx-mask` `^21.0.0`, and — for `@smartbit4all/document-explorer` —
520
+ `ngx-extended-pdf-viewer` `^25.0.0` (the 24.x line still caps its Angular
521
+ peer below 21). No pdf-viewer API rename at this bump.
522
+ - **`ngx-quill` 30.x is intentionally *not* used.** 30.x is the "zoneless"
523
+ line and drops the `zone.js` peer dependency; the 7.0 lib is still
524
+ zone-based. Hosts that have already gone zoneless may use 30.x, but the
525
+ lib is only validated against 29.x.
526
+ - **TypeScript stays 5.9** and **zone.js stays 0.15** (Angular 21 accepts
527
+ `~0.15.0 || ~0.16.0`). **Node** must satisfy
528
+ `^20.19.0 || ^22.12.0 || >=24.0.0`.
529
+ - **`MatCommonModule` is removed from `@angular/material/core` (breaking).**
530
+ It was already deprecated in Material 20 (`@breaking-change 21.0.0`). Any
531
+ host NgModule that imports it will fail to compile with
532
+ `Unknown reference` — and because a broken NgModule cascades, the real
533
+ error usually shows up as a flood of `NG8001: '<some-component>' is not a
534
+ known element` / `NG6002: … does not appear to be an NgModule class`
535
+ further down the build log. **Fix: just delete the import and the array
536
+ entry.** `MatCommonModule` had no template surface; it only
537
+ (a) applied the `cdk-high-contrast-*` body classes — `A11yModule` still
538
+ does this, and Material components that need it import `A11yModule`
539
+ themselves — and (b) re-exported `BidiModule`. If your host actually uses
540
+ the `Dir` directive or injects `Directionality`, import
541
+ `BidiModule` from `@angular/cdk/bidi` explicitly.
542
+ The lib dropped it from all 10 of its own modules.
543
+ - **`provideZoneChangeDetection()` is now added at bootstrap** by the
544
+ `ng update` migration, e.g.
545
+ `bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()] })`.
546
+ This keeps zone-based change detection explicit; behaviour is unchanged.
547
+ - **The control-flow migration (`*ngIf` → `@if`) now runs as a *mandatory*
548
+ `ng update` migration**, not an optional one. It rewrites every structural
549
+ directive in the workspace. The lib **reverted** it to keep the version hop
550
+ reviewable — `NgIf`/`NgForOf`/`NgSwitch` still ship in Angular 21, so
551
+ `*ngIf`/`*ngFor` keep working. Hosts may keep or revert it as they prefer;
552
+ if you want it as a separate change, run
553
+ `npx ng generate @angular/core:control-flow` on its own commit.
554
+ - **New `NG8107` extended diagnostic** (warning): an optional chain `?.`
555
+ whose left side can no longer be `null`/`undefined` under Angular 21's
556
+ sharpened template inference. Warning only — expect a batch of them in any
557
+ older template set.
558
+ - **`tsconfig.json` loses its explicit `lib` array** (the CLI derives it from
559
+ `target`). Automatic migration.
560
+ - The Material and CDK v21 migration schematics made no source changes, and
561
+ the `Router.lastSuccessfulNavigation` migration was a no-op. The optional
562
+ `router-current-navigation` migration was not run.
563
+ - The `.mdc-button__label { white-space: nowrap }` workaround introduced at
564
+ the Angular 20 hop is **still required on Material 21** — the class still
565
+ exists in the button template and Material still ships no `white-space`
566
+ rule for it.
567
+
568
+ ### Angular 22 hop (Phase 2, 21 → 22) — final hop
569
+
570
+ The library line now builds against **Angular 22**. This is the biggest
571
+ host-facing hop of the series: it changes the **change-detection default**, the
572
+ **HTTP backend default** and the **meaning of `?.` in templates**. All three are
573
+ handled by `ng update` migrations, but you must let them run.
574
+
575
+ - **Peer dependency bumps** (hosts must upgrade together with the lib):
576
+ `@angular/*` `^22.0.0`, `@angular/youtube-player` `^22.0.0`,
577
+ `ngx-quill` `^31.0.0` (the Angular 22 line; quill stays `^2.0.3`),
578
+ `ngx-mask` `^22.0.0`, and — for `@smartbit4all/document-explorer` —
579
+ `ngx-extended-pdf-viewer` `^28.0.0` (25/26/27 all cap their Angular peer
580
+ below 22). **No pdf-viewer API rename at this bump.**
581
+ `@ngx-translate/core` `^14` still works (its peer is `>=13`).
582
+ - **TypeScript must be 6.0** (`@angular/compiler-cli` peers `>=6.0 <6.1`);
583
+ **zone.js stays 0.15** (`~0.15.0 || ~0.16.0`) and **rxjs is unchanged**.
584
+ **Node** must satisfy `^22.22.3 || ^24.15.0 || >=26.0.0` — note the
585
+ **22.22.3** floor, which is stricter than Angular 21's `^22.12.0`.
586
+
587
+ #### 1. `ChangeDetectionStrategy.OnPush` is the new default
588
+
589
+ In Angular 22 the enum is redefined: `OnPush = 0` (default), `Eager = 1`, and
590
+ `Default = 1` is kept as a **deprecated alias of `Eager`**. Any component
591
+ without an explicit `changeDetection` therefore switches from CheckAlways to
592
+ OnPush — a real behaviour change for components that mutate their own state
593
+ without signals or `markForCheck()`.
594
+
595
+ An `ng update` schematic **automatically adds
596
+ `changeDetection: ChangeDetectionStrategy.Eager` to every component that had
597
+ none**, which keeps behaviour bit-identical. The lib took exactly this route
598
+ (114 components) and did **not** opt into OnPush at this hop. Recommended host
599
+ approach: let the schematic run, verify your app, and move components to OnPush
600
+ deliberately afterwards.
601
+
602
+ Watch out for components the schematic can skip — it only touches components it
603
+ can resolve. After updating, grep for `@Component` files with no
604
+ `changeDetection` and decide about each one; a component that is dynamically
605
+ instantiated but never statically declared can slip through.
606
+
607
+ #### 2. `provideHttpClient` now defaults to the fetch backend
608
+
609
+ Angular 22 switches `HttpClient` from `XMLHttpRequest` to `fetch`. An
610
+ `ng update` migration inserts **`withXhr()`** into your `provideHttpClient(...)`
611
+ calls to preserve the old backend:
612
+
613
+ ```ts
614
+ provideHttpClient(withXhr(), withInterceptorsFromDi())
615
+ ```
616
+
617
+ The lib does this in `SmartViewContextModule`. **Hosts that call
618
+ `provideHttpClient` themselves must do the same** (or consciously move to
619
+ fetch). This matters most for **upload progress**: `reportProgress` events
620
+ behave differently on the fetch backend, so if your host reports upload
621
+ progress, keep `withXhr()` until you have retested it.
622
+
623
+ #### 3. `?.` in templates now yields `undefined`, not `null`
624
+
625
+ Angular 22's safe-navigation operator returns `undefined` where it used to
626
+ return `null`. Where that difference is observable, an `ng update` migration
627
+ wraps the expression in the **`$safeNavigationMigration()` compiler builtin**:
628
+
629
+ ```html
630
+ [ngStyle]="calcStyle($safeNavigationMigration(diagramModel?.descriptor?.style))"
631
+ ```
632
+
633
+ This is an official builtin (handled like `$any()` — the compiler unwraps it and
634
+ the type-checker sees straight through it), not a temporary marker, so wrapped
635
+ templates are safe to keep. The same `ng update` run also sets the
636
+ `nullishCoalescingNotNullable` and `optionalChainNotNullable` extended
637
+ diagnostics to `suppress` in your tsconfigs, which silences the `NG8107`
638
+ warnings the Angular 21 hop introduced.
639
+
640
+ #### 4. `ComponentFactoryResolver` is removed (breaking)
641
+
642
+ `ComponentFactoryResolver` is gone and `ComponentFactory` is no longer public
643
+ (it only survives as `ɵRender3ComponentFactory`). Code like this stops
644
+ compiling with `TS2305` / `NG2003`:
645
+
646
+ ```ts
647
+ constructor(private resolver: ComponentFactoryResolver) {}
648
+ ngAfterViewInit() {
649
+ const factory = this.resolver.resolveComponentFactory(MyComponent);
650
+ this.vcRef.createComponent(factory);
651
+ }
652
+ ```
653
+
654
+ Replace it with the type-based overload — behaviour is identical (v22 builds the
655
+ same factory internally and resolves the injector from the parent):
656
+
657
+ ```ts
658
+ ngAfterViewInit() {
659
+ this.vcRef.createComponent(MyComponent);
660
+ }
661
+ ```
662
+
663
+ **Public-API change in the lib:** `ComponentFactoryService` no longer exposes
664
+ its `factory` field. `createComponent()` / `destroyComponent()` keep their
665
+ signatures, so hosts that only call those need no change.
666
+
667
+ #### 5. TypeScript 6.0 deprecates `baseUrl` and `downlevelIteration`
668
+
669
+ TS 6.0 raises **`TS5101`** for both options ("deprecated and will stop
670
+ functioning in TypeScript 7.0"). Two ways out:
671
+
672
+ - Remove them. `downlevelIteration` is a no-op at `target: ES2022`. `baseUrl`
673
+ can only go once every non-relative first-party import (`from 'projects/…'`,
674
+ `from 'src/…'`) is relative or covered by a `paths` entry.
675
+ - Or silence them for now with `"ignoreDeprecations": "6.0"` in
676
+ `compilerOptions`. This is what the lib workspace does, because `baseUrl` is
677
+ still load-bearing there. It only buys time until TS 7.
678
+
679
+ #### 6. Other notes
680
+
681
+ - **Material 22 removed no further modules or tokens** — the CDK and Material
682
+ v22 migration schematics made no source changes. CDK 22 moves to a
683
+ package `exports` map (the per-entry-point sub-directories are gone from
684
+ `node_modules/@angular/cdk`), but every `@angular/cdk/*` import still
685
+ resolves; this only matters if you referenced those paths on disk.
686
+ - The `.mdc-button__label { white-space: nowrap }` workaround introduced at the
687
+ Angular 20 hop is **still required on Material 22** — the class still appears
688
+ in Material's button template and Material still ships no `white-space` rule
689
+ for it.
690
+ - **The control-flow migration did not re-run at this hop** (it was mandatory at
691
+ 21). The lib still ships `*ngIf`/`*ngFor` templates; `NgIf`/`NgForOf`/
692
+ `NgSwitch` continue to work in Angular 22.
693
+ - **The webpack-based builders are deprecated.** `@angular-devkit/build-angular`
694
+ now prints a notice recommending `@angular/build`. Nothing breaks yet, but
695
+ plan the switch.
696
+ - `strictTemplates` is on by default in v22; the lib workspace was already
697
+ strict, so nothing changed. If your host was not strict, the `ng update`
698
+ migration adds `"strictTemplates": false` to keep it that way.
699
+
700
+ ### The third-party packages the library does *not* own (Phase 2)
701
+
702
+ Every hop section above lists the library's own peer dependencies. Your host almost
703
+ certainly carries packages the library never sees, and those have their own Angular
704
+ peer ceilings — each one can stop a hop dead. Measured on p014 (2026-07-29), which
705
+ went 17 → 22 with the library still installed:
706
+
707
+ - **`@swimlane/ngx-charts` needs a ladder of its own**: `20 → 22 → 22 → 23 → 24 → 25`
708
+ across the five hops. At the Angular 21 hop it peer-caps the CDK, and because
709
+ `ng update`'s own `npm` step is **not** `--force`d (even when you passed `--force`
710
+ to `ng update`), the install fails there while `package.json` has already been
711
+ written correctly. The fix both times was to redo the install by hand, not to
712
+ re-run the update. `p043` and `app-tournament` both carry ngx-charts `^20.4.1`
713
+ and will hit this; `app-fitnessmirror` and `app-finance-ai` do not.
714
+ - **`ngx-extended-pdf-viewer` moved to `.mjs`** at the 21 hop and stopped shipping
715
+ `pdf-3.10.560-es5.min.js`. If your `angular.json` names that file under `scripts`
716
+ or `assets`, the build fails on a missing file rather than on anything Angular.
717
+ - **`karma` and `@angular/build`**: `@angular/build` 22 declares
718
+ `peerOptional karma@^6.4.0`. A host pinned to `~6.3.0` gets a wall of
719
+ `npm warn ERESOLVE overriding peer dependency` — **warnings, exit code 0**, so it
720
+ is not blocking. It becomes a *hard* install failure the moment `@angular/build`
721
+ is a direct devDependency rather than a nested one, which is what happened in the
722
+ library workspace. `karma@~6.4.4` clears it.
723
+
724
+ The general shape: **`ng update` needs `--force` at every hop** (the library and
725
+ PrimeNG both peer-cap at Angular 17 until the very end), and `--force` covers the
726
+ Angular schematics but not the `npm install` they trigger. When that install fails,
727
+ check `package.json` before assuming the hop did nothing — it is usually already
728
+ correct.
729
+
730
+ ### Housekeeping (Phase 3.0)
731
+
732
+ #### The dialog's built-in Ok/Cancel buttons are gone
733
+
734
+ `smartdialog.component.html` no longer renders an `Ok` and a `Cancel` button.
735
+ They were dead: their `(click)` handlers were the no-op expressions
736
+ `(click)="(data.okCallback)"` / `(click)="(data.cancelCallback)"` — a bare
737
+ property read, not a call — so pressing them did nothing, and the whole block
738
+ was behind `*ngIf="!data.customComponent"`, which is never true for a host
739
+ dialog (`SmartDialog.ngAfterViewInit` unconditionally instantiates
740
+ `data.customComponent`).
741
+
742
+ **The `okCallback` / `cancelCallback` model fields stay.** Hosts set and invoke
743
+ them from their own `customComponent` dialogs, and that keeps working
744
+ unchanged. Only the library-rendered buttons are gone. The `actionCallback` /
745
+ `actionLabel` button — the one with a real handler (`onActionClick()`) — is
746
+ untouched.
747
+
748
+ Action needed: none, unless you relied on the library rendering those two
749
+ buttons — in which case they never worked, and you should render them in your
750
+ own dialog component.
751
+
752
+ ### Full standalone + `provideSmartNgClient()` (Phase 3.2) — **the big one**
753
+
754
+ This is the change that rewrites your `app.module.ts`. Everything else in this
755
+ guide is small next to it.
756
+
757
+ #### Every NgModule is gone
758
+
759
+ The library no longer ships a single `@NgModule`. All 58 are deleted —
760
+ `SmartNgClientModule`, `SmartViewContextModule`, `SmartSessionModule`,
761
+ `SmartGridModule`, `SmarttableModule`, `SmarttreeModule`, `SmartdialogModule`,
762
+ `SmartIconModule`, `SmartNavbarModule`, `SmartFilterModule`,
763
+ `SmartFilterEditorModule`, `SmartComponentLayoutModule`,
764
+ `SmartExpandableSectionModule`, `SmartMapModule`, `SmartDiagramModule`,
765
+ `SmartNavigationModule`, `SmartValidationModule`, `SmartGenericPagesModule`,
766
+ `ComponentFactoryServiceModule`, `SharedModule`, `SmartTabGroupModule`,
767
+ `SmartDocuStoreExplorerModule` and the generated OpenAPI `ApiModule`s.
768
+
769
+ Every component, directive and pipe is now **standalone**. Import the ones your
770
+ templates use directly, in the component that uses them:
771
+
772
+ ```ts
773
+ @Component({
774
+ selector: 'app-my-page',
775
+ templateUrl: './my-page.component.html',
776
+ imports: [SmartGridComponent, UiActionToolbarComponent, SmartEmbeddedSlotDirective],
777
+ })
778
+ export class MyPageComponent extends SmartComponent<MyModel> { … }
779
+ ```
780
+
781
+ **The compiler tells you what is missing** — one `NG8001: '<smart-grid>' is not
782
+ a known element` per unresolved element, per template. Work through them; there
783
+ is no guessing involved. In practice ~16 module imports become ~40 component
784
+ imports spread across the components that actually use them.
785
+
786
+ **The compiler does NOT tell you about the DI changes below.** Nothing fails at
787
+ build time if you get those wrong; you find out on the first page load. Smoke
788
+ your app.
789
+
790
+ #### `provideSmartNgClient(config, ...features)` replaces the module imports
791
+
792
+ ```ts
793
+ bootstrapApplication(AppComponent, {
794
+ providers: [
795
+ provideAnimations(),
796
+ provideRouter(ROUTES),
797
+ provideSmartNgClient(
798
+ {
799
+ gridMenuIcon: 'more_horiz',
800
+ treeMenuIcon: 'more_horiz',
801
+ aclEditingViewName: Pages.ACL_MATRIX_PAGE,
802
+ invalidSmartlinkPageName: Pages.INVALID_SMARTLINK_PAGE_NAME,
803
+ namedValidators: [MY_VALIDATOR_FACTORY],
804
+ },
805
+ withSmartMap({ engine: MapEngine.LEAFLET }),
806
+ withSmartDiagram({ customOptions: [MY_CHART] })
807
+ ),
808
+ { provide: HTTP_INTERCEPTORS, useClass: MyLoadingInterceptor, multi: true },
809
+ ],
810
+ });
811
+ ```
812
+
813
+ The core is always wired and cannot be forgotten: session, view context, the
814
+ three BFF interceptors, layout, form, grid, table, tree, dialog, icon, navbar,
815
+ filter, filter editor, validation, expandable section, navigation, shared and
816
+ the generic pages. That is the point — the session and header interceptors used
817
+ to be easy to leave out, and leaving them out fails *silently* (no
818
+ `Authorization` header).
819
+
820
+ Only two features are opt-in, because they pull heavy third-party code that not
821
+ every host renders: `withSmartMap()` (leaflet / Google Maps) and
822
+ `withSmartDiagram()` (chart.js). There is **no `withSmartTabGroup()`** —
823
+ `smart-tab-group` had no component left, only an empty module, and is deleted.
824
+
825
+ `provideSmartNgClient()` calls `provideHttpClient()` itself, with
826
+ `withInterceptorsFromDi()` and its own three functional interceptors, and leaves the backend
827
+ on the framework default (fetch). Your own class-based `HTTP_INTERCEPTORS` keep working and
828
+ still run after the library's three. Pass extra `HttpClient` features in
829
+ `config.httpFeatures` rather than making a second `provideHttpClient()` call — see
830
+ [HTTP](#http-the-library-no-longer-forces-the-xhr-backend-phase-38), which is also where the
831
+ 6.x "do not call it twice" trap went.
832
+
833
+ #### String-keyed providers become typed config fields
834
+
835
+ Delete these from your providers array and pass them to `provideSmartNgClient`:
836
+
837
+ | 6.x provider | 7.0 config field |
838
+ |---|---|
839
+ | `{ provide: 'gridMenuIcon', useValue: … }` | `gridMenuIcon` |
840
+ | `{ provide: 'treeMenuIcon', useValue: … }` | `treeMenuIcon` |
841
+ | `{ provide: 'searchPageName' / 'searchComponentName', … }` | `searchPageName` / `searchComponentName` |
842
+ | `{ provide: 'genericPageName' / 'genericComponentName', … }` | `genericPageName` / `genericComponentName` |
843
+ | `{ provide: 'subjectSelectorPageName' / 'subjectSelectorComponentName', … }` | `subjectSelectorPageName` / `subjectSelectorComponentName` |
844
+ | `{ provide: 'validationResultPageName' / 'validationResultComponentName', … }` | `validationResultPageName` / `validationResultComponentName` |
845
+ | `{ provide: 'noPermissionPageName' / 'noPermissionComponentName', … }` | `noPermissionPageName` / `noPermissionComponentName` |
846
+ | `{ provide: 'invalidSmartlinkPageName' / 'invalidSmartlinkComponentName', … }` | `invalidSmartlinkPageName` / `invalidSmartlinkComponentName` |
847
+ | `{ provide: 'aclEditingViewName', useValue: … }` | `aclEditingViewName` |
848
+ | `{ provide: DIALOG_DISABLE_CLOSE, useValue: … }` | `dialogDisableClose` |
849
+ | `{ 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) |
850
+ | `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) |
851
+ | `{ provide: NAMED_VALIDATOR, useValue: F, multi: true }` | `namedValidators: [F]` |
852
+ | `{ provide: SMART_DEFAULT_VIEW_COMPONENTS, useValue: E, multi: true }` | `defaultViewComponents: [E]` |
853
+ | `SmartValidationModule.forRoot([...])` | `namedValidators` — **the element type changes**, see below |
854
+ | `SmartDiagramModule.forRoot([...])` | `withSmartDiagram({ customOptions })` |
855
+ | `{ provide: MAP_ENGINE, useValue: … }` | `withSmartMap({ engine })` |
856
+
857
+ `'pageName'`, `'componentName'` and `'treeId'` stay as string tokens on purpose:
858
+ they identify one component instance rather than configuring the library, and
859
+ hosts also provide them at component level.
860
+
861
+ You can also delete these from your **root** providers array — the core provides them:
862
+ `SmartSessionService`, `SmartViewContextService`, `SmartNavigationService`,
863
+ `SmartIconService`, `SmartFilterEditorService`, `SmartFormService`,
864
+ `SmartCookieService`, `NamedValidatorService`, `ComponentFactoryService`, and the date
865
+ adapter wiring (`MAT_DATE_FORMATS`, `DateAdapter`).
866
+
867
+ ⚠️ **`namedValidators` takes factories, not providers.** `SmartValidationModule.forRoot()`
868
+ took `Provider[]`, so hosts wrote the wrapper by hand:
869
+
870
+ ```ts
871
+ // 6.x — validator.factories.ts
872
+ export const VALIDATOR_PROVIDER_EMPTY_INPUT: Provider = {
873
+ provide: NAMED_VALIDATOR, useValue: VALIDATOR_FACTORY_EMPTY_INPUT, multi: true,
874
+ };
875
+ // app.module.ts
876
+ imports: [SmartValidationModule.forRoot([VALIDATOR_PROVIDER_EMPTY_INPUT])]
877
+ ```
878
+
879
+ `provideSmartNgClient()` adds that wrapper itself, so the config takes the bare
880
+ `ValidatorFactory[]`. Export the factories and pass those:
881
+
882
+ ```ts
883
+ // 7.0
884
+ export const VALIDATOR_FACTORY_EMPTY_INPUT: ValidatorFactory = { … };
885
+ provideSmartNgClient({ namedValidators: [VALIDATOR_FACTORY_EMPTY_INPUT] })
886
+ ```
887
+
888
+ The codemod moves the argument into the config and **reports it**, because the entries are
889
+ normally named constants declared in another file and only you know what each one wraps. If
890
+ you miss the note, `TS2322: Type 'Provider' is not assignable to type 'ValidatorFactory'`
891
+ lands on every element.
892
+
893
+ ⚠️ **The same line on a `@Component` is not the same thing.**
894
+ `providers: [SmartFilterEditorService]` on a page component asks for one instance *per page*,
895
+ which is usually deliberate — p014 does it in five components. Leave those alone; the codemod
896
+ does.
897
+
898
+ #### The module → standalone mapping
899
+
900
+ What each `Smart*Module` used to export, reduced to what 7.0 still exports publicly. An
901
+ NgModule's `imports:` array accepts standalone components, so this substitution is all a host
902
+ that is still NgModule-based needs to keep compiling — the codemod performs it.
903
+
904
+ | 6.x module | 7.0 imports |
905
+ |---|---|
906
+ | `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 |
907
+ | `SmartComponentLayoutModule` | `SmartComponentLayoutComponent` |
908
+ | `SmartViewContextModule` | `UiActionToolbarComponent`, `UiActionButtonComponent`, `UiActionDialogButtonComponent`, `SmartformComponent`, `SmartfileuploaderComponent`, `SmartFileEditorComponent`, `SmartMultiFileEditorComponent`, `SmartVoiceRecorderComponent`, `SmartEmbeddedSlotDirective`, `HighlightPipe` |
909
+ | `SmartGridModule` | `SmartGridComponent` |
910
+ | `SmarttreeModule` | `SmartTreeComponent` |
911
+ | `SmartdialogModule` | `SmartDialog` |
912
+ | `SmartIconModule` | `SmartIconComponent`, `UiBadgeComponent`, `UiBadgeDirective` |
913
+ | `SmartNavbarModule` | `SmartNavbarComponent` |
914
+ | `SmartFilterModule` | `SmartFilterComponent` |
915
+ | `SmartFilterEditorModule` | `SmartFilterEditorContentComponent` |
916
+ | `SmartExpandableSectionModule` | `ExpandableSectionComponent` |
917
+ | `SmartSessionModule` | `SmartSessionTimerComponent` |
918
+ | `SmartGenericPagesModule` | `SearchPageComponent`, `GenericPageComponent`, `SubjectSelectorComponent`, `InvalidSmartlinkComponent` |
919
+ | `SmartMapModule` | `SmartMapComponent` + `withSmartMap({ engine })` |
920
+ | `SmartDiagramModule` | `SmartDiagramComponent` + `withSmartDiagram({ customOptions })` |
921
+ | `SharedModule` | `SmartTooltipDirective`, `SmartDatePipe`, `SmartDateTimePipe`, `SmartTimePipe` |
922
+ | `SmartValidationModule` | nothing — `forRoot()`'s validators become `namedValidators` |
923
+ | `SmarttableModule` | nothing — `SmarttableComponent` is no longer public; render a `<smart-grid>` |
924
+ | `SmartNavigationModule`, `ComponentFactoryServiceModule` | nothing — they only carried services, which the core provides |
925
+ | `SmartTabGroupModule` | nothing — deleted; it had no component left |
926
+ | `SmartDocuStoreExplorerModule` (`@smartbit4all/document-explorer`) | `SmartDocuStoreExplorerComponent`, `FolderContentComponent`, `PdfViewerDialogPageComponent` |
927
+
928
+ Two things this table will not tell you, and the compiler will not either: an import you no
929
+ longer need is invisible (unused entries in an NgModule `imports:` array are not reported),
930
+ and a name you already bind from elsewhere — a `SmartDatePipe` of your own — must not be
931
+ imported twice. The codemod reports both cases rather than guessing.
932
+
933
+ #### Your `SmartComponent` subclasses silently become OnPush
934
+
935
+ This is an **Angular 22 fact rather than something 7.0.0 introduces**, but it
936
+ lands on you at the same moment, so it is worth stating plainly: `SmartComponent`
937
+ is an abstract `@Component`, and each of your subclasses carries its own
938
+ `@Component` decorator. On Angular 22 a component without an explicit
939
+ `changeDetection` gets the new `OnPush` default. p014 has 75 such subclasses,
940
+ p043 has 70.
941
+
942
+ Let the `ng update` schematic add `changeDetection: ChangeDetectionStrategy.Eager`
943
+ to all of them (that is what the library did), verify your app, and move to
944
+ OnPush deliberately afterwards. A subclass that mutates its own state outside a
945
+ signal and without `markForCheck()` will stop repainting under OnPush.
946
+
947
+ #### Removed from the public API
948
+
949
+ A major is the only chance to narrow the surface, so these are no longer
950
+ exported. All of them are library internals; none is used by p014, p043,
951
+ app-formmate or `@smartbit4all/document-explorer`:
952
+
953
+ - `SmarttableComponent`, `SmarttableService` — use `<smart-grid>`, which renders
954
+ the table internally. **All `SmartTable*` model types stay** (`SmartTableType`,
955
+ `SmartTableInterfaceTypeEnum`, `SmartTableHeader`, …). p043 imports
956
+ `SmarttableComponent` in `ad-groups.component.ts` but never uses it — delete
957
+ that one import line.
958
+ - `GoogleMap`, `LeafletMap`, `AbstractMap` — select an engine with
959
+ `withSmartMap({ engine })`.
960
+ - `SmartFilterParamComponent`, `SmartFilterParamsComponent`,
961
+ `SmartFilterExpressionItemComponent`, `SmartFilterExpressionItemsComponent` —
962
+ internals of `SmartFilterEditorContentComponent`, which stays public.
963
+ - `HoverMenuComponent`, `ClickMenuComponent` — rendered by the ui-action toolbar.
964
+ - `UiActionConfirmDialogComponent`, `UiActionInputDialogComponent` — opened by
965
+ their services, which stay public.
966
+ - `UploadWidgetComponent`, `PhotoCaptureWidgetComponent`,
967
+ `VoiceRecordWidgetComponent` — contents of the ui-action upload dialog.
968
+ - `SmartNgClientService` — deleted; it was an empty stub with no members.
969
+ - The generated `ApiModule`s — they had no usages and their `forRoot()` told you
970
+ to import `HttpClientModule`, which no longer applies.
971
+
972
+ Two internal string tokens are gone as well, replaced by real `InjectionToken`s
973
+ (`'confirmDialogService'`, `'textFieldDialogService'`, `'fileUploadDialogService'`).
974
+ No host provided them, so no action is needed.
975
+
976
+ #### Behaviour notes
977
+
978
+ - `<mat-slide-toggle>` in the form widget no longer carries a `value` binding,
979
+ `<mat-nested-tree-node>` no longer carries an interpolated `matTreeNodeToggle`,
980
+ and the ui-action menus no longer carry `[subActions]`. All four were dead
981
+ bindings that the NgModules' `CUSTOM_ELEMENTS_SCHEMA` had been hiding — they
982
+ wrote a DOM property nobody read. Rendering is unchanged.
983
+ - The dead `ExpandableGridComponent` is deleted (it was declared in
984
+ `SmartGridModule` but its selector was used nowhere).
985
+
986
+ ### Widget ↔ SmartComponent: the model routing is inverted (Phase 3.4)
987
+
988
+ A widget no longer waits to be collected by the screen component it lives in: it finds its
989
+ `SmartComponentApiClient` through DI and registers itself. `SmartComponent` carries the
990
+ channel as an inherited host directive, so **no host component has to be changed for the
991
+ routing itself** — everything below is the *legacy surface* that went with the old
992
+ collection path. The protocol is written up in `WIDGETS.md` next to this file; the decision
993
+ record behind it is ADR-0005 in the `platform-angular2` working copy.
994
+
995
+ #### What a host must delete (all of it mechanical, all reported by the compiler)
996
+
997
+ | Delete | Why |
998
+ |---|---|
999
+ | `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. |
1000
+ | `[parentSmartComponent]="this"` on `<smart-component-layout>` | The input is gone; the layout injects the client. `strictTemplates` reports leftovers as **NG8002**. |
1001
+ | `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. |
1002
+ | `this.widgets.delete(…)` / `this.widgets.set(…)` / `this.formWidgets…` | The maps are gone (they were only written by the legacy path and read by nothing). |
1003
+ | 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. |
1004
+ | `override getSmartXxxQL()` (e.g. the two `throw new Error` overrides in a `FilterPageService`) | The eight abstract getters are gone from `SmartComponentApiClient`. |
1005
+ | `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. |
1006
+ | `handleDataChangeSubscriptions()` | Gone. A form publishes its own value-change keys when it constructs. |
1007
+
1008
+ Removed from the public API in the same step, in case a host imported them directly:
1009
+ `SmartComponentLayoutUtility` and its `EmbeddedSlotInfo` interface (the whole file — its
1010
+ collectors walked the layout tree for widgets, which nothing does now), and the `[parent]`
1011
+ inputs of `<smart-map>` / `<smart-diagram>` (they were bound to `parentSmartComponent` and read
1012
+ by nothing; both widgets resolve the client themselves). `initActions()` also runs less often:
1013
+ only when the model or its action list changes, no longer after every layout render, grid view
1014
+ init and filter model change — a host override that relied on one of those extra calls has to
1015
+ key off `dataChanged` instead.
1016
+
1017
+ The [codemod](#migrating-a-host-the-codemod) deletes the first two rows and the last one, and
1018
+ lists the rest — `addForm()`, the 4th `addGrid()` argument, the getter overrides — with their
1019
+ line numbers, because each of those is a small decision rather than a substitution.
1020
+
1021
+ #### What stays
1022
+
1023
+ - `SmartService` — a view-less client. Its `@ViewChildren` never populated anyway; they are
1024
+ simply gone now.
1025
+ - `initActions()` — still the hook a host overrides to build its own action list. The base
1026
+ implementation only asks for a render; toolbars pull their actions themselves.
1027
+ - `setUpDefaultTree()` / `createTreeService()`, `addGrid()`, `submitForms()`,
1028
+ `getInvalidFields()`, `uiActionModels` (now a getter over the model's actions).
1029
+
1030
+ #### `uiActionModels` is read-only now — assigning to it no longer compiles
1031
+
1032
+ It became a getter over the `actionModels` computed, so a host that *replaced* the list in
1033
+ place gets `TS2540: Cannot assign to 'uiActionModels' because it is a read-only property`.
1034
+ Measured in **p014 and p043**, `document-storage-editor.component.ts`:
1035
+
1036
+ ```ts
1037
+ // before — no longer compiles
1038
+ this.uiActionModels = this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL');
1039
+
1040
+ // after — filter into a field of your own and bind it
1041
+ protected readonly visibleActions = signal<UiActionModel[]>([]);
1042
+ // …
1043
+ this.visibleActions.set(this.uiActionModels.filter((a) => a.uiAction.code !== 'CANCEL'));
1044
+ ```
1045
+
1046
+ ```html
1047
+ <smart-ui-action-toolbar [uiActionModels]="visibleActions()"></smart-ui-action-toolbar>
1048
+ ```
1049
+
1050
+ A plain field works just as well; what matters is that the **array reference changes**, which
1051
+ is what tells the toolbar to re-render. See also the entry-freeze note in the Phase 3.7
1052
+ section.
1053
+
1054
+ #### Behavioural deltas, accepted
1055
+
1056
+ - **Wider reach.** A widget in a host's *own* sub-component now participates, where a view
1057
+ query could not see it (measured on p014/p043: 5 toolbars and 3–4 forms per host). A form
1058
+ takes part only if it displays the client's component model, which is what the forms the
1059
+ queries used to find have in common; a form that renders something else of its own (a
1060
+ search box, a field editor) does not.
1061
+ - Two widgets sharing one identifier both reload; the old map kept only the last.
1062
+ - `getInvalidFields()` collects in subscription order rather than view order — visible only
1063
+ in the order of the names in the validation dialog.
1064
+ - A toolbar entry is tracked by a synthesized key (action code + ordinal among entries with
1065
+ the same code), so a button's DOM node — with its focus and ripple — follows its action
1066
+ across reordering. Rendering is otherwise identical.
1067
+
1068
+ #### `[smartComponentDetached]`
1069
+
1070
+ The one brake on the automatic registration. Put it on an element whose subtree deliberately
1071
+ renders something other than the client's model; widgets below it resolve an empty slot and
1072
+ neither reload nor take part in submit/validation.
1073
+
1074
+ ```html
1075
+ <div smartComponentDetached>
1076
+ <smart-grid [smartGrid]="ownGrid" [uuid]="ownUuid"></smart-grid>
1077
+ </div>
1078
+ ```
1079
+
1080
+ #### `SmartSubject` is deleted
1081
+
1082
+ It was a no-op wrapper over `Subject`: `subscription.add(this.unsubscribe$.subscribe())`
1083
+ registers the teardown in the wrong direction, so after `destroy$.next()`, `.complete()` or
1084
+ `.unsubscribe()` subscribers still received values. Replace `new SmartSubject(destroy$)`
1085
+ with `new Subject()`; where a subscription really must die with a component, use
1086
+ `takeUntil(this._destroy$)` (which is what actually stopped delivery all along) or
1087
+ `takeUntilDestroyed()`. No host used it.
1088
+
1089
+ ### Toolbars resolve their own actions and own the execution context (Phase 3.5)
1090
+
1091
+ One rule decides which actions a toolbar shows, and the toolbar — not each list entry —
1092
+ says who performs them. `WIDGETS.md`, *Toolbars*, is the short version; the decision record
1093
+ behind it is ADR-0006 in the `platform-angular2` working copy.
1094
+
1095
+ > A toolbar renders the actions **addressed to its `id`** (`uiAction.toolbar == id`). The
1096
+ > list comes from an explicit `[uiActionModels]` binding if there is one, otherwise from the
1097
+ > screen component above it in the DOM. **Without an `id` it never pulls** — "unaddressed" is
1098
+ > not an address, so a toolbar with neither an id nor a binding shows nothing.
1099
+
1100
+ `[uiActionModels]="uiActionModels"` therefore keeps meaning "put my page's unaddressed
1101
+ actions here" and needs no change.
1102
+
1103
+ #### `UseUiAction` is deleted; `UseUiAction2` is now `UiActionExecutor`
1104
+
1105
+ There is one executor interface. If a host class implements `UseUiAction` — a `submit` /
1106
+ `reSubscribeToChange` subject handshake — replace the two subjects with the two methods:
1107
+
1108
+ ```ts
1109
+ // before
1110
+ export class DelegateInboxDialogService implements UseUiAction {
1111
+ submit: Subject<void> = new Subject();
1112
+ reSubscribeToChange: Subject<void> = new Subject();
1113
+
1114
+ }
1115
+
1116
+ // after
1117
+ export class DelegateInboxDialogService implements UiActionExecutor {
1118
+ submitForm(validate: boolean): void {
1119
+ this.submit.next(); // keep the subject if a component subscribes to it
1120
+ }
1121
+ getInvalidFields(): SmartFormInvalidFields {
1122
+ return { invalidFieldKeys: [], invalidFieldNames: [] };
1123
+ }
1124
+
1125
+ }
1126
+ ```
1127
+
1128
+ `Subject.next()` is synchronous, so a component that subscribed to `submit` has already run
1129
+ by the time `submitForm()` returns — which is exactly what the old handshake tried to
1130
+ express. Keep `reSubscribeToChange` only if something subscribes to it; the library no longer
1131
+ fires it.
1132
+
1133
+ This also **fixes a silent hang**: the old branch `await`ed `submit.toPromise()`, which only
1134
+ settles when somebody `complete()`s the subject. Three of the five implementors never did, so
1135
+ a `submit`/`model` action on those services waited forever.
1136
+
1137
+ #### `UiActionService.execute()` lost its first argument
1138
+
1139
+ It was always `uiActionModel.uiAction`:
1140
+
1141
+ ```ts
1142
+ - this.uiActionService.execute(uiActionModel.uiAction, uiActionModel);
1143
+ + this.uiActionService.execute(uiActionModel);
1144
+ ```
1145
+
1146
+ #### The execution context moved from the entry to the toolbar
1147
+
1148
+ | what | bind on the toolbar | entry field (still honoured) |
1149
+ |---|---|---|
1150
+ | who performs the action | `[executor]` | `serviceToUse` — **deprecated** |
1151
+ | which widget it runs against | `[widgetId]` | `widgetId` — **deprecated**, wins if set |
1152
+ | which row inside it | `[nodeId]` | `nodeId` — **deprecated**, wins if set |
1153
+ | extra params for every action | `[actionParams]` | (clone the `UiAction` — no longer necessary) |
1154
+
1155
+ `[executor]` defaults to the screen component the toolbar sits under, which is what almost
1156
+ every entry named. Bind it only when the actions belong to another API — a tree service, a
1157
+ filter editor, a dialog service of your own:
1158
+
1159
+ ```html
1160
+ <smart-ui-action-toolbar [uiActionModels]="actions" [executor]="myService"></smart-ui-action-toolbar>
1161
+ ```
1162
+
1163
+ `[actionParams]` replaces cloning a `UiAction` per row just to inject a model:
1164
+
1165
+ ```html
1166
+ <smart-ui-action-toolbar
1167
+ [uiActionModels]="rowActions"
1168
+ [widgetId]="gridId"
1169
+ [nodeId]="row.id"
1170
+ [actionParams]="{ model: row }"
1171
+ ></smart-ui-action-toolbar>
1172
+ ```
1173
+
1174
+ #### Removed from the public API
1175
+
1176
+ | Removed | Replacement |
1177
+ |---|---|
1178
+ | `UseUiAction` | `UiActionExecutor` (see above) |
1179
+ | the third `SmartTable` constructor parameter and `getServiceToUse()` | nothing — the executor no longer travels through the table (0 host call sites) |
1180
+ | `SmartformComponent.getToolbars()` | nothing collects toolbars any more; each one resolves its own actions |
1181
+ | `ISmartFilterEditorService.get/setComplexToolbars()` and the `submit` / `reSubscribeToChange` members | as above |
1182
+ | `FileEditorToolbarComponent` | a plain `<smart-ui-action-toolbar>` with `[actionParams]` |
1183
+ | `SmartGridComponent.setupToolbar()` / `.toolbar` / `.headerToolbar` | the id is a binding; there is nothing to reach into |
1184
+
1185
+ #### Behavioural deltas, accepted
1186
+
1187
+ - A widget renders a toolbar exactly when its model carries a `toolbarId`, with no type list.
1188
+ A `toolbarId` on a type whose template has no place for a toolbar (`RECORDING_UPLOADER`)
1189
+ now logs a warning instead of being silently dropped.
1190
+ - Only the actions a toolbar actually shows are scheduled. Since 3.4 a toolbar armed every
1191
+ scheduled action of the view, so one action could fire from several toolbars at once.
1192
+ - A grid row's actions are no longer cloned per row; the row model travels as
1193
+ `[actionParams]`. The multi-file editor's per-file toolbars appear when their own list
1194
+ resolves rather than when a hidden sibling's did, and the tree's `doAction` clones instead
1195
+ of mutating the caller's `UiAction`.
1196
+
1197
+ ### The form widget's rendering decisions (Phase 3.6)
1198
+
1199
+ Nothing here needs a code change in a host — there is no API to adapt to. What
1200
+ changes is what the widget puts on the page, so read this before you compare
1201
+ screenshots.
1202
+
1203
+ #### Every widget carried a spare empty wrapper; it is gone
1204
+
1205
+ The `COMPONENT` branch's wrapper had no type guard, so
1206
+ `<div class="widgetContainer"><div class="widgetContent"><ng-template
1207
+ #customComponent>` rendered after **every** widget of every type. Measured on the
1208
+ playground: 14 widgets, 28 `.widgetContainer` — one spare, empty, per widget.
1209
+
1210
+ The library's own CSS puts no box properties on `.widgetContainer`, so most pages
1211
+ will look identical. **Yours may not**, if you style that class with anything that
1212
+ occupies space. Both in-house hosts do, on one page:
1213
+
1214
+ ```css
1215
+ /* p014 + p043, dossier-publication.component.css */
1216
+ :host ::ng-deep .widgetContainer { margin-bottom: 0.5rem !important; }
1217
+ ```
1218
+
1219
+ There, every widget loses half a rem of trailing space. Grep your stylesheets for
1220
+ `.widgetContainer` and check the pages the matching selectors reach. Rules that
1221
+ need content still match what they always matched — an empty wrapper never
1222
+ satisfied `.label.widgetContainer` or `.widgetContainer:has(…)`.
1223
+
1224
+ #### `MATRIX` and `YOUTUBE_PLAYER` now obey `isVisible` and get their `cssClass`
1225
+
1226
+ Those two branches sat outside the `@if (isVisible)` guard and outside the
1227
+ `<div [ngClass]="cssClass" class="container">` every other type lives in, so
1228
+ `isVisible: false` did not hide them and `cssClass` never reached them. Both are
1229
+ inside now. If a backend view relies on a matrix rendering while marked invisible,
1230
+ it will stop rendering.
1231
+
1232
+ #### A container keeps its invisible children
1233
+
1234
+ A `CONTAINER` widget's `ngOnInit` used to overwrite `widgetInstance.valueList`
1235
+ with the visible subset, destroying the invisible children in the model the
1236
+ backend sent. The visible subset is derived per check now, so the model stays
1237
+ whole and a later `applyConstraints()` that flips a child's `isVisible` brings it
1238
+ back — which it could not before. The DOM is unchanged: the same children render.
1239
+
1240
+ Side effect worth knowing: the model walks that recurse into a container's
1241
+ `valueList` (`applyComponentConstraints`, `copyValueFromFormToWidgetBeans`,
1242
+ `translateWidgets`) now see the invisible children too. Their form controls always
1243
+ existed — `createFormControls` runs over the whole model before any widget renders
1244
+ — so this only makes the widget beans agree with the controls.
1245
+
1246
+ #### A YouTube shorts link resolves
1247
+
1248
+ `parseYoutubeUrl` threw away its own `replace('/shorts/', '/watch?v=')`, so a
1249
+ shorts link produced no player. It plays now.
1250
+
1251
+ #### Removed from the public API
1252
+
1253
+ | Removed | Replacement |
1254
+ |---|---|
1255
+ | `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.1 splits it into per-type components |
1256
+ | `SmartWidgetSettings` (its only member was `static useUtc`) | nothing — **delete the assignment** |
1257
+
1258
+ 3.6 moved the `useUtc` static into `SmartNgClientConfig.useUtcDates`; **3.8 then removed the
1259
+ field altogether**, together with the moment adapter it fed. If you set
1260
+ `SmartWidgetSettings.useUtc = true` before bootstrap, delete that line and read
1261
+ [Dates §4](#4-smartngclientconfiguseutcdates-is-removed) — the timezone contract is fixed now,
1262
+ and the flag defaulted to `false` everywhere anyway.
1263
+
1264
+ #### What deliberately did not change
1265
+
1266
+ A required field is marked in both label positions, by two different mechanisms:
1267
+ a shown label (`showLabel: true`) is the widget's own `<h4>` and carries a literal
1268
+ ` *`; a `mat-label` sits inside a `mat-form-field`, which renders
1269
+ `.mat-mdc-form-field-required-marker` from the control's own validators. That is
1270
+ why `getWidgetLabel` ties its asterisk to `showLabel` — dropping the condition
1271
+ would mark the floating label twice.
1272
+
1273
+ ### Change detection: frozen action entries, no zone.js (Phase 3.7)
1274
+
1275
+ 7.0 runs without zone.js. Every component of the library is `OnPush` — nothing is checked
1276
+ just because something, somewhere, ticked — and the library says when its own state changed.
1277
+ Two consequences reach a host.
1278
+
1279
+ #### A `UiActionModel` is frozen: build an entry, never edit one
1280
+
1281
+ Every field is `readonly`, and `[uiActionModels]` takes `readonly UiActionModel[]`.
1282
+
1283
+ ```ts
1284
+ // before — compiles on 6.x, and on 7.0 changes nothing on screen
1285
+ entry.cssClass = 'active-nav-action';
1286
+
1287
+ // after
1288
+ this.actions = this.actions.map((a) =>
1289
+ a.uiAction.code === code ? { ...a, cssClass: 'active-nav-action' } : a
1290
+ );
1291
+ ```
1292
+
1293
+ Measured across the 12 hosts in scope (2026-07-28): **72 sites in 7 hosts**, all of them
1294
+ `cssClass` — p009-angular alone has 38, p014 and p043 12 each. The codemod prints every one
1295
+ of them with its line number; it cannot rewrite them, because only you know which array the
1296
+ entry belongs to.
1297
+
1298
+ The compile error and the fix are the same thing. Writing into an entry that a toolbar is
1299
+ already rendering never reached the screen under zone.js either — it worked only because
1300
+ some other event happened to tick the application. Rebuilding the array is what actually
1301
+ re-renders.
1302
+
1303
+ **The gap this does not close.** `this.actions[0] = { ...this.actions[0], cssClass: 'x' }`
1304
+ compiles: `actions` is your field, and the freeze only covers the entries. It keeps the same
1305
+ array reference, so the toolbar does not re-render. Reassign the array — or, if you hold it
1306
+ in a `signal`, `update()` it.
1307
+
1308
+ The descriptor a toolbar resolves is no longer written back into the entry either. Nothing
1309
+ outside the toolbar read it; if you did, resolve it yourself through
1310
+ `UiActionDescriptorService.getActionDescriptor(uiAction)` — which is
1311
+ [synchronous in 7.0](#getactiondescriptor-is-synchronous-phase-39).
1312
+
1313
+ #### Client-side translation is gone
1314
+
1315
+ `SmartTranslateService`, `viewContext.translateService` and the `translateServiceChanged`
1316
+ subject are removed, together with `originalLabel` / `originalPlaceholder` on the widget
1317
+ interfaces. No measured host assigned any of them — the three references in p014/p043 were
1318
+ unused imports — and the backend localizes server-side.
1319
+
1320
+ What the frontend authors itself is now configuration:
1321
+
1322
+ ```ts
1323
+ provideSmartNgClient({
1324
+ errorDialog: { title: 'Hiba', message: 'Váratlan hiba történt.', buttonLabel: 'Rendben' },
1325
+ });
1326
+ ```
1327
+
1328
+ `SmartViewContextService.getSmartViewContextApiErrorByCode()` is no longer `async`.
1329
+
1330
+ #### If your host still bootstraps with zone.js
1331
+
1332
+ Nothing forces you to drop it — the library works either way, because it no longer depends
1333
+ on an application-wide tick. The playground runs `provideZonelessChangeDetection()` with no
1334
+ `zone.js` polyfill, and that is the configuration 7.0 is tested in.
1335
+
1336
+ If you do go zoneless, the same rule applies to your own components: state written from a
1337
+ subscription, a promise or a timer needs a signal or a `markForCheck()`; state written from a
1338
+ template event or an input does not.
1339
+
1340
+ ### Dates: moment → date-fns, and the timezone contract (Phase 3.8)
1341
+
1342
+ **This section needs action at install time.** The reasoning is ADR-0008 in the
1343
+ `platform-angular2` working copy; the contract itself is stated in §4 below.
1344
+
1345
+ #### 1. Swap the peer dependency
1346
+
1347
+ `@angular/material-moment-adapter` is out of the library's `peerDependencies`;
1348
+ `@angular/material-date-fns-adapter` and `date-fns` are in. `date-fns` is listed explicitly
1349
+ rather than left transitive, because the library imports `date-fns/locale` itself.
1350
+
1351
+ ```bash
1352
+ npm uninstall @angular/material-moment-adapter moment
1353
+ npm install @angular/material-date-fns-adapter@^22.0.0 date-fns@^4
1354
+ ```
1355
+
1356
+ If your own code imports moment for something unrelated, keep it — nothing here forces its
1357
+ removal, only the adapter's.
1358
+
1359
+ **But check *how* it imports moment.** `import * as moment from 'moment'` stops being callable
1360
+ under `moduleResolution: "bundler"`, which the Angular 20 hop turns on: moment is an
1361
+ `export =` module, and a namespace import of one is not a callable value. The symptom is
1362
+ **`TS2349: This expression is not callable`** on every `moment(...)` call, and it arrives at the
1363
+ Angular 20 hop rather than at this step. Two ways out:
1364
+
1365
+ - `esModuleInterop: true` plus `import moment from 'moment'` — correct, but it changes how every
1366
+ CommonJS import in the app resolves, which is a wide blast radius for one file.
1367
+ - Replace the calls. Worth checking first how much moment is actually doing: in p014 the three
1368
+ surviving calls were `moment(new Date()).toDate()` (i.e. `new Date()`) and two moments compared
1369
+ with `<=` (`Date` compares through `valueOf()` identically). All three became plain `Date`s, and
1370
+ **nothing under `src/` imported moment any more** — so the `npm uninstall … moment` above was
1371
+ right for that host after all, just for a reason nobody predicted.
1372
+
1373
+ #### 2. Leave your `MAT_DATE_LOCALE` provider alone
1374
+
1375
+ If you have this line — p014 and p043 both do — **keep it as it is**:
1376
+
1377
+ ```ts
1378
+ { provide: MAT_DATE_LOCALE, useValue: 'hu-HU' },
1379
+ ```
1380
+
1381
+ The strict reading of the date-fns adapter would require a `Locale` **object** here, and a bare
1382
+ string would throw inside date-fns the first time a user opened a date field. The library's
1383
+ `SmartDateFnsAdapter` accepts both: it resolves `'hu'` / `'hu-HU'` / `'en'` / `'en-US'`
1384
+ (case-insensitively) to a date-fns locale, passes a real `Locale` object through untouched, and
1385
+ falls back to Hungarian with a `console.warn` for anything else.
1386
+
1387
+ So: **hosts on Hungarian or English need no change.** For any other language, provide a real
1388
+ date-fns locale:
1389
+
1390
+ ```ts
1391
+ import { de } from 'date-fns/locale';
1392
+
1393
+ { provide: MAT_DATE_LOCALE, useValue: de },
1394
+ ```
1395
+
1396
+ `SmartNgClientConfig.dateLocale` is unchanged — still a `string`, still defaulting to `'hu-HU'`.
1397
+
1398
+ #### 3. If you override `MAT_DATE_FORMATS`, your format strings will throw — and you probably want to delete the override
1399
+
1400
+ Measured across 12 hosts on 2026-07-28: three of them (**p014**, **p043**, **p009-angular**)
1401
+ provide their own formats next to the locale, and two more (`app-vlab`, `app-tournament`) have the
1402
+ same line commented out:
1403
+
1404
+ ```ts
1405
+ { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
1406
+ ```
1407
+
1408
+ with
1409
+
1410
+ ```ts
1411
+ export const MY_FORMATS = {
1412
+ parse: { dateInput: 'YYYY.MM.DD' },
1413
+ display: { dateInput: 'YYYY.MM.DD', monthYearLabel: 'YYYY',
1414
+ dateA11yLabel: 'LL', monthYearA11yLabel: 'YYYY' },
1415
+ };
1416
+ ```
1417
+
1418
+ Those are **moment tokens**. date-fns treats `YYYY`, `DD`, `YY` and `D` as *protected* and throws a
1419
+ `RangeError` on them, and its `LL` means something else entirely (stand-alone month, not a
1420
+ localized long date). So this override **crashes at runtime, on the first screen with a date
1421
+ field** — the same failure shape as the string locale above, and just as invisible to the compiler.
1422
+
1423
+ **The fix is almost certainly deletion.** That `MY_FORMATS` exists to render `YYYY.MM.DD`, which is
1424
+ exactly what the library's own `SMART_DATE_FORMATS` now produces (`2026.07.28.`). Drop the
1425
+ provider and the constant and you get the format you wanted, plus the more forgiving parsing.
1426
+
1427
+ If you genuinely need a different format, translate the tokens rather than copying them:
1428
+
1429
+ | moment | date-fns |
1430
+ |---|---|
1431
+ | `YYYY` | `yyyy` |
1432
+ | `DD` | `dd` |
1433
+ | `LL` | `PP` (or spell it out: `yyyy. MMMM d.`) |
1434
+ | `MMM YYYY` | `yyyy. LLL` |
1435
+
1436
+ Note that the library's `parse.dateInput` is an **array** — `DateFnsAdapter._parse` tries each entry
1437
+ in turn (and ISO-8601 first), so you can list several accepted spellings instead of one.
1438
+
1439
+ #### 4. `SmartNgClientConfig.useUtcDates` is removed
1440
+
1441
+ Delete it if you set it. No known host does. It existed for a client-configurable timezone that
1442
+ was abandoned as a requirement; the contract is now fixed and explicit:
1443
+
1444
+ > **The browser's zone on screen. Zulu (`…Z`) on the wire. The server converts.**
1445
+
1446
+ This is unchanged behaviour in practice — `useUtcDates` defaulted to `false`, and the moment
1447
+ adapter with `useUtc: false` behaves exactly like a plain `Date`.
1448
+
1449
+ #### 5. What a date widget's value now *is*
1450
+
1451
+ Form control values from the date pickers are plain **`Date`** objects, where they were
1452
+ `Moment` objects. The values reach your backend through a `UiActionRequest` param, so the wire
1453
+ format is `JSON.stringify` of that value — and `Date.toJSON()` emits the same ISO-8601 UTC string
1454
+ `Moment.toJSON()` did. **Nothing changes on the wire.** Only host code that read a value out of a
1455
+ form control and called a moment method on it (`.format()`, `.add()`, `.startOf()`) needs
1456
+ rewriting; the platform APIs never exposed a `Moment` in a signature.
1457
+
1458
+ #### 6. The one visible difference
1459
+
1460
+ Date inputs render **`2026.07.28.`** where 6.x rendered `2026.7.28.` — the documented `YYYY.MM.DD`
1461
+ convention. The old rendering was not a decision: `MAT_MOMENT_DATE_FORMATS` used moment's `'l'`,
1462
+ which strips the zero padding off the Hungarian `L`. Typed input is **more** forgiving than 6.x,
1463
+ not less: `2026.07.28.`, `2026.07.28`, `2026.7.28.`, `2026.7.28` and ISO-8601 are all accepted.
1464
+
1465
+ The month picker is unchanged (`07/2026`), and grid/table date columns were never affected —
1466
+ `SmartDatePipe` and friends extend Angular's own `DatePipe` and never used moment.
1467
+
1468
+ **Verified on a running host** (p014, 2026-07-29, live backend, `Europe/Budapest`). Picking
1469
+ 2026-07-15 in a `Kezdődátum` field gave, end to end:
1470
+
1471
+ | | |
1472
+ |---|---|
1473
+ | input | `2026.07.15.` |
1474
+ | widget value | `instanceof Date` — not a moment, not a luxon `DateTime` |
1475
+ | `JSON.stringify` of that value | `"2026-07-14T22:00:00.000Z"` |
1476
+ | `getHours()` | `0` |
1477
+
1478
+ That third row is the whole contract in one line: the value's `toJSON()` **is** the wire format
1479
+ (see §4), and it is zulu, while the screen and `getHours()` stay local — 15 July 00:00 CEST is
1480
+ 14 July 22:00 UTC. A grid column on the same host rendered `2026.04.20 0:03`, confirming the
1481
+ untouched pipe path.
1482
+
1483
+ ### HTTP: the library no longer forces the XHR backend (Phase 3.8)
1484
+
1485
+ `provideSmartNgClient()` used to pass `withXhr()` to its internal `provideHttpClient()` call,
1486
+ overriding the Angular 22 default on your behalf. It no longer does — the application runs on the
1487
+ framework default **fetch** backend.
1488
+
1489
+ For almost every host this is invisible. The one behavioural difference that matters: **the fetch
1490
+ backend does not support upload progress.** A request with `reportProgress: true` and a body
1491
+ throws `NG2824` instead of emitting `HttpUploadProgress` events.
1492
+
1493
+ Measured 2026-07-28 across 12 hosts: **`reportProgress: true` and `observe: 'events'` appear
1494
+ nowhere** outside the generated API services' unused parameters. So this is a theoretical risk for
1495
+ the current host set, not a live one. If *your* code does ask for upload progress, pass the
1496
+ feature back in:
1497
+
1498
+ ```ts
1499
+ import { withXhr } from '@angular/common/http';
1500
+
1501
+ provideSmartNgClient({ httpFeatures: [withXhr()] })
1502
+ ```
1503
+
1504
+ The upside is that the "**do not call `provideHttpClient()` twice**" trap this guide warned about
1505
+ is gone rather than merely documented: a second call now re-provides the same backend the library
1506
+ already uses. The library's interceptors were never at risk from it — they are `multi` providers.
1507
+
1508
+ ### `getActionDescriptor()` is synchronous (Phase 3.9)
1509
+
1510
+ `UiActionDescriptorService.getActionDescriptor(uiAction)` returns a `UiActionDescriptor`.
1511
+ It used to return a `Promise<UiActionDescriptor>`, and it was `async` for exactly one
1512
+ reason: the client-side translation layer that [Phase 3.7](#client-side-translation-is-gone)
1513
+ retired. Nothing it reads has been asynchronous since, so every caller was awaiting a value
1514
+ that was already there.
1515
+
1516
+ **If you only `await` the call, you are already fine.** `await` on a non-promise is legal
1517
+ TypeScript, so this compiles and behaves identically before and after. The codemod drops the
1518
+ `await` anyway, so the code stops claiming an asynchrony that is not there:
1519
+
1520
+ ```ts
1521
+ const descriptor = await this.uiActionDescriptor.getActionDescriptor(uiAction); // 6.x
1522
+ const descriptor = this.uiActionDescriptor.getActionDescriptor(uiAction); // 7.0
1523
+ ```
1524
+
1525
+ Two shapes genuinely break. Measured 2026-07-28 across the 12 hosts in scope: **5 hosts,
1526
+ 7 sites** — and none of them in a host's own `getActionDescriptor**s**()` map helper, which
1527
+ is an unrelated API and is left alone.
1528
+
1529
+ **1. A subclass that overrides it** — `TS2416`, because `Promise<UiActionDescriptor>` is not
1530
+ assignable to `UiActionDescriptor`. Found in three hosts, as a byte-identical
1531
+ `MdmUiActionDescriptorService`. **The codemod rewrites this one.**
1532
+
1533
+ ```ts
1534
+ // 6.x
1535
+ override async getActionDescriptor(uiAction: UiAction): Promise<UiActionDescriptor> {
1536
+ let d: UiActionDescriptor = await super.getActionDescriptor(basicUiAction);
1537
+
1538
+ }
1539
+
1540
+ // 7.0
1541
+ override getActionDescriptor(uiAction: UiAction): UiActionDescriptor {
1542
+ let d: UiActionDescriptor = super.getActionDescriptor(basicUiAction);
1543
+
1544
+ }
1545
+ ```
1546
+
1547
+ This is also the service you hand to a toolbar through `[uiActionDescriptorService]`, so a
1548
+ host with a custom one hits this at compile time rather than on screen.
1549
+
1550
+ **2. `.then(…)` on the result** — `TS2339`, because a descriptor has no `.then`. Found in
1551
+ two hosts, four sites. **The codemod reports this one and does not rewrite it**: turning a
1552
+ callback into straight-line code means moving its body, which is a judgment call. The fix is
1553
+ mechanical anyway — assign, then inline:
1554
+
1555
+ ```ts
1556
+ // 6.x
1557
+ constructForm() {
1558
+ this.actionDescriptorService.getActionDescriptor(this.uiAction).then((d) => {
1559
+ this.buttonTitle = d.title;
1560
+ this.buttonColor = d.color;
1561
+ });
1562
+
1563
+ }
1564
+
1565
+ // 7.0
1566
+ constructForm() {
1567
+ const d = this.actionDescriptorService.getActionDescriptor(this.uiAction);
1568
+ this.buttonTitle = d.title;
1569
+ this.buttonColor = d.color;
1570
+
1571
+ }
1572
+ ```
1573
+
1574
+ If the callback was the reason a field lived in a `signal` — because the value arrived after
1575
+ the first render — it no longer needs to. The library made that same simplification in its
1576
+ own three action dialogs and its snack bar.
1577
+
1578
+ **What went synchronous with it, inside the library:** the toolbar's descriptor resolution,
1579
+ `SmartGridComponent`'s header construction, and `SmartTreeGenericService.cacheActionDesciptors()`
1580
+ and **`syncTree()`**. `syncTree()` now returns `void`, which is what
1581
+ `SmartTreeServiceInterface` always declared; a host that calls it and discards the result —
1582
+ the only shape measured — needs no change, and a host that `await`s it still compiles.
1583
+
1584
+ #### The same cleanup, elsewhere: `SmartdialogService.closeDialog()` returns `void`
1585
+
1586
+ A sweep for the same shape found eight more library functions declared `async` with nothing
1587
+ asynchronous in them. Only one of them is on a surface a host touches:
1588
+ **`SmartdialogService.closeDialog(stopPropagate?)` returns `void`** instead of
1589
+ `Promise<void>`.
1590
+
1591
+ **Nothing to do.** Both measured hosts that extend `SmartdialogService` override this method
1592
+ as `override async closeDialog(): Promise<void>`, and that **still compiles**: TypeScript
1593
+ accepts any return type where the base declares `void`. `await this.closeDialog()` keeps
1594
+ compiling too. This is the difference from `getActionDescriptor` above — that one returns a
1595
+ *value*, which is why the same override shape fails there with `TS2416` and not here.
1596
+
1597
+ You may of course drop the `async` from your own override; nothing requires it.
1598
+
1599
+ ### Upload actions now wait for the server, and report their failures (Phase 3.9)
1600
+
1601
+ **This one changes behaviour, on purpose.** `SmartViewContextService.performUploadAction()`
1602
+ and `performUploadMultipleAction()` used to start the upload and resolve immediately,
1603
+ without waiting for it and without passing on its failure. Every other action in the family
1604
+ (`performAction`, `performWidgetAction`, `performWidgetMainAction`, `dataChanged`) always
1605
+ waited; the two upload ones were an omission.
1606
+
1607
+ Two things follow, and both are visible in a running app:
1608
+
1609
+ 1. **An upload action now blocks until the server has answered.** Before, the action was
1610
+ "done" the moment the request went out — so `UiActionService` showed its success snackbar
1611
+ over an upload that was still in flight, and anything the action was sequenced before ran
1612
+ against a view the server had not updated yet. Now the snackbar appears when the upload
1613
+ really has succeeded. On a large file that is a longer wait than 6.x showed you.
1614
+
1615
+ 2. **A failed upload now reaches your error handling.** The rejection used to be dropped —
1616
+ it became an unhandled promise rejection, `UiActionService`'s `catch` never ran, and the
1617
+ user was told the upload had worked. It now propagates, which means
1618
+ `setActionErrorHandler()` (or the default error dialog) will fire on upload failures it
1619
+ never fired on before. **If your host reports "new" upload errors after upgrading, they
1620
+ are not new** — they were happening silently.
1621
+
1622
+ Multi-file uploads also go out **one batch at a time** now. The batches were already being
1623
+ computed from the descriptor's `maxSize` and `maxBatchSize`, but they were then all sent at
1624
+ once, which put the whole set on the server simultaneously and defeated the split. If your
1625
+ backend measured upload concurrency, this is why it drops.
1626
+
1627
+ Nothing to change in host code.
1628
+
1629
+ ### New in 7.0: a grid row can render a backend layout (#29717)
1630
+
1631
+ A card-mode grid row that carries `layoutDescriptor.componentLayouts['GRID_ROW_LAYOUT']`
1632
+ renders that layout instead of the card component the host registered under
1633
+ `'<GRID_ID>Card'`. Both branches stay: a row without the descriptor behaves exactly as
1634
+ before, and this needs no host change.
1635
+
1636
+ Two consequences worth knowing if you style or extend it:
1637
+
1638
+ - The rendered layout's root element gets the `gridCardLayout` class, applied by the card to
1639
+ its own child rather than pushed into the `style.classesToAdd` of the object the backend
1640
+ sent. Nothing mutates the server model.
1641
+ - **The row's actions reach the layout's toolbars by a pull, not a walk.** The card provides
1642
+ a `SmartActionHost` for its row subtree and writes the row's action list into it; a toolbar
1643
+ inside the layout resolves that host before falling back to the page client. If you build
1644
+ a component that owns such a subtree, do the same — `WIDGETS.md`, *Toolbars*.
1645
+
1646
+ This shipped in 7.0.0 rather than as a 6.0.3x patch because it is built on the inverted
1647
+ widget contract that 7.0 introduces.
1648
+
1649
+ ### Table detail rows: rendered on demand, `example-*` classes renamed (7.0.10)
1650
+
1651
+ The Material table used to define its expandable detail row the way the Angular Material
1652
+ example does: a second `matRowDef` rendered under **every** data row, kept at zero height
1653
+ with `tr.example-detail-row { height: 0 }` and an `@angular/animations` trigger. Any host
1654
+ rule that gave table cells vertical padding, or rows a height, made those empty rows visible
1655
+ the p043 report and every table carried twice its rows in the DOM.
1656
+
1657
+ Now the detail row exists only in an expandable table (`SmartTable.expandable`), and only
1658
+ under the row that is expanded. There is nothing a host stylesheet can inflate. Expanding
1659
+ and collapsing animate through the native `animate.enter` / `animate.leave` of Angular 20.2+
1660
+ (a `grid-template-rows: 0fr 1fr` keyframe, as `mat-expansion-panel` does it), so the table
1661
+ no longer uses `@angular/animations` at all.
1662
+
1663
+ The classes lost their `example-` prefix, taken over from the Material sample:
1664
+
1665
+ | 6.x | 7.0.10 |
1666
+ |---|---|
1667
+ | `example-element-row` | `smart-table-row` |
1668
+ | `example-expanded-row` | `smart-table-row-expanded` |
1669
+ | `example-detail-row` | `smart-table-detail-row` |
1670
+ | `example-element-detail` | `smart-table-detail` |
1671
+
1672
+ `.example-element-diagram`, `-symbol`, `-description` and `-description-attribution` — the
1673
+ sample's own styles, referenced by nothing are gone.
1674
+
1675
+ What to do in a host:
1676
+
1677
+ - Rename the classes in any stylesheet that targets them. The usual suspects are a hover
1678
+ rule on `tr.example-element-row:not(.example-expanded-row)` and a
1679
+ `:host ::ng-deep tr.example-detail-row { display: table-row !important }` in a page that
1680
+ expands rows; the latter can simply be deleted, the row is rendered when it is needed.
1681
+ - A data row now carries its own bottom border. Before, the always-present detail row drew
1682
+ it, and the data row's border was turned off to avoid a double line. Nothing changes on
1683
+ screen unless you drew your own row separators to compensate.
1684
+
1685
+ ### Writing your own widget
1686
+
1687
+ `WIDGETS.md`, next to this file, is the protocol: how a widget finds its screen component
1688
+ through DI and registers itself, when to opt out with `[smartComponentDetached]`, what has to
1689
+ be a signal now that there is no zone, how a toolbar resolves its actions, and the three
1690
+ traps that have actually bitten (imperatively-fed inputs cannot be signal inputs; `@for`
1691
+ tracking over backend objects; clear-then-apply styling).
1692
+
1693
+ In 6.x this was not possible at all `SmartComponentApiClient` collected its children with
1694
+ eight `@ViewChildren` over eight concrete widget classes.
1695
+
1696
+ ### Imperatively created components are given their inputs properly
1697
+
1698
+ `ComponentFactoryService` creates the components the library instantiates by hand grid
1699
+ cards, expanded rows, table cell components, the expandable section's content, the form's
1700
+ `COMPONENT` widget, the filter editor's field editors — and most of those classes come from a
1701
+ host. It used to write their inputs by assigning the field. Two things follow, and both are
1702
+ fixed:
1703
+
1704
+ - **A declared input is now written with `ref.setInput()`**, i.e. the same way a template
1705
+ binding writes it. So a **signal input** works (assignment used to overwrite the input
1706
+ function with the value, and the component's next `this.x()` threw `x is not a function`),
1707
+ an **aliased** input is addressable under either name, and **`ngOnChanges` runs**.
1708
+ - **Falsy values arrive.** The old guard was `if (value)`, so `false`, `0` and `''` never
1709
+ reached the component. Only `undefined` is skipped now, which keeps meaning "not passed".
1710
+
1711
+ If your component takes something from the library this way and the key is **not** a declared
1712
+ input, the field is still assigned nothing breaks but you get one `console.warn` per
1713
+ component type and key. Declaring it with `@Input()` (or as a signal input) is the fix.
1714
+
1715
+ **Check your falsy defaults.** If a host component relied on the old guard — expecting to
1716
+ keep its own default when the library passed `false` or `''` it now receives the value.
1717
+ This is the only part of 7.0 where a bugfix can change what you see on screen without any
1718
+ code of yours changing.
1719
+
1720
+ ### Bugfixes shipped with 7.0
1721
+
1722
+ - `SmartformwidgetComponent.ngAfterViewInit` no longer crashes with
1723
+ `Cannot read properties of undefined (reading 'valueChanges')` when the form
1724
+ contains a `MONTH_PICKER` widget (it wrongly subscribed to the non-existent
1725
+ `<key>-time` control; only `DATE_TIME_PICKER` has one).
1726
+ - A widget nested in a CONTAINER receives `blurSophisticatedValueChange`; the recursion never
1727
+ passed it down, so a BLUR-mode child would have thrown on blur.
1728
+ - `SmartformComponent` reaches widgets nested in a CONTAINER when it changes them in place
1729
+ (values, constraints, the touched state after a failed submit). The view query it used
1730
+ could not see them.
1731
+ - `UiActionDescriptorService` no longer shares one dialog object between actions. Any action
1732
+ whose descriptor had no `dialog` of its own was given the service's single placeholder — the
1733
+ same object every time — and had its title and button caption written into it, so resolving
1734
+ one action renamed the dialog every other one was holding. The service is
1735
+ `providedIn: 'root'`, so this outlasted the view.
1736
+
1737
+ What you saw on screen, measured on both versions:
1738
+
1739
+ | The action | 6.x, when the dialog opened | 7.0 |
1740
+ |---|---|---|
1741
+ | described by the backend, no dialog of its own | **another action's code** | its own code |
1742
+ | described by nothing at all | **another action's code**, or empty | empty |
1743
+ | registered client-side, no dialog of its own | its own code | unchanged |
1744
+
1745
+ The middle column is not a typo. An action the backend describes is resolved twice in the
1746
+ normal flow — once when the toolbar renders it, again when the dialog it opens resolves it
1747
+ for itself — and the first resolution wrote a dialog onto the action's own descriptor, so the
1748
+ second one skipped the step that fills in the title. What the dialog then showed was whatever
1749
+ code had last passed through the shared object. An **already open** dialog could change too,
1750
+ since its title is re-read on every change-detection pass.
1751
+
1752
+ An action nothing describes still gets an untitled placeholder dialog: its *button* carries
1753
+ the code, the dialog does not. That is unchanged and deliberate — such an action is a gap in
1754
+ the host's descriptor map, and this is the honest rendering of it.
1755
+
1756
+ A resolved descriptor is now the caller's own object throughout, which also means
1757
+ `getActionDescriptor()` no longer writes a placeholder dialog and a `SNACKBAR` feedbackType
1758
+ into the `descriptor` the backend put on the action itself.