@smartbit4all/ng-client 7.2.3 → 7.2.5

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/HTML-WIDGET.md CHANGED
@@ -38,6 +38,7 @@ A `DIV` widget változatlanul megmarad; a HTML widget annak a használatnak a re
38
38
  | `root` | dot-path a kontextus `data`-jához képest, amihez minden helyőrző relatív; **objektumot** nevez; üres = a `data` maga |
39
39
  | `fields` | a widget által olvasott abszolút pathok — a builder származtatja a helyőrzőkből, a kliens **nem** olvassa |
40
40
  | `css` | a widget stíluslapja, a html mellett utazik — lásd *Stíluslap* |
41
+ | `icons` | az **ikon-szótár**: `kulcs → ImageResource`, amit az ikon-slotok `data-sb4-icon-key`-jel címeznek — lásd *Ikon-slot* |
41
42
 
42
43
  A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
43
44
  (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
@@ -55,6 +56,20 @@ layoutBuilder(view).vForm(f -> f
55
56
  vagy template-kulcsból (`h.template(ctx, "CONTACT_ENTRY_LIST")`): a kulcs a szerveren oldódik fel,
56
57
  a dróton nem utazik.
57
58
 
59
+ A html sima szöveg, tehát a `Tag` builder helyett **text blockként** is írható — így a sablon
60
+ ugyanúgy néz ki, ahogy a kliens megkapja:
61
+
62
+ ```java
63
+ .html("""
64
+ <div class="card">
65
+ <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
66
+ <b>{{name}}</b>
67
+ </div>""")
68
+ ```
69
+
70
+ A `html(String, Object...)` `String.format`-tal illeszti be az argumentumokat (akciókód,
71
+ toolbar-id), ilyenkor a szövegben a `%` `%%`.
72
+
58
73
  ## Stíluslap
59
74
 
60
75
  A widget a kinézetét is hozhatja: a `css` mezőben egy CSS-szöveget, amit a kliens a dokumentum
@@ -217,7 +232,9 @@ Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
217
232
 
218
233
  - Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
219
234
  - Primitív → `String(v)` (`false` → `false`, `0` → `0`).
220
- - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`.
235
+ - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`. Egyetlen kivétel: egy **ikon-slot**
236
+ név-attribútumának teljes értékeként az objektum az ikon `ImageResource`-a lesz — lásd
237
+ *Ikon-slot*.
221
238
  - Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
222
239
  nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
223
240
 
@@ -300,7 +317,9 @@ escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en m
300
317
 
301
318
  | Hol | Mi történik |
302
319
  |---|---|
303
- | Java-builder (`build()`) | `IllegalArgumentException`: párosítatlan / keresztezett szekció, raw tag, érvénytelen path vagy `root`, a `root` fölé mutató `../`, ismeretlen pipe, rossz pipe-szintaxis, üres `data-sb4-toolbar` / `data-sb4-action` |
320
+ | Java-builder (`build()`) | `IllegalArgumentException`: párosítatlan / keresztezett szekció, raw tag, érvénytelen path vagy `root`, a `root` fölé mutató `../`, ismeretlen pipe, rossz pipe-szintaxis, üres `data-sb4-toolbar` / `data-sb4-action` /
321
+ `data-sb4-icon` / `data-sb4-icon-key`, `data-sb4-icon` és `data-sb4-icon-key` ugyanazon az elemen |
322
+ | Java-builder (`build()`) | `IllegalStateException`: a sablonban álló literál ikon-kulcs, amit az ikon-szótár nem tartalmaz |
304
323
  | Java-builder (`build()`), `resolveHtml` | `IllegalStateException`: feloldatlan szerver-helyőrző (`${alias:/`), lásd *A szerver-fázis* |
305
324
  | kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
306
325
 
@@ -344,6 +363,92 @@ nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarPropert
344
363
  - A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
345
364
  eredménynél a hidratált toolbarok élnek tovább.
346
365
 
366
+ ## Ikon-slot
367
+
368
+ ```html
369
+ <smart-icon data-sb4-icon="mail" data-sb4-icon-color="warn"></smart-icon>
370
+ <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
371
+ <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
372
+ ```
373
+
374
+ A kliens a marker-elembe egy **valódi `smart-icon`-t hidratál**, ugyanazt a komponenst, amit a
375
+ gomb, a grid-cella és a form használ. Java: `Tag.icon(name)`, `Tag.icon(name, color)`,
376
+ `Tag.icon(imageResource)`, `Tag.iconKey(key)`, a szótár a widget-builderen: `h.icon(key, resource)`
377
+ / `h.icons(map)`.
378
+
379
+ Az ikon címe négyféle lehet, és mindegyiknek megvan a maga esete:
380
+
381
+ | Forma | Mit ad az ikonnak | Mikor |
382
+ |---|---|---|
383
+ | `data-sb4-icon="mail"` | `icon` input: a név előbb a regisztrált svg-névterekben, aztán Material-ligatúraként | fix ikon, vagy adatból jövő **ikonnév** |
384
+ | `data-sb4-icon="{{typeIcon}}"`, az érték **objektum** | `imageResource` input | soronként más erőforrás (feltöltött kép, `ImageSettingApi` a modellben) |
385
+ | `data-sb4-icon="{…}"` (JSON) | `imageResource` input | a page által **egyszer** feloldott erőforrás (`Tag.icon(resource)`) |
386
+ | `data-sb4-icon-key="{{type}}"` | `imageResource` input a **szótárból** | véges típus → ikon leképezés |
387
+
388
+ - **`data-sb4-icon-color`**: a `color` input, vagyis téma- vagy host-osztálynév (`primary`,
389
+ `text-primary`, `warn` …), **nem** CSS-szín. Csak a név-formához való: az `ImageResource` a saját
390
+ `color`-ját hozza. Elhagyva a komponens alapértelmezése (`primary`) marad.
391
+ - **Objektum csak ott**, ahol az attribútum értéke pontosan egy helyőrző. Szöveggel keverve
392
+ (`data-sb4-icon="ic-{{typeIcon}}"`), pipe-pal vagy listán az érték a nyelvtan általános szabálya
393
+ szerint üres marad, warninggal. A feloldó az objektumot egy **renderelésenként újrahúzott,
394
+ véletlen tokennel** adja át a hidratálásnak, hogy adatból ne lehessen másik ikon erőforrására
395
+ hivatkozni.
396
+ - **Üres cím** (nincs ikonja ennek a sornak) → néma kihagyás, az elem üresen marad. A sablonban
397
+ **szó szerint üres** marker (`data-sb4-icon=""`) viszont `build()`-hiba, ahogy a toolbarnál.
398
+ - **Ismeretlen szótárkulcs** → nincs ikon, egy warning. A sablonban álló **literál** kulcsot, ami
399
+ nincs a szótárban, már a `build()` elutasítja.
400
+ - Egy elemen **nem állhat** a `data-sb4-icon` és a `data-sb4-icon-key` egyszerre: `build()`-hiba, a
401
+ kliens a kulcsot használja és warningot ír.
402
+ - A marker `class` és `style` attribútuma a helyén marad (a hidratálás a marker-elembe rendereli a
403
+ komponenst), a méretezés a widget CSS-éből megy: `.card-icon mat-icon { font-size: … }`.
404
+ - Az ikon a nevét a regiszterben keresi meg, ezért **egy microtaskkal később** jelenik meg — a
405
+ kliens ettől még ugyanabban a renderelésben írja ki a html-t.
406
+
407
+ ### Példa: típus szerinti ikon egy ImageResource-ból
408
+
409
+ A szerveren az `ImageSettingApi` képezi le a típust ikonra (`images-*.properties`):
410
+
411
+ ```properties
412
+ ContactType.EMAIL={"identifier":"mail","color":"accent","tooltip":{"tooltip":"E-mail cím"}}
413
+ ContactType.PHONE={"identifier":"call","color":"primary","tooltip":{"tooltip":"Telefonszám"}}
414
+ ```
415
+
416
+ **Soronként más erőforrás** — az objektum az adatban utazik:
417
+
418
+ ```java
419
+ row.getData().put("typeIcon", imageSettingApi.get("ContactType", type.name()));
420
+ ```
421
+
422
+ ```html
423
+ <div class="pg-row">
424
+ <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
425
+ <b>{{name}}</b>
426
+ </div>
427
+ ```
428
+
429
+ **Véges leképezés** — az erőforrás egyszer utazik, a sor csak a kódot viszi:
430
+
431
+ ```java
432
+ .htmlColumn("type", h -> h
433
+ .icon("EMAIL", imageSettingApi.get("ContactType", "EMAIL"))
434
+ .icon("PHONE", imageSettingApi.get("ContactType", "PHONE"))
435
+ .html("""
436
+ <div class="pg-row">
437
+ <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
438
+ <b>{{name}}</b>
439
+ </div>"""))
440
+ ```
441
+
442
+ Mindkettőből ugyanaz lesz a DOM-ban:
443
+
444
+ ```html
445
+ <smart-icon data-sb4-icon="…"><mat-icon class="mat-accent material-icons">mail</mat-icon></smart-icon>
446
+ ```
447
+
448
+ A szótár a **sablon** része (`HtmlProperties.icons`), nem a soré: a sima HTML widgetnél is egyszer
449
+ megy le, és egy újra-feloldás már a leküldött erőforrások közül választ. Nem tévesztendő össze a
450
+ `GridRow.icons`-szal, ami a **cella értéke mellé** rajzolt ikonokat írja le.
451
+
347
452
  ## Akció-trigger
348
453
 
349
454
  ```html
@@ -468,7 +573,7 @@ gridBuilder.htmlColumn("name", h -> h.html(t -> t
468
573
  .toolbar("ROW_TOOLBAR")))
469
574
  ```
470
575
 
471
- - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, és
576
+ - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, `icons`, és
472
577
  ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
473
578
  `valueSets` a gridet mutató oldalé.
474
579
  - A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
@@ -518,7 +623,8 @@ gridBuilder.htmlColumn("name", h -> h.html(t -> t
518
623
  `root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
519
624
  szándékosan nem tud, annak a helye:
520
625
 
521
- - **ikon / értékfüggő megjelenés**: szerver-származtatott mező, vagy érték-alapú CSS-osztály
626
+ - **ikon**: ikon-slot (`data-sb4-icon` / `data-sb4-icon-key`), a leképezés az `ImageSettingApi`-ban;
627
+ értékfüggő megjelenés egyébként: szerver-származtatott mező, vagy érték-alapú CSS-osztály
522
628
  (`contact-card-icon-{{channelType}}`);
523
629
  - **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
524
630
  logikai mező + `{{?…}}` őr;
package/MIGRATION-7.0.md CHANGED
@@ -720,6 +720,13 @@ went 17 → 22 with the library still installed:
720
720
  is not blocking. It becomes a *hard* install failure the moment `@angular/build`
721
721
  is a direct devDependency rather than a nested one, which is what happened in the
722
722
  library workspace. `karma@~6.4.4` clears it.
723
+ - **`ngx-markdown` 22 lazily imports `marked-katex-extension`**, an optional peer. The
724
+ application builder leaves that import alone; the webpack `browser` builder every measured
725
+ host still runs resolves it, and the build fails with `Can't resolve
726
+ 'marked-katex-extension'` against ngx-markdown's own bundle. Declare
727
+ `katex@^0.16` and `marked-katex-extension@^5`: webpack only puts them in a lazy chunk that
728
+ loads when KaTeX is switched on. ngx-markdown 22 also wants `marked@^17 || ^18` declared
729
+ (app-formmate, 2026-09-22).
723
730
 
724
731
  The general shape: **`ng update` needs `--force` at every hop** (the library and
725
732
  PrimeNG both peer-cap at Angular 17 until the very end), and `--force` covers the
@@ -1327,6 +1334,13 @@ provideSmartNgClient({
1327
1334
 
1328
1335
  `SmartViewContextService.getSmartViewContextApiErrorByCode()` is no longer `async`.
1329
1336
 
1337
+ **`SmartComponent` no longer has a `translate` member**, and a host that injected
1338
+ ngx-translate's `TranslateService` under that name as `override translate: TranslateService`
1339
+ now gets `TS4113` on every such constructor (app-formmate had 22). Do not just delete
1340
+ `override`: it was also what made the parameter a property, so without an access modifier
1341
+ `this.translate` disappears. Write `public translate: TranslateService` — and keep `override`
1342
+ on subclasses of your own pages, which now override *your* base class instead of the library's.
1343
+
1330
1344
  #### If your host still bootstraps with zone.js
1331
1345
 
1332
1346
  Nothing forces you to drop it — the library works either way, because it no longer depends
@@ -1860,6 +1874,15 @@ Additive, with **one behaviour change** and one removed field. The reference is
1860
1874
  going out.
1861
1875
  Inside a `{{#section}}` a placeholder reads the surrounding scope only when it says so,
1862
1876
  `{{../title}}`: one step per entering section, never above the widget's root.
1877
+ - **Icon slots (in the tree, released after 7.2.4)**: an element in the html marked with
1878
+ `data-sb4-icon` hydrates into a
1879
+ `smart-icon` — the same component a button or a cell uses. The marker addresses a name
1880
+ (`data-sb4-icon="mail"`, coloured by `data-sb4-icon-color`, a theme or host class name), an
1881
+ `ImageResource` (a placeholder that resolves to one, or the json `Tag.icon(resource)` writes
1882
+ in), or a key of the template's icon dictionary (`data-sb4-icon-key`,
1883
+ `HtmlProperties.icons`), which sends a finite type-to-icon mapping once with the template
1884
+ rather than once per row. An address that resolves to nothing renders no icon and says
1885
+ nothing; an unknown dictionary key warns. Nothing changes for a host that sends no icon slot.
1863
1886
  - **Row layouts**: a cards grid can carry its card layouts once per grid
1864
1887
  (`GridViewDescriptor.rowLayouts`, picked per row by `GridRow.rowLayout`) instead of once per
1865
1888
  row. The per-row `layoutDescriptor.GRID_ROW_LAYOUT` still works and wins over them; a
package/WIDGETS.md CHANGED
@@ -155,7 +155,9 @@ toolbar slot of the html the backend sent (`<smart-ui-action-toolbar data-sb4-to
155
155
  such a toolbar resolves by the very same pull: a `SmartActionHost` above it if one has a list, the
156
156
  screen component otherwise. That is why a row layout's html gets the row's actions on a grid card
157
157
  with no code of its own, and a table cell rendered from a column template gets them the same way.
158
- An action trigger (`data-sb4-action`) looks its action up in the same list. See `HTML-WIDGET.md`.
158
+ An action trigger (`data-sb4-action`) looks its action up in the same list, and an icon slot
159
+ (`data-sb4-icon`, `data-sb4-icon-key`) hydrates into a `smart-icon` the same way. See
160
+ `HTML-WIDGET.md`.
159
161
 
160
162
  **A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
161
163
  `readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, reflectComponentType, Injectable, inject, DOCUMENT, PLATFORM_ID, Optional, Inject, RendererStyleFlags2, input, computed, Component, HostListener, HostBinding, Directive, ViewContainerRef, ElementRef, Renderer2, ChangeDetectorRef, Input, output, signal, Injector, ViewChild, ViewChildren, effect, afterNextRender, NgZone, forwardRef, createComponent, CSP_NONCE, EnvironmentInjector, ApplicationRef, untracked, DestroyRef, ChangeDetectionStrategy, ViewEncapsulation, Pipe, SkipSelf, NgModule, EventEmitter, Output, viewChild, makeEnvironmentProviders, importProvidersFrom } from '@angular/core';
2
+ import { InjectionToken, reflectComponentType, Injectable, inject, DOCUMENT, PLATFORM_ID, Optional, Inject, RendererStyleFlags2, input, computed, Component, HostListener, HostBinding, Directive, ViewContainerRef, ElementRef, Renderer2, ChangeDetectorRef, Input, output, signal, Injector, SkipSelf, NgModule, ViewChild, ViewChildren, effect, afterNextRender, NgZone, forwardRef, createComponent, CSP_NONCE, EnvironmentInjector, ApplicationRef, untracked, DestroyRef, ChangeDetectionStrategy, ViewEncapsulation, Pipe, EventEmitter, Output, viewChild, makeEnvironmentProviders, importProvidersFrom } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
4
  import { HttpHeaders, HttpContext, HttpErrorResponse, HttpParams, provideHttpClient, withInterceptors, withInterceptorsFromDi } from '@angular/common/http';
5
5
  import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
@@ -6162,18 +6162,6 @@ const APIS$4 = [DefaultService$1];
6162
6162
  * Do not edit the class manually.
6163
6163
  */
6164
6164
 
6165
- /**
6166
- * Form layout definition
6167
- * Contains form layout definition objects.
6168
- *
6169
- * The version of the OpenAPI document: 1.0.0
6170
- * Contact: info@it4all.hu
6171
- *
6172
- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
6173
- * https://openapi-generator.tech
6174
- * Do not edit the class manually.
6175
- */
6176
-
6177
6165
  var RecordingUploaderPropertiesOpenDirectionEnum;
6178
6166
  (function (RecordingUploaderPropertiesOpenDirectionEnum) {
6179
6167
  RecordingUploaderPropertiesOpenDirectionEnum["RIGHT"] = "RIGHT";
@@ -7634,6 +7622,42 @@ var ToolbarDirection;
7634
7622
  ToolbarDirection["VERTICAL"] = "VERTICAL";
7635
7623
  })(ToolbarDirection || (ToolbarDirection = {}));
7636
7624
 
7625
+ let ApiModule$1 = class ApiModule {
7626
+ static forRoot(configurationFactory) {
7627
+ return {
7628
+ ngModule: ApiModule,
7629
+ providers: [{ provide: Configuration$5, useFactory: configurationFactory }]
7630
+ };
7631
+ }
7632
+ constructor(parentModule, http) {
7633
+ if (parentModule) {
7634
+ throw new Error('ApiModule is already loaded. Import in your base AppModule only.');
7635
+ }
7636
+ if (!http) {
7637
+ throw new Error('You need to import the HttpClientModule in your AppModule! \n' +
7638
+ 'See also https://github.com/angular/angular/issues/20575');
7639
+ }
7640
+ }
7641
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiModule, deps: [{ token: ApiModule, optional: true, skipSelf: true }, { token: i1.HttpClient, optional: true }], target: i0.ɵɵFactoryTarget.NgModule }); }
7642
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: ApiModule }); }
7643
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiModule }); }
7644
+ };
7645
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiModule$1, decorators: [{
7646
+ type: NgModule,
7647
+ args: [{
7648
+ imports: [],
7649
+ declarations: [],
7650
+ exports: [],
7651
+ providers: []
7652
+ }]
7653
+ }], ctorParameters: () => [{ type: ApiModule$1, decorators: [{
7654
+ type: Optional
7655
+ }, {
7656
+ type: SkipSelf
7657
+ }] }, { type: i1.HttpClient, decorators: [{
7658
+ type: Optional
7659
+ }] }] });
7660
+
7637
7661
  class UiMenuService {
7638
7662
  constructor() {
7639
7663
  this.closeMenus$ = new Subject();
@@ -9617,6 +9641,79 @@ function enumAttribute(marker, name, values, ctx) {
9617
9641
  }
9618
9642
  return value;
9619
9643
  }
9644
+ /**
9645
+ * Turns every icon slot under the container into a live `smart-icon` rendered into the marker
9646
+ * element itself. A slot addresses its icon in one of three ways, and an empty address — the
9647
+ * ordinary case of a row that has no icon — is skipped without a word:
9648
+ *
9649
+ * - `data-sb4-icon` with a name, which the icon resolves against the registered svg namespaces
9650
+ * and falls back to a material ligature, optionally coloured by `data-sb4-icon-color`;
9651
+ * - `data-sb4-icon` with an image resource, either as the token the resolver left for an object
9652
+ * valued placeholder or as the JSON a backend builder wrote in;
9653
+ * - `data-sb4-icon-key` with a key of the template's icon dictionary, which travels once however
9654
+ * many rows read it.
9655
+ *
9656
+ * The refs are the caller's, like the toolbars': destroy them before the html is rewritten.
9657
+ */
9658
+ function hydrateIcons(container, ctx, icons) {
9659
+ const refs = [];
9660
+ container
9661
+ .querySelectorAll('[data-sb4-icon], [data-sb4-icon-key]')
9662
+ .forEach((marker) => {
9663
+ const key = marker.getAttribute('data-sb4-icon-key');
9664
+ const name = marker.getAttribute('data-sb4-icon');
9665
+ if (key !== null && name !== null) {
9666
+ ctx.warn('an icon slot addresses both a name and a dictionary key, the key is used');
9667
+ }
9668
+ const ref = createComponent(SmartIconComponent, {
9669
+ hostElement: marker,
9670
+ elementInjector: ctx.elementInjector,
9671
+ environmentInjector: ctx.environmentInjector,
9672
+ });
9673
+ if (key !== null) {
9674
+ const resource = icons.dictionary?.[key];
9675
+ if (key && !resource) {
9676
+ ctx.warn(`the icon dictionary has no '${key}'`);
9677
+ }
9678
+ ref.setInput('imageResource', resource);
9679
+ }
9680
+ else {
9681
+ const resource = imageResource(name, icons, ctx);
9682
+ if (resource) {
9683
+ ref.setInput('imageResource', resource);
9684
+ }
9685
+ else {
9686
+ ref.setInput('icon', name || undefined);
9687
+ ref.setInput('color', marker.getAttribute('data-sb4-icon-color') ?? undefined);
9688
+ }
9689
+ }
9690
+ // Without a zone nothing else would ever refresh a view that is in no template.
9691
+ ctx.appRef.attachView(ref.hostView);
9692
+ ref.changeDetectorRef.detectChanges();
9693
+ refs.push(ref);
9694
+ });
9695
+ return refs;
9696
+ }
9697
+ /** The resource an address stands for, undefined when it is a plain icon name. */
9698
+ function imageResource(address, icons, ctx) {
9699
+ if (icons.objects.has(address)) {
9700
+ return icons.objects.get(address);
9701
+ }
9702
+ if (address.startsWith('sb4-img:')) {
9703
+ ctx.warn(`the image resource of '${address}' is not from this render, the icon is left empty`);
9704
+ return {};
9705
+ }
9706
+ if (!address.startsWith('{')) {
9707
+ return undefined;
9708
+ }
9709
+ try {
9710
+ return JSON.parse(address);
9711
+ }
9712
+ catch {
9713
+ ctx.warn(`an icon slot holds html that is not an image resource: ${address.substring(0, 40)}`);
9714
+ return {};
9715
+ }
9716
+ }
9620
9717
 
9621
9718
  /** Pipes that must have an argument, and pipes that must not; the Java scanner checks the same. */
9622
9719
  const PIPES_REQUIRING_ARGUMENT = ['label'];
@@ -9694,6 +9791,8 @@ function formatNumber(v, digitsInfo, ctx) {
9694
9791
  }
9695
9792
 
9696
9793
  const IDENTIFIER = '[A-Za-z_][A-Za-z0-9_]*';
9794
+ /** The whole value of an icon slot's name attribute: only there may a placeholder give an object. */
9795
+ const ICON_ATTRIBUTE = /data-sb4-icon\s*=\s*"$/i;
9697
9796
  const PATH = new RegExp(`^((?:\\.\\./)*)(${IDENTIFIER}(?:\\.${IDENTIFIER})*)$`);
9698
9797
  const PIPE_NAME = new RegExp(IDENTIFIER, 'y');
9699
9798
  function resolveTemplate(template, scope, ctx) {
@@ -9702,11 +9801,33 @@ function resolveTemplate(template, scope, ctx) {
9702
9801
  nodes = parse(template);
9703
9802
  }
9704
9803
  catch (e) {
9705
- return { html: template, errors: [e.message] };
9804
+ return { html: template, errors: [e.message], objects: new Map(), identity: template };
9706
9805
  }
9707
9806
  const out = [];
9708
- render(nodes, [scope], ctx, out);
9709
- return { html: out.join(''), errors: [] };
9807
+ const sink = { prefix: `sb4-img:${nonce()}:`, objects: new Map() };
9808
+ render(nodes, [scope], ctx, out, sink);
9809
+ const html = out.join('');
9810
+ return { html, errors: [], objects: sink.objects, identity: identityOf(html, sink) };
9811
+ }
9812
+ /** The html with each token replaced by its object, so an unchanged render stays unchanged. */
9813
+ function identityOf(html, sink) {
9814
+ let identity = html;
9815
+ sink.objects.forEach((value, token) => {
9816
+ let written;
9817
+ try {
9818
+ written = JSON.stringify(value) ?? '';
9819
+ }
9820
+ catch {
9821
+ written = token.substring(sink.prefix.length);
9822
+ }
9823
+ identity = identity.split(token).join(written);
9824
+ });
9825
+ return identity;
9826
+ }
9827
+ function nonce() {
9828
+ return typeof crypto !== 'undefined' && crypto.randomUUID
9829
+ ? crypto.randomUUID()
9830
+ : String(Math.random()).substring(2);
9710
9831
  }
9711
9832
  /** Walks a dot-path; a missing, null or non-object step anywhere gives undefined. */
9712
9833
  function lookupPath(scope, path) {
@@ -9787,6 +9908,9 @@ function parse(template) {
9787
9908
  ...(path === '.' ? { parents: 0, path } : parsePath(path, tag, open)),
9788
9909
  pipes: pipe < 0 ? [] : parsePipes(content.substring(pipe + 1), tag),
9789
9910
  tag,
9911
+ resource: pipe < 0 &&
9912
+ template[end + 2] === '"' &&
9913
+ ICON_ATTRIBUTE.test(template.substring(pos, start)),
9790
9914
  });
9791
9915
  }
9792
9916
  pos = end + 2;
@@ -9873,38 +9997,38 @@ function skipSpaces(s, i) {
9873
9997
  * `scopes` holds one entry per open entering section above the root scope, the innermost last;
9874
9998
  * the parser has already checked that no `../` reaches below its bottom.
9875
9999
  */
9876
- function render(nodes, scopes, ctx, out) {
10000
+ function render(nodes, scopes, ctx, out, sink) {
9877
10001
  const scope = scopes[scopes.length - 1];
9878
10002
  for (const node of nodes) {
9879
10003
  if (node.kind === 'text') {
9880
10004
  out.push(node.text);
9881
10005
  }
9882
10006
  else if (node.kind === 'value') {
9883
- out.push(renderValue(node, scopes, ctx));
10007
+ out.push(renderValue(node, scopes, ctx, sink));
9884
10008
  }
9885
10009
  else {
9886
10010
  const value = lookup(scopes, node);
9887
10011
  if (node.sigil === '^') {
9888
10012
  if (!truthy(value)) {
9889
- render(node.children, scopes, ctx, out);
10013
+ render(node.children, scopes, ctx, out, sink);
9890
10014
  }
9891
10015
  }
9892
10016
  else if (!truthy(value)) {
9893
10017
  continue;
9894
10018
  }
9895
10019
  else if (node.sigil === '?') {
9896
- render(node.children, scopes, ctx, out);
10020
+ render(node.children, scopes, ctx, out, sink);
9897
10021
  }
9898
10022
  else if (Array.isArray(value)) {
9899
- value.forEach((item) => render(node.children, [...scopes, item], ctx, out));
10023
+ value.forEach((item) => render(node.children, [...scopes, item], ctx, out, sink));
9900
10024
  }
9901
10025
  else if (typeof value === 'object') {
9902
- render(node.children, [...scopes, value], ctx, out);
10026
+ render(node.children, [...scopes, value], ctx, out, sink);
9903
10027
  }
9904
10028
  else {
9905
10029
  ctx.warn(`{{#${node.source}}} entered a single value; use {{?${node.source}}} to guard on it`);
9906
10030
  // Still a level, so `../` inside counts the same as the server's field derivation does.
9907
- render(node.children, [...scopes, scope], ctx, out);
10031
+ render(node.children, [...scopes, scope], ctx, out, sink);
9908
10032
  }
9909
10033
  }
9910
10034
  }
@@ -9912,7 +10036,7 @@ function render(nodes, scopes, ctx, out) {
9912
10036
  function lookup(scopes, path) {
9913
10037
  return lookupPath(scopes[scopes.length - 1 - path.parents], path.path);
9914
10038
  }
9915
- function renderValue(node, scopes, ctx) {
10039
+ function renderValue(node, scopes, ctx, sink) {
9916
10040
  let value = node.path === '.' ? scopes[scopes.length - 1] : lookup(scopes, node);
9917
10041
  for (const pipe of node.pipes) {
9918
10042
  // Own names only: `constructor` and the like must not reach Object.prototype.
@@ -9927,6 +10051,12 @@ function renderValue(node, scopes, ctx) {
9927
10051
  return '';
9928
10052
  }
9929
10053
  if (typeof value === 'object') {
10054
+ // An icon slot takes an image resource as it stands; everywhere else an object is nothing.
10055
+ if (node.resource && !Array.isArray(value)) {
10056
+ const token = `${sink.prefix}${sink.objects.size}`;
10057
+ sink.objects.set(token, value);
10058
+ return token;
10059
+ }
9930
10060
  ctx.warn(`${node.tag} names an object or a list, rendered empty`);
9931
10061
  return '';
9932
10062
  }
@@ -10167,17 +10297,23 @@ function injectSmartHtmlRenderer(container) {
10167
10297
  untracked(() => triggers.mark());
10168
10298
  }, /* @ts-ignore */
10169
10299
  ...(ngDevMode ? [{ debugName: "marking" }] : /* istanbul ignore next */ []));
10170
- const destroyToolbars = () => {
10300
+ const destroyHydrated = () => {
10171
10301
  refs.forEach((ref) => ref.destroy());
10172
10302
  refs = [];
10173
10303
  };
10174
- const write = (html) => {
10175
- current = html;
10176
- // Rewriting the html does not destroy the toolbars hydrated into it.
10177
- destroyToolbars();
10304
+ const write = (resolved) => {
10305
+ current = resolved;
10306
+ // Rewriting the html does not destroy the toolbars and icons hydrated into it.
10307
+ destroyHydrated();
10178
10308
  // Trusted as authored by the backend; every value the resolver put in is escaped.
10179
- container.innerHTML = html;
10180
- refs = hydrateToolbars(container, hydration);
10309
+ container.innerHTML = resolved.html;
10310
+ refs = [
10311
+ ...hydrateToolbars(container, hydration),
10312
+ ...hydrateIcons(container, hydration, {
10313
+ dictionary: resolved.icons,
10314
+ objects: resolved.objects,
10315
+ }),
10316
+ ];
10181
10317
  triggers.mark();
10182
10318
  };
10183
10319
  const cancelLeave = () => {
@@ -10186,7 +10322,7 @@ function injectSmartHtmlRenderer(container) {
10186
10322
  container.classList.remove(LEAVING_CLASS);
10187
10323
  };
10188
10324
  /** Plays what the style sheets give the leaving class, then writes the latest pending html. */
10189
- const leaveThenWrite = (html) => {
10325
+ const leaveThenWrite = (resolved) => {
10190
10326
  // Only what the class starts counts; an entry or endless animation already running does not.
10191
10327
  const running = new Set(container.getAnimations({ subtree: true }));
10192
10328
  container.classList.add(LEAVING_CLASS);
@@ -10195,10 +10331,10 @@ function injectSmartHtmlRenderer(container) {
10195
10331
  .filter((animation) => !running.has(animation));
10196
10332
  if (!leaving.length) {
10197
10333
  container.classList.remove(LEAVING_CLASS);
10198
- write(html);
10334
+ write(resolved);
10199
10335
  return;
10200
10336
  }
10201
- pending = html;
10337
+ pending = resolved;
10202
10338
  const token = ++leave;
10203
10339
  Promise.race([
10204
10340
  Promise.allSettled(leaving.map((animation) => animation.finished)),
@@ -10217,30 +10353,32 @@ function injectSmartHtmlRenderer(container) {
10217
10353
  // the action list the triggers read.
10218
10354
  render: (newTemplate, context) => untracked(() => {
10219
10355
  template = newTemplate;
10220
- const { html, errors } = resolveTemplate(template, context.data, {
10356
+ const result = resolveTemplate(template, context.data, {
10221
10357
  valueSets: context.valueSets,
10222
10358
  locale: locale(),
10223
10359
  warn,
10224
10360
  });
10225
- errors.forEach((error) => report(`the html is left unresolved: ${error}`, console.error));
10361
+ const resolved = { ...result, icons: context.icons };
10362
+ result.errors.forEach((error) => report(`the html is left unresolved: ${error}`, console.error));
10363
+ const unchanged = resolved.identity === current?.identity;
10226
10364
  if (pending !== undefined) {
10227
10365
  // Mid-leave the latest html wins; back to the one on screen, nothing leaves after all.
10228
- if (html === current) {
10366
+ if (unchanged) {
10229
10367
  cancelLeave();
10230
10368
  }
10231
10369
  else {
10232
- pending = html;
10370
+ pending = resolved;
10233
10371
  }
10234
10372
  return;
10235
10373
  }
10236
- if (html === current) {
10374
+ if (unchanged) {
10237
10375
  return;
10238
10376
  }
10239
10377
  if (current === undefined) {
10240
- write(html);
10378
+ write(resolved);
10241
10379
  return;
10242
10380
  }
10243
- leaveThenWrite(html);
10381
+ leaveThenWrite(resolved);
10244
10382
  }),
10245
10383
  useCss: (newCss) => {
10246
10384
  const next = newCss || undefined;
@@ -10255,7 +10393,7 @@ function injectSmartHtmlRenderer(container) {
10255
10393
  disposed = true;
10256
10394
  releaseCss();
10257
10395
  marking.destroy();
10258
- destroyToolbars();
10396
+ destroyHydrated();
10259
10397
  triggers.dispose();
10260
10398
  },
10261
10399
  };
@@ -10283,6 +10421,7 @@ class SmartHtmlWidgetComponent {
10283
10421
  this.renderer.render(properties?.html ?? '', {
10284
10422
  data: this.scope(model?.data, properties?.root, properties?.html),
10285
10423
  valueSets: model?.valueSets,
10424
+ icons: properties?.icons,
10286
10425
  });
10287
10426
  });
10288
10427
  }
@@ -13075,7 +13214,7 @@ class SmartHtmlCellComponent {
13075
13214
  }
13076
13215
  ngOnChanges() {
13077
13216
  this.actionHost.actionModels.set(rowActionModels(this.row, this.gridIdentifier));
13078
- const { html, root, css } = this.template;
13217
+ const { html, root, css, icons } = this.template;
13079
13218
  const scope = rootScope(this.row.data, root);
13080
13219
  if (scope === undefined && root && this.warnedRoot !== root) {
13081
13220
  this.warnedRoot = root;
@@ -13086,6 +13225,7 @@ class SmartHtmlCellComponent {
13086
13225
  this.renderer.render(html ?? '', {
13087
13226
  data: scope,
13088
13227
  valueSets: this.smartComponent?.model?.valueSets,
13228
+ icons,
13089
13229
  });
13090
13230
  }
13091
13231
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SmartHtmlCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -23624,6 +23764,7 @@ class SmartComponentLayoutComponent {
23624
23764
  ['smartComponentLayout', data],
23625
23765
  ['parentLayoutComponent', this.parentLayoutComponent],
23626
23766
  ]),
23767
+ isExpanded: data.expanded ?? false,
23627
23768
  headerToolbarId: this.smartComponentLayout?.expandableSectionHeaderToolbarId,
23628
23769
  customComponent: SmartComponentLayoutComponent,
23629
23770
  };