@smartbit4all/ng-client 7.3.21 → 7.3.23

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
@@ -1,710 +1,743 @@
1
- # HTML widget — szerverről küldött html a `@smartbit4all/ng-client` 7.2-ben
2
-
3
- Ez a dokumentum a **HTML widget**, a **kliens-helyőrzők** (`{{…}}`), a html-be írt **toolbar-slot**
4
- és **akció-trigger**, a kártya-grid **sor-layoutjainak**, valamint a tábla **html-celláinak**
5
- referenciája. A Java-oldali builder (`HtmlWidgetBuilder`, `HtmlBuilder.Tag.toolbar/action`,
6
- `GridBuilder.rowLayout/htmlColumn/contentType`) javadocja
7
- ide hivatkozik; ha a kettő ellentmond egymásnak, ez a fájl az igazság.
8
-
9
- ## Mire való
10
-
11
- A backend egy `HTML` típusú form-widgetet tehet a layoutba, amelynek a tartalma egy darab html. A
12
- html-t **két fázisban** oldjuk fel, két külön jelöléssel:
13
-
14
- | Fázis | Jelölés | Ki oldja fel | Mire jó |
15
- |---|---|---|---|
16
- | szerver-helyőrző | `${…}` | a backend, a page-ben, mielőtt a html elhagyja | feltétel, iteráció, szkript, lokalizáció — a platform meglévő template-dialektusa |
17
- | kliens-helyőrző | `{{…}}` | a kliens, a widget **feloldási kontextusán** | ugyanaz a html egyszer utazik és soronként / modell-változásonként újra feloldódik |
18
-
19
- A kliens a szerver-fázisból semmit nem lát: mire a html megérkezik, a `${…}` már nincs benne.
20
-
21
- **Bizalmi határ.** A html-t a backend írta, ezért a kliens **változtatás nélkül** írja ki (nincs
22
- sanitizálás: `data-*`, `style`, `<svg>`, inline `on*` mind él). Amit viszont a kliens helyettesít be
23
- egy `{{…}}` helyére, az **mindig HTML-escape-en** megy át, kivétel és „raw" forma nélkül. Ha a host
24
- Trusted Types-ot használ, a html kiírása miatt engedélyeznie kell a megfelelő policy-t. A widget
25
- stíluslapja (`css`, lásd *Stíluslap*) ugyanígy, változatlanul kerül a dokumentumba.
26
-
27
- A `DIV` widget változatlanul megmarad; a HTML widget annak a használatnak a rendes helye, amikor a
28
- `DIV`-be kész html-t tettünk.
29
-
30
- ## A widget a dróton
31
-
32
- `SmartWidgetDefinition.type = HTML`, a tulajdonságai a `properties["HTML_properties"]` alatt
33
- (`HtmlProperties`):
34
-
35
- | Mező | Jelentés |
36
- |---|---|
37
- | `html` | a html, a `${…}` már feloldva, a `{{…}}` benne hagyva |
38
- | `root` | dot-path a kontextus `data`-jához képest, amihez minden helyőrző relatív; **objektumot** nevez; üres = a `data` maga |
39
- | `fields` | a widget által olvasott abszolút pathok — a builder származtatja a helyőrzőkből, a kliens **nem** olvassa |
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* |
42
-
43
- A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
44
- (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
45
- semmit nevez, minden helyőrző üres marad, és a kliens egyszer `console.warn`-t ír.
46
-
47
- A widgetnek nincs kontrollja, címkéje, validálása és saját kattintása sem (lásd *Akció-trigger*).
48
-
49
- Java-oldalon:
50
-
51
- ```java
52
- layoutBuilder(view).vForm(f -> f
53
- .html(h -> h.root("card").html("<div class=\"card\">{{name}}</div>")))
54
- ```
55
-
56
- vagy template-kulcsból (`h.template(ctx, "CONTACT_ENTRY_LIST")`): a kulcs a szerveren oldódik fel,
57
- a dróton nem utazik.
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
-
73
- ## Stíluslap
74
-
75
- A widget a kinézetét is hozhatja: a `css` mezőben egy CSS-szöveget, amit a kliens a dokumentum
76
- `<head>`-jébe tesz (`<style data-sb4-html-css>`). Így a widget teljes egészében a szerverről jön, a
77
- hostnak nem kell hozzá stíluslapot szállítania.
78
-
79
- ```java
80
- vf.html(h -> h.root("card")
81
- .html("<div class=\"contact-card\">{{name}}</div>")
82
- .css(".contact-card { padding: 1rem; border-radius: 12px; }"))
83
- ```
84
-
85
- - **Egyszer kerül ki**, akárhány widget, kártya vagy cella hozza ugyanazt a szöveget, és az
86
- utolsó eltűnésével a kliens kiveszi. A sor-layout és az oszlop-sablon is így működik: a `css` a
87
- layout, illetve a sablon része, nem soronként utazik.
88
- - **Globális, nincs hatóköre.** Minden szelektor a html gyökérelemének egy osztályával kezdődjön
89
- (`.contact-card …`), különben az egész oldalra hat. Két különböző szöveg ugyanarra a
90
- szelektorra a betöltés sorrendjében nyer: adj widgetenként saját előtagot.
91
- - **Nem oldódik fel**: a `{{…}}`-nak nincs benne jelentése, modell-értéket nem lehet belevinni. Ami
92
- a modelltől függ, az a html-ben legyen osztály (`class="status-{{status}}"`), a CSS pedig
93
- osztályonként szabályozzon.
94
- - **CSP**: ha a host `style-src`-je nonce-os, adja meg az Angular `CSP_NONCE` tokenjét, a kliens
95
- ráteszi a `<style>`-ra. Nonce és `'unsafe-inline'` nélkül a böngésző a stíluslapot blokkolja.
96
- - A builder nem vizsgálja és nem formázza: a `%` és a `{{` sem zavarja.
97
-
98
- ### Stíluslap a template-kulcs mellől
99
-
100
- A `template(ctx, KEY)` a html mellé a stíluslapot is behúzza, ugyanazokon a neveken egy `.css`
101
- szegmenssel: `<viewName>.<KEY>.css`, `<KEY>.css`, … egészen a kulcs utolsó szegmenséig (a puszta
102
- `css` név sosem jön szóba). A fájl-registryben ez egy sor, pl.
103
- `CONTACT_ENTRY_LIST.css=templates/contact_entry_list.css`; MDM-ben egy ugyanilyen nevű
104
- `text/css` template.
105
-
106
- A html-lel ellentétben itt **nem egy nyer, hanem mind összefűződik**: előbb a fájlos stíluslapok,
107
- aztán az MDM-esek, mindkét rétegen belül az általánostól a specifikusig:
108
-
109
- ```
110
- <KEY>.css (fájl) → <viewName>.<KEY>.css (fájl) → <KEY>.css (MDM) → <viewName>.<KEY>.css (MDM)
111
- ```
112
-
113
- Mivel egyenlő specificitásnál a később jövő szabály nyer, az MDM a kódban szállított kinézetet
114
- szabályonként felülírhatja (ugyanazzal a szelektorral) vagy kiegészítheti (új szabállyal). A
115
- fájlban lévő `!important`-ot csak `!important` írja felül. A template-ből jött stíluslap lecseréli
116
- az addigi `css(...)`-t, egy későbbi `css(...)` pedig őt; ha a kulcshoz nincs stíluslap, az addigi
117
- `css` marad. A stíluslap nem értékelődik ki.
118
-
119
- ### Belépő és kilépő animáció
120
-
121
- A stíluslap (a widgeté vagy a hosté) animálhatja a widget és a cella tartalmának érkezését és
122
- távozását:
123
-
124
- ```css
125
- .contact-card { animation: contact-card-enter 300ms ease-out backwards; }
126
- .sb4-html-leaving .contact-card { animation: contact-card-leave 150ms ease-in forwards; }
127
-
128
- @keyframes contact-card-enter { from { opacity: 0; transform: translateY(12px); } }
129
- @keyframes contact-card-leave { to { opacity: 0; transform: translateY(-8px); } }
130
-
131
- @media (prefers-reduced-motion: reduce) {
132
- .contact-card, .sb4-html-leaving .contact-card { animation: none; }
133
- }
134
- ```
135
-
136
- - **Belépés**: a kiírt html új elemei a szokásos módon animálódnak. Ez minden olyan renderkor
137
- lefut, amely **új html-t ír**: az első megjelenéskor, és valahányszor a feloldás eredménye
138
- megváltozik (a modell vagy a sor olyan mezője változott, amit a html olvas). A változatlan
139
- eredményű push semmit nem ír, ott nem fut.
140
- - **Kilépés**: mielőtt a kliens a képernyőn lévő html-t lecseréli, a widget, illetve a cella
141
- elemére (`smart-html-widget`, `smart-html-cell`) felteszi az **`sb4-html-leaving`** osztályt.
142
- Ha egy stíluslap erre az osztályra animációt vagy transitiont indít, a kliens megvárja a
143
- végét, és csak utána írja ki az új html-t (és veszi le az osztályt). Ha semmi nem indul, azonnal
144
- ír: kilépő CSS nélkül minden úgy működik, mint eddig.
145
- - A várakozás **legfeljebb 600 ms**: egy végtelen vagy túl hosszú kilépő animáció sem tartja fel
146
- a frissítést. Csak az osztály által **elindított** animáció számít; ami már futott (pl. egy még
147
- tartó belépő vagy egy végtelen pörgés), az nem.
148
- - Kilépés közben érkező újabb renderből **az utolsó** html kerül ki. Ha a feloldás közben
149
- visszatér a képernyőn lévő html-re, a kilépés elmarad, az osztály lekerül.
150
- - Az első megjelenésnek nincs kilépése, csak belépése. Ugyanígy, ha a nézet layoutja cserélődik,
151
- a widget újraépül: a régi elem kilépés nélkül tűnik el, az új belép.
152
- - A kilépés alatt a régi html és a hidratált toolbarjai még élnek és kattinthatók. A kilépő
153
- animációt ezért érdemes röviden tartani, és `forwards` kitöltéssel a végállapotban hagyni, hogy
154
- az új html kiírásáig ne villanjon vissza.
155
- - A `backwards` kitöltés a belépésnél azért jó, mert a vége után a `:hover` és a többi szabály
156
- ismét mozgathatja az elemet (a `both` vagy a `forwards` animációs értéke felülírná őket).
157
-
158
- ## A szerver-fázis
159
-
160
- A szerver-helyőrző a platform template-dialektusa: `${alias:/útvonal#mező?stratégia}`, mellette a
161
- `data-sb4-context` (iteráció) és a `data-sb4-if` (feltétel) attribútum. A builder a **nyitásáról**
162
- ismeri fel — `${alias:/` —, a sima `${name}` neki és a motornak is közömbös szöveg. Literálként
163
- `&#36;{`-nak írd.
164
-
165
- Négy út oldja fel, és **egyik sem fut magától**:
166
-
167
- | Út | Mikor | Hiba esetén |
168
- |---|---|---|
169
- | `resolveTemplate(ctx, …)`, és az eredmény megy a `html(...)`-be | a build előtt | elnyel: logol, és a kulcsot vagy üres stringet ad vissza |
170
- | `h.template(ctx, KEY)` | a widget felépítésekor, kulcsból (lásd *Stíluslap a template-kulcs mellől*) | dob |
171
- | `h.resolveLater()` + `resolveHtml(ctx, layout)` | a **felépített layouton** | dob |
172
- | `h.templateKey(KEY)` + `resolveHtml(ctx, viewName, layout)` | a **felépített vagy tárolt layouton**, kulcsból (lásd *Template-kulcs egy tárolt layoutban*) | dob |
173
-
174
- ```java
175
- SmartComponentLayoutDefinition layout = layoutBuilder(view)
176
- .vForm(f -> f.html(h -> h.html(CARD_HTML).resolveLater()))
177
- .build();
178
- resolveHtml(objectApi.contextObject().set("contact", contact), layout); // PageApiImpl
179
- ```
180
-
181
- - A `resolveHtml` bejárja a layout `FORM` / `CONTAINER` fáját és a widgetek gyerek-widgetjeit, és
182
- minden HTML widget `html`-jét feloldja **ugyanazzal a ctx-szel**. A `css`, a `root` és a `fields`
183
- marad. A gridekhez nem nyúl — egy sor-layout maga is layout, arra külön hívható. Page-en kívül:
184
- `ObjectLayouts.resolveHtml(layout, resolver, objectApi, ctx)`; listára is van változata.
185
- - **Amiben nincs `${alias:/`, ahhoz hozzá sem ér** — az a html nem megy át a motoron, tehát nem is
186
- normalizálódik. Ezért bármilyen layoutra ráhívható, és a második futás egy már feloldott
187
- widgeten nem csinál semmit.
188
- - **Helyben módosít.** Ha az adat változik, **építsd újra a layoutot, ne oldd fel újra**: a
189
- view-ban tárolt, egyszer már feloldott layoutban nincs mit feloldani, az új ctx-szel hívott
190
- második futás némán a régi értékeket hagyja. Aki a feloldatlan eredetit meg akarja tartani,
191
- másolaton hívja.
192
- - **Hangosan bukik** (`IllegalStateException`): ha van `${alias:/`, de nincs template-modul a
193
- hostban; és ha a feloldás **után** is maradt — a motor az ismeretlen aliasú helyőrzőt ugyanis
194
- szó nélkül benne hagyja. A motor másik két csendes esete megmarad: a hiányzó / `null` mező
195
- helyére szóköz kerül, a mezőszintű feloldási hiba helyére a „Feldolgozási hiba" szöveg.
196
-
197
- **A `build()` őre.** Az a `${alias:/`, amit a négy út egyike sem oldott fel, régen szó nélkül
198
- kiment a kliensre, és nyersen megjelent. Most a `HtmlWidgetBuilder.build()` `IllegalStateException`-t
199
- dob rá, kivéve ha a widget `resolveLater()`-t mondott. Ez a `vf.html(String)`, az
200
- `ObjectLayoutBuilder.html(...)` és a `GridBuilder.htmlColumn(...)` útjára is igaz.
201
-
202
- **Üres ctx-szel semmi nem oldódik fel**: a `${…}` literál marad (és a `build()` elbukik rajta), a
203
- `data-sb4-context` elemek eltűnnek. A grid-szintű sor-layoutnak és az oszlop-sablonnak nincs
204
- szerver-oldali, soronkénti kontextusa — ott a sorból jövő érték `{{…}}`, a szerveren számolt érték
205
- pedig a sor egy mezője legyen.
206
-
207
- ### Template-kulcs egy tárolt layoutban
208
-
209
- Egy elmentett layout (`ObjectLayoutDescriptor`) nem a page kódjában épül, ezért nem tud
210
- `template(ctx, KEY)`-t hívni. Ott a widget csak **megnevezi** a kulcsot a
211
- `HtmlProperties.templateKey` mezőben, és a szerver tölti be, amikor a layout a view-ra kerül:
212
-
213
- ```java
214
- // kódból: f.html(h -> h.root("contact").templateKey("CONTACT_CARD"))
215
- // layout DSL: html card { template: CONTACT_CARD, root: contact }
216
-
217
- view.putComponentLayoutsItem(Layouts.DEFAULT_LAYOUT, // PageApiImpl
218
- resolvedCopy(ctx, view.getViewName(), descriptor.getLayouts().get(Layouts.DEFAULT_LAYOUT)));
219
- ```
220
-
221
- - **Tárolt layoutot a `resolvedCopy` visz a view-ra**: mély másolatot készít, és azon futtatja a
222
- `resolveHtml`-t. A `resolveHtml` helyben ír, a descriptor példánya pedig lehet cache-elt — a
223
- másolat nélkül a betöltött html a descriptorba kerülne. Page-en kívül:
224
- `ObjectLayouts.resolvedCopy(layout, resolver, objectApi, ctx, viewName)`. Kódból épített
225
- layoutra (ahol nincs mit megőrizni) elég a `resolveHtml(ctx, viewName, layout)`.
226
- - **Ugyanazok a szabályok, mint a `template(ctx, KEY)`-nél**: a html a legspecifikusabb névről jön
227
- (`<viewName>.<KEY>` → `<KEY>` → …), a stíluslapok mind összefűződnek (fájl, aztán MDM), és
228
- lecserélik a widget saját `css`-ét, ha van belőlük. A betöltött html ugyanazt az ellenőrzést
229
- kapja, mint a `build()`-ben: a kliens-helyőrzők nyelvtana és az ikon-szótár kulcsai.
230
- - **A `viewName` kötelező a page-en.** A kulcsos layoutra hívott `resolveHtml(ctx, layout)`
231
- kivételt dob: név nélkül ugyanaz a kulcs más template-et adhatna, mint a view-n épített widgeté.
232
- Aki szándékosan nem akar nézetenkénti template-et, `null` nevet ad át.
233
- - **A kulcs nyer, és a widgetben marad.** A mellette tárolt `html`-t a betöltött felülírja, és a
234
- kulcsos widget **minden futásnál újratöltődik** — rá tehát nem igaz, hogy a második futás nem
235
- csinál semmit: az MDM-ben módosított template a következő futásnál megjelenik. A layout DSL
236
- ezért hibát jelez, ha egy widgeten `template` és `html` is van.
237
- - **A field-lista: a megadott és a betöltött uniója.** A betöltött html mezői hozzáadódnak ahhoz,
238
- amit a widget maga megadott (`fields`). Amíg a layout nincs feloldva, a kulcsos widgetnek csak a
239
- megadott mezői vannak — ha a layout modelljét feloldás előtt kell belőle felépíteni
240
- (`constructSimpleModel`, object editing), add meg őket kézzel.
241
- - **Hangosan bukik**, ha nincs template-modul, ha a kulcs egyik neve sem létezik, ha a betöltött
242
- html-ben a ctx-szel sem oldódott fel egy `${alias:/`, vagy ha a html nem megy át az
243
- ellenőrzésen.
244
- - **Aminek a layoutján nem futott feloldás, az üresen megy ki.** A kliens ilyenkor egy
245
- konzol-figyelmeztetést ír a kulcs nevével; a `GridBuilder.rowLayout(...)` már a szerveren
246
- kivételt dob a betöltetlen kulcsra.
247
- - **Egy helyben újra feloldott layout csak bővül.** Ha ugyanazon a példányon fut újra a feloldás,
248
- és a template közben mezőt vagy stíluslapot veszített, a régi mező a listában, a régi `css` a
249
- widgeten marad. A `resolvedCopy` minden alkalommal a tárolt állapotból indul, ott ez nem fordul
250
- elő.
251
-
252
- A platform saját útjai, amelyek tárolt layoutot tesznek view-ra, mind a `resolvedCopy`-n mennek át,
253
- és **mindegyik megnevezi az adatát** — üres ctx-szel feloldó út nincs, mert az addig „működik", amíg
254
- az első template adatot nem olvas. A view modellje minden ilyen úton ott van `model` néven
255
- (`ObjectLayouts.CONTEXT_MODEL`): egy MDM-be feltett template `${model:/#name}`-mel olvassa.
256
-
257
- | Út | Ctx | Megjegyzés |
258
- |---|---|---|
259
- | `ObjectLayoutDescriptorUtilApi.initLayoutDescriptor(view, descriptor, ctx)` | a hívóé + `model` | a `DataSheetPageApiImpl` ezt hívja, a view modelljével `dataSheet` néven is: `${dataSheet:/#name}` |
260
- | `…initDescriptorState(view, schema, descriptor)` | — | **nem old fel**: csak a descriptor constraintjeit, value setjeit és fájl-widgetjeit teszi a view-ra; annak a page-nek, amely a layoutot maga építette és oldotta fel |
261
- | MDM bejegyzés-szerkesztő (`MDMEntryListPageApiImpl`) | az entry `model` néven | az `MDM_VIEW.<név>` descriptor layoutja |
262
- | Object navigation (`NavigationNodeLayoutInternalApi`) | a node adata `node` néven | a view nevével, a body építésekor; a `setView` már nem old fel újra, a descriptor feloldott body-ját teszi ki |
263
-
264
- Az `ObjectLayoutApi.getObjectDisplay` **nem** old fel: aki annak a layoutját, vagy közvetlenül a
265
- `descriptor.getLayouts()`-ét teszi a view-ba, a `resolvedCopy`-t maga hívja.
266
-
267
- ## A kliens-helyőrzők nyelvtana
268
-
269
- Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
270
- `HtmlTemplateScanner` is, ezért a hibás template már a page felépítésekor elbukik.
271
-
272
- ### Tag és path
273
-
274
- ```
275
- {{path}} érték
276
- {{path | pipe:'arg' | pipe2}} érték formázó pipe-okkal
277
- {{#path}} … {{/path}} belépő szekció
278
- {{?path}} … {{/path}} őr: van érték
279
- {{^path}} … {{/path}} őr: nincs érték
280
- {{.}} / {{. | pipe}} az aktuális scope maga
281
- {{../path}} path a körülvevő belépő szekció scope-jából
282
- ```
283
-
284
- - A tagen belül a szóköz megengedett (`{{ name }}`, `{{# parties }}`).
285
- - **Path**: `(../)*azonosító(.azonosító)*`, az azonosító `[A-Za-z_][A-Za-z0-9_]*`. **Nincs** index
286
- (`list[0]`), abszolút út, `{{..}}` és a path közepén álló `../` (`a/../b`).
287
- - Helyőrző text-node-ban és attribútum-**értékben** állhat (`class="status-{{status}}"`); tag- vagy
288
- attribútum-névben nem.
289
- - **Nincs** raw (`{{{x}}}`, `{{&x}}` → hiba), partial, komment, delimiter-váltás, `@index`.
290
- - Literál `{{` a html-ben: `&#123;&#123;`.
291
-
292
- ### Értékek
293
-
294
- - Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
295
- - Primitív → `String(v)` (`false` → `false`, `0` → `0`).
296
- - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`. Egyetlen kivétel: egy **ikon-slot**
297
- név-attribútumának teljes értékeként az objektum az ikon `ImageResource`-a lesz — lásd
298
- *Ikon-slot*.
299
- - Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
300
- nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
301
-
302
- ### Szekciók — a jel dönt, nem az érték
303
-
304
- | Jel | Név | Mit csinál |
305
- |---|---|---|
306
- | `{{#p}}` | **belépő** szekció | lista: elemenként egyszer, a scope az elem; objektum: egyszer, a scope az objektum |
307
- | `{{?p}}` | **őr** | egyszer renderel, ha van érték; **a scope nem mozdul** |
308
- | `{{^p}}` | hamis-őr | egyszer renderel, ha nincs érték; a scope nem mozdul |
309
-
310
- - **„Nincs érték"** = hiányzó, `null`, `false`, `""`, üres lista. **A `0` érték** (szándékos eltérés
311
- a Mustache-től: `{{?count}}` a nullát ne rejtse el).
312
- - **Szigorú scope**: belépő szekción belül a path csak az aktuális elemben oldódik, a szülő mezőit
313
- magától nem éri el (nincs Mustache-féle felfelé keresés — attól a szerver mezőlistája az adattól
314
- függne). A körülvevő scope **kimondva** érhető el: `{{../path}}`.
315
- - **`../`**: minden `../` egy **belépő** szekcióval lép kijjebb. A `?` és `^` őr nem szint (a scope-ot
316
- sem mozdítja), átlát rajta. Érték-tagen, pipe-pal, attribútum-értékben és szekció-tagen is állhat
317
- (`{{#../members}}…{{/../members}}` — a záró tag ugyanazt a pathot ismétli). A widget `root`-ja
318
- fölé nem megy: több `../`, mint ahány belépő szekció nyitva van, hiba.
319
- - Szekció-tagen nincs pipe.
320
- - `{{#p}}` egyetlen értéken (szám, string): a kliens őrként rendereli és `console.warn`-t ír — a
321
- Java-builder ezt nem látja előre (nincs adata), ilyenkor `{{?p}}` kell. A `../`-nek ez is egy
322
- szint (a scope-ja a körülvevő), így a kliens és a szerver mezőlistája egyezik.
323
- - A szekció-tag üres stringre cserélődik, a körülötte lévő whitespace marad.
324
-
325
- ```html
326
- <ul>
327
- {{#parties}}
328
- <li>{{displayName}} {{#roles}}<span>{{. | label:'BusinessRole'}}</span>{{/roles}}</li>
329
- {{/parties}}
330
- </ul>
331
- {{^parties}}<p>— nincs érintett —</p>{{/parties}}
332
- {{?note}}<p class="note">{{note}}</p>{{/note}}
333
- ```
334
-
335
- A kártya saját mezői a lista elemein belül, `root = "card"` mellett:
336
-
337
- ```html
338
- <ul>
339
- {{#parties}}
340
- <li data-sb4-action="REMOVE_PARTY" data-sb4-action-identifier="{{../id}}_{{id}}">
341
- {{displayName}} — {{../title}}
342
- {{#roles}}<span>{{. | label:'BusinessRole'}} ({{../displayName}})</span>{{/roles}}
343
- </li>
344
- {{/parties}}
345
- </ul>
346
- ```
347
-
348
- A származtatott mezők: `card.parties`, `card.id`, `card.parties.id`, `card.parties.displayName`,
349
- `card.title`, `card.parties.roles` — a `{{../displayName}}` a `roles`-on belülről a
350
- `card.parties.displayName`, a `{{../title}}` a `card.title`.
351
-
352
- ### Formázó pipe-ok
353
-
354
- `| név` vagy `| név:'arg'`; az argumentum egyetlen, **egyszeres idézőjelű** literál (`\'` és `\\`
355
- escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en megy át. A készlet
356
- **zárt**:
357
-
358
- | Pipe | Argumentum | Default | Szemantika |
359
- |---|---|---|---|
360
- | `date` | date-fns formátum | `'yyyy.MM.dd'` | ISO string → böngésző-időzóna szerinti dátum |
361
- | `datetime` | date-fns formátum | `'yyyy.MM.dd H:mm'` | ugyanaz |
362
- | `time` | date-fns formátum | `'H:mm'` | ugyanaz; a csak-idő (`12:34:56`) értéket is olvassa |
363
- | `number` | Angular `digitsInfo` | `'1.0-3'` | `Intl.NumberFormat` a session locale-jával; szám és numerikus string |
364
- | `label` | a valueSet neve (**kötelező**) | — | kód → címke a kontextus `valueSets`-éből |
365
- | `default` | literál | `''` | üres / hiányzó érték helyett a literál |
366
- | `upper` / `lower` | nincs | — | kis/nagybetű a session locale-jával |
367
-
368
- - A defaultok a grid cella-defaultjai: a kártya és a tábla ugyanazt mutatja.
369
- - **Dátum**: a dróton nem minden dátum zulu (`…Z` / offset, offset nélküli `LocalDateTime`,
370
- `LocalDate`, `LocalTime`) — mindet olvassa. Nem ISO érték → nyers érték + warn.
371
- **A date-fns `YYYY` / `DD` tokenje hibás**: `yyyy` / `dd` kell.
372
- - **`label`**: a kulcs-mező `valueSetData.keyProperty`, különben `objectUri`, különben `uri`; a címke
373
- `displayValue`, különben `name` — ugyanaz a lánc, mint a form select-jeinél. Nincs valueSet vagy
374
- nincs találat → a nyers kód. A valueSetnek az oldal `ComponentModel.valueSets`-ében kell lennie.
375
- - Üres értéket a `default` kivételével minden pipe érintetlenül enged tovább.
376
-
377
- ### Hibák
378
-
379
- | Hol | Mi történik |
380
- |---|---|
381
- | 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` /
382
- `data-sb4-icon` / `data-sb4-icon-key`, `data-sb4-icon` és `data-sb4-icon-key` ugyanazon az elemen |
383
- | Java-builder (`build()`) | `IllegalStateException`: a sablonban álló literál ikon-kulcs, amit az ikon-szótár nem tartalmaz |
384
- | Java-builder (`build()`), `resolveHtml` | `IllegalStateException`: feloldatlan szerver-helyőrző (`${alias:/`), lásd *A szerver-fázis* |
385
- | kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
386
-
387
- Egyetlen szándékos különbség: az ismeretlen pipe-név a kliensen csak warn (az érték formázatlanul
388
- megy ki), hogy egy régebbi kliens ne törjön el egy újabb szerver pipe-ján.
389
-
390
- ### Szerzői szabályok a szerver-fázis miatt
391
-
392
- Ha a html átmegy a szerver template-motorján (Jsoup), akkor:
393
-
394
- - **egy gyökérelem kötelező** — a gyökér-szintű szöveg és szekció-tag eldobódik;
395
- - táblázatban **explicit `<tbody>`** kell, különben a parser a `{{#rows}}` után szúr egyet;
396
- - a whitespace-re (`:empty`, `white-space: pre`, `&nbsp;`) ne építs, a motor átírja;
397
- - a szerverről behelyettesített **érték ne tartalmazzon `{{`-t**: a motor HTML-escape-eli, de a
398
- `{{` átmegy rajta, és a kliens helyőrzőnek olvasná (a néző saját adatán oldaná fel);
399
- - `?stratégia` után is állhat `{{…}}` — a stratégia a `{{`-on nem nyúl át.
400
-
401
- ## Toolbar-slot
402
-
403
- ```html
404
- <smart-ui-action-toolbar data-sb4-toolbar="ROW_TOOLBAR"
405
- data-sb4-toolbar-direction="HORIZONTAL" data-sb4-toolbar-alignment="END"
406
- data-sb4-toolbar-scrollable></smart-ui-action-toolbar>
407
- ```
408
-
409
- A kliens a marker-elembe egy **valódi toolbart hidratál**, amely a `data-sb4-toolbar` alatti
410
- címre (`uiAction.toolbar == id`) címzett akciókat rajzolja ki — ugyanazokkal a szabályokkal, mint
411
- bármely más toolbar: a listát és a végrehajtót a környezetéből húzza (kártyán a sor akcióit,
412
- nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarProperties)`.
413
-
414
- - A marker **maga a `<smart-ui-action-toolbar>` tag** legyen: a hostok tag-szelektorral stílusozzák
415
- a toolbart, egy `<div data-sb4-toolbar>` működne, de a host-CSS nem érné el.
416
- - Üres tartalommal írd (a hidratálás úgyis kitörli); a saját `class` / `style` attribútumaid
417
- megmaradnak.
418
- - `direction`: `HORIZONTAL` | `VERTICAL`, `alignment`: `START` | `END`, a `scrollable` logikai
419
- attribútum. Ismeretlen érték → warn, a tulajdonság kimarad.
420
- - A helyőrzők feloldása **megelőzi** a hidratálást, tehát az id is lehet sablon:
421
- `data-sb4-toolbar="addr_{{id}}_toolbar"` — szekcióban elemenként más toolbar.
422
- - Üres vagy üresre feloldott id → warn, a slot kimarad. Ismétlődő marker → markerenként saját
423
- toolbar.
424
- - A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
425
- eredménynél a hidratált toolbarok élnek tovább.
426
-
427
- ## Ikon-slot
428
-
429
- ```html
430
- <smart-icon data-sb4-icon="mail" data-sb4-icon-color="warn"></smart-icon>
431
- <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
432
- <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
433
- ```
434
-
435
- A kliens a marker-elembe egy **valódi `smart-icon`-t hidratál**, ugyanazt a komponenst, amit a
436
- gomb, a grid-cella és a form használ. Java: `Tag.icon(name)`, `Tag.icon(name, color)`,
437
- `Tag.icon(imageResource)`, `Tag.iconKey(key)`, a szótár a widget-builderen: `h.icon(key, resource)`
438
- / `h.icons(map)`.
439
-
440
- Az ikon címe négyféle lehet, és mindegyiknek megvan a maga esete:
441
-
442
- | Forma | Mit ad az ikonnak | Mikor |
443
- |---|---|---|
444
- | `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** |
445
- | `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) |
446
- | `data-sb4-icon="{…}"` (JSON) | `imageResource` input | a page által **egyszer** feloldott erőforrás (`Tag.icon(resource)`) |
447
- | `data-sb4-icon-key="{{type}}"` | `imageResource` input a **szótárból** | véges típus → ikon leképezés |
448
-
449
- - **`data-sb4-icon-color`**: a `color` input, vagyis téma- vagy host-osztálynév (`primary`,
450
- `text-primary`, `warn` …), **nem** CSS-szín. Csak a név-formához való: az `ImageResource` a saját
451
- `color`-ját hozza. Elhagyva a komponens alapértelmezése (`primary`) marad.
452
- - **Objektum csak ott**, ahol az attribútum értéke pontosan egy helyőrző. Szöveggel keverve
453
- (`data-sb4-icon="ic-{{typeIcon}}"`), pipe-pal vagy listán az érték a nyelvtan általános szabálya
454
- szerint üres marad, warninggal. A feloldó az objektumot egy **renderelésenként újrahúzott,
455
- véletlen tokennel** adja át a hidratálásnak, hogy adatból ne lehessen másik ikon erőforrására
456
- hivatkozni.
457
- - **Üres cím** (nincs ikonja ennek a sornak) → néma kihagyás, az elem üresen marad. A sablonban
458
- **szó szerint üres** marker (`data-sb4-icon=""`) viszont `build()`-hiba, ahogy a toolbarnál.
459
- - **Ismeretlen szótárkulcs** → nincs ikon, egy warning. A sablonban álló **literál** kulcsot, ami
460
- nincs a szótárban, már a `build()` elutasítja.
461
- - Egy elemen **nem állhat** a `data-sb4-icon` és a `data-sb4-icon-key` egyszerre: `build()`-hiba, a
462
- kliens a kulcsot használja és warningot ír.
463
- - **A jelölő bármely elemen állhat** (`<span class="badge" data-sb4-icon="mail"></span>`), nem csak
464
- `<smart-icon>`-on: a kliens attribútum szerint keres, és a komponenst magába a jelölő-elembe
465
- rendereli. A `Tag.icon(...)` azért mégis `<smart-icon>` taget generál, mert a hostok tag-szelektorral
466
- szokták stílusozni az ikont — saját elemnél erre a szelektorra ne számíts.
467
- - **Üres tartalommal írd**, mint a toolbar-slotot: a jelölő-elem gyerekei helyére a komponens
468
- kerül. A saját `class`, `style` és `title` attribútuma megmarad, a méretezés a widget CSS-éből
469
- megy (`.card-icon mat-icon { font-size: … }`). Ha szöveg és ikon is kell egy elemben — például egy
470
- badge-ben —, az ikon **gyerek**-slot legyen:
471
- `<span class="pill"><smart-icon data-sb4-icon="bolt"></smart-icon>count 0</span>`.
472
- - Az ikon a nevét a regiszterben keresi meg, ezért **egy microtaskkal később** jelenik meg — a
473
- kliens ettől még ugyanabban a renderelésben írja ki a html-t.
474
-
475
- ### Példa: típus szerinti ikon egy ImageResource-ból
476
-
477
- A szerveren az `ImageSettingApi` képezi le a típust ikonra (`images-*.properties`):
478
-
479
- ```properties
480
- ContactType.EMAIL={"identifier":"mail","color":"accent","tooltip":{"tooltip":"E-mail cím"}}
481
- ContactType.PHONE={"identifier":"call","color":"primary","tooltip":{"tooltip":"Telefonszám"}}
482
- ```
483
-
484
- **Soronként más erőforrás** — az objektum az adatban utazik:
485
-
486
- ```java
487
- row.getData().put("typeIcon", imageSettingApi.get("ContactType", type.name()));
488
- ```
489
-
490
- ```html
491
- <div class="pg-row">
492
- <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
493
- <b>{{name}}</b>
494
- </div>
495
- ```
496
-
497
- **Véges leképezés** — az erőforrás egyszer utazik, a sor csak a kódot viszi:
498
-
499
- ```java
500
- .htmlColumn("type", h -> h
501
- .icon("EMAIL", imageSettingApi.get("ContactType", "EMAIL"))
502
- .icon("PHONE", imageSettingApi.get("ContactType", "PHONE"))
503
- .html("""
504
- <div class="pg-row">
505
- <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
506
- <b>{{name}}</b>
507
- </div>"""))
508
- ```
509
-
510
- Mindkettőből ugyanaz lesz a DOM-ban:
511
-
512
- ```html
513
- <smart-icon data-sb4-icon="…"><mat-icon class="mat-accent material-icons">mail</mat-icon></smart-icon>
514
- ```
515
-
516
- A szótár a **sablon** része (`HtmlProperties.icons`), nem a soré: a sima HTML widgetnél is egyszer
517
- 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
518
- `GridRow.icons`-szal, ami a **cella értéke mellé** rajzolt ikonokat írja le.
519
-
520
- ## Akció-trigger
521
-
522
- ```html
523
- <div class="card" data-sb4-action="OPEN_ENTRY">…</div>
524
- <li data-sb4-action="REMOVE_MEMBER" data-sb4-action-identifier="{{id}}">…</li>
525
- <div data-sb4-action="OPEN_MEMBER" data-sb4-action-identifier="{{id}}" data-sb4-action-direct>…</div>
526
- ```
527
-
528
- Bármely elem hordozhat triggert: a kattintás (vagy Enter / Space) azt az akciót futtatja, amelynek
529
- a `code`-ja — és ha meg van adva, az `identifier`-e — egyezik. Az akciót a kliens **kattintáskor**
530
- keresi meg ugyanabban a listában, amiből az ott lévő toolbarok húznak, **a toolbar-címtől
531
- függetlenül**. Hiányzó `data-sb4-action-identifier` = identifier nélküli akció. Java:
532
- `Tag.action(code)` / `Tag.action(code, identifier)`.
533
-
534
- - **Lista-szekcióban elemenként egy akció kell a szerveren.** A `{{id}}`-s identifier címez, nem
535
- paraméterez: a kliens a (kód, identifier) párt keresi a listában, tehát a page minden elemhez
536
- kínáljon egy akciót a megfelelő `identifier`-rel (kártyán a sor akciói közt, nézeten a view akciói
537
- közt). Egyetlen, identifier nélküli akció mellett az identifieres triggerek nem interaktívak.
538
- - **A gyökérelemre tett trigger a `DIV` widget `onClick`-jének megfelelője.** A HTML widgetnek
539
- nincs saját kattintása.
540
- - **Csak-trigger akció**: ha egy nézet-szintű akciót csak trigger lőhet, a címe legyen
541
- `UiActions.NO_TOOLBAR` (`"_none"`) — olyan cím, amit egyetlen toolbar sem visel, így az id nélküli
542
- toolbarok sem rajzolják ki gombként. Kártyán (sorakciónál) nem kell.
543
- - A trigger nem enged tovább se klikket, se dupla klikket a körülötte lévő elemeknek: kártyán nem
544
- jelöl ki és nem futtat default akciót. Beágyazott triggereknél a legbelső nyer.
545
- - `<a href>`-re tett triggernél az akció nyer, a link nem navigál.
546
- - **a11y**: nem natív elem (`div`, `span`, `li`…) `role="button"`-t és `tabindex="0"`-t kap, ha a
547
- szerző nem adott mást; natív elemhez (`button`, `a[href]`, `input`…) a kliens nem nyúl.
548
- - **Letiltott akció**: `aria-disabled="true"` + `sb4-action-disabled` osztály, a kattintás no-op; az
549
- állapot a lista változását a html újraírása nélkül követi.
550
- - **Nem kínált akció** (a sor / nézet nem adja): az elem nem interaktív, `sb4-action-inert` osztályt
551
- kap, a kliens egyszer warn-ol, a kattintás a körülötte lévő triggerre vagy a kártyára jut.
552
- - **Direkt trigger** (`data-sb4-action-direct`): akkor is lő, ha a lista nem kínálja az akciót —
553
- ilyenkor a kliens maga rakja össze a `{ code, identifier }` akciót az attribútumokból. Ha a lista
554
- kínál egyező akciót, az nyer (letiltás, descriptor a listából jön). Nem inert és nem warn-ol.
555
- Mivel a kínált-akció lista már nem kapuzza, **a szerver handlere maga ellenőrizze a
556
- jogosultságot**. Java: `Tag.directAction(code)` / `Tag.directAction(code, identifier)`.
557
- - Paraméter-attribútum nincs: az identifier és a sor-scope (`params.model` = a sor) fedi az
558
- eseteket.
559
-
560
- ## Sor-layoutok a kártya-griden
561
-
562
- A `GridViewDescriptor.rowLayouts` kulcsonként egy-egy layoutot visz **gridenként egyszer**; a
563
- kliens soronként rendereli a sor `data`-jával. Csak `kind == CARDS` mellett él.
564
-
565
- ```java
566
- gridBuilder.kind(KindEnum.CARDS)
567
- .rowLayout(layoutBuilder(view).vForm(f -> f.html(h -> h.html(CARD_HTML))).build()) // default
568
- .rowLayout("DISTRIBUTION_LIST", listLayout); // kulcsos
569
- ```
570
-
571
- Melyik layouttal renderel egy sor, sorrendben:
572
-
573
- 1. a sor saját `layoutDescriptor.componentLayouts.GRID_ROW_LAYOUT`-ja (soronkénti felülbírálás);
574
- 2. a `rowLayouts[row.rowLayout]`;
575
- 3. a **default sor-layout**: a `"default"` kulcs (`Layouts.DEFAULT_LAYOUT`), vagy ha csak egy
576
- bejegyzés van, az;
577
- 4. a host `<gridId>Card` néven regisztrált kártya-komponense;
578
- 5. semmi.
579
-
580
- Ismeretlen kulcs → a default + egy `console.warn` (grid, sor, kulcs): az elgépelt kulcs nem tüntet
581
- el kártyát, de látszik.
582
-
583
- **Oszlopok.** A kártyán olvasott mezőket a grid oszlopainak kell adniuk: a `GridBuilder.build()`
584
- WARN-t ír minden olyan mezőre, amit egy oszlop sem fed (név vagy dot-prefix szerint: a `card`
585
- oszlop fedi a `card.parties.displayName`-et). A mezőlista ellenőriz, nem választ oszlopot.
586
- Szerver-származtatott mező (pl. `partyCount`) legyen valódi oszlop.
587
-
588
- **Kattintás** (csak a layoutból renderelt kártyán; a host-regisztrált kártya maga kezeli a saját
589
- klikkjeit). Két független tengely, ugyanaz a modell, mint a táblán:
590
-
591
- - **a kijelölést a `selectionMode` adja** (hiányzó = `NONE` = nincs kijelölés);
592
- - **a default sor-akció gesztusát a `GridViewDescriptor.defaultRowActionTrigger`**
593
- (`click` | `doubleClick`), Java: `GridBuilder.defaultRowActionTrigger(...)`.
594
-
595
- | `defaultRowActionTrigger` | klikk | dupla klikk |
596
- |---|---|---|
597
- | nincs megadva (kártyán = `click`) | a sor **első** illő default akciója | — |
598
- | `click` | default akció; több illő akciónál **menü az egérnél** | — |
599
- | `doubleClick` | kijelölés, ha a grid és a sor jelölhető | default akció; több illőnél menü |
600
-
601
- - **A klikkes trigger elviszi a klikket a kijelölés elől**: ha a sornak van default akciója és a
602
- trigger `click` (vagy nincs megadva), a kártya klikkre nem jelölhető ki, akármi a
603
- `selectionMode`. Kártya-kijelöléshez `doubleClick` trigger kell, vagy default akció nélküli grid.
604
- - „Illő" default akció: a grid `defaultRowActions` kódjai közül az, amit a sor `actions`-e kínál —
605
- a menü tehát soronként más lehet. Megadott trigger mellett egy illő akció azonnal elsül.
606
- - A dupla klikk második klikkje nem számít külön klikknek: nem nyit kétszer, és nem vonja vissza
607
- az első klikk kijelölését (dupla klikk = kijelöl + megnyit). Amíg a default akció fut, az újabb
608
- klikk is eldobódik.
609
- - A kijelölt kártya hostja `selected` osztályt és egy minimális, felülírható outline-t kap. A
610
- kijelölés toggle, mint a táblán; `selectionType: CHECKBOX` a kártyán nem rajzol checkboxot.
611
- - A toolbar-gomb és a trigger megtartja magának a kattintást.
612
- - A tábla gesztusa változatlanul a dupla klikk, a mező értékétől függetlenül.
613
-
614
- ## Html a tábla cellájában
615
-
616
- A tábla egy cellája kétféleképpen kaphat html-t, és a kettő **más bizalmi szinten** áll.
617
-
618
- | | HTML column | Column template |
619
- |---|---|---|
620
- | Honnan jön a html | a cella **értéke**, adatként (`GridRow.data`) | az oszlop **sablonja** (`GridColumnMeta.htmlProperties`) |
621
- | Ki írta | bármi, ami az adatot előállítja | a backend, mint a HTML widgetnél |
622
- | Kiírás | **sanitizált** innerHTML (Angular default) | változtatás nélkül, a behelyettesített értékek escape-elve |
623
- | Helyőrzők, toolbar-slot, trigger | nincs (a `data-*` és az ismeretlen tag kiesik) | mind, mint a widgetben |
624
-
625
- ### HTML column és `contentType`
626
-
627
- A `GridColumnMeta.contentType` mondja meg, hogyan kerül ki az érték:
628
-
629
- - **hiányzó** vagy `html` — sanitizált innerHTML, ahogy a tábla mindig is írta. Semmi nem változik
630
- annak a hostnak, amelyik nem állítja.
631
- - `text` — **escape-elt** szöveg: a `<b>` betűkként látszik. Erre való minden adat, amiben
632
- felhasználói szöveg van és nem jelölésként kell olvasni.
633
-
634
- A rendezés a szerveren a **nyers** értéken fut, ezért html-oszlophoz adj
635
- `sortOrderPropertyName`-et. Java:
636
-
637
- ```java
638
- gridBuilder.contentType(GridColumnContentType.TEXT, "name", "comment")
639
- ```
640
-
641
- ### Column template
642
-
643
- ```java
644
- gridBuilder.htmlColumn("name", h -> h.html(t -> t
645
- .b(b -> b.text("{{name}}"))
646
- .span(s -> s.action("OPEN").text("megnyitás"))
647
- .toolbar("ROW_TOOLBAR")))
648
- ```
649
-
650
- - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, `icons`, és
651
- ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
652
- `valueSets` a gridet mutató oldalé.
653
- - A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
654
- checkbox- és ikon-formázást is — a formázás a sablon pipe-jaiba kerül. A sor ikonjai
655
- (`GridRow.icons`) és az oszlop cella-toolbarja (`columnActions`) mellette megmaradnak. Ha van
656
- sablon, a `contentType` nem számít.
657
- - A toolbar-slot és a trigger **a sor akcióit** látja (`row.actions`), pontosan úgy, mint a
658
- sor-layouttal renderelt kártya: az akció modellje a sor, a scope a grid és a sor. A trigger a
659
- kattintást és a dupla kattintást megtartja magának, a sor-klikk nem fut.
660
- - **Sor-menü**: a csak-trigger akció (`UiActions.NO_TOOLBAR`) sosem kerül a sor menüjébe. Ha a
661
- táblában van oszlop-sablon, akkor a **toolbar-címes** sorakció sem — az egy sablonbeli slot-é.
662
- Sablon nélküli táblán a címes sorakció a menüben marad, ahogy eddig.
663
- - A cella a sor minden cseréjekor újra felold; változatlan eredménynél a html és a hidratált
664
- toolbar él tovább. Teljesítmény-kalap nincs: nagy táblán a sablon soronként egyszer oldódik fel,
665
- hidratálás csak ott fut, ahol slot van.
666
- - A `GridBuilder.build()` a sablon mezőit is ellenőrzi az oszlopokkal szemben, ugyanazzal a
667
- WARN-nal, mint a sor-layoutét (`column '<név>' reads '<mező>' which no column covers`). Az
668
- ismeretlen oszlopra adott sablon vagy `contentType` szintén WARN.
669
- - **Kulcsból**: `gridBuilder.htmlColumn("name", h -> h.template(objectApi.contextObject(), KEY))`
670
- — a page `gridBuilder(...)`-e átadja a template-feloldót, a sablon és a stíluslapja a kulcsról
671
- jön, mint a widgetnél. Soronkénti szerver-kontextus itt sincs, a sablonban `{{…}}` legyen. A
672
- halasztott `templateKey(KEY)` oszlopon nem megy (nincs layout, amin a `resolveHtml` futna), a
673
- builder kivételt dob rá. Sor-layoutnál mindkettő működik: `template(ctx, KEY)` építéskor, vagy
674
- `templateKey(KEY)` és `resolveHtml(ctx, viewName, rowLayout)` a `rowLayout(...)` hívás előtt.
675
-
676
- ## Példa: egy kártya
677
-
678
- ```html
679
- <div class="contact-card contact-card-{{status}}" data-sb4-action="OPEN_CONTACT_ENTRY">
680
- <div class="contact-card-main">
681
- <div class="contact-card-icon contact-card-icon-{{channelType}}"></div>
682
- <div class="contact-card-value">{{value}}</div>
683
- <div class="contact-card-channel-type">{{channelType | label:'ContactChannelType'}}</div>
684
- <div class="contact-card-modified">{{modifiedAt | datetime}}</div>
685
- <smart-ui-action-toolbar data-sb4-toolbar="CONTACT_ROW_TOOLBAR"></smart-ui-action-toolbar>
686
- </div>
687
- <div class="contact-card-associations">
688
- <div class="contact-card-associations-title">Érintettek ({{partyCount}})</div>
689
- {{#parties}}
690
- <div class="contact-card-related-party">
691
- <div class="contact-card-related-party-initials">{{monogram}}</div>
692
- <div class="contact-card-related-party-company">{{displayName}}</div>
693
- <div>{{#businessRoles}}<span>{{. | label:'BusinessRole'}}</span> {{/businessRoles}}</div>
694
- </div>
695
- {{/parties}}
696
- {{^parties}}<div class="contact-card-associations-title">— nincs érintett —</div>{{/parties}}
697
- </div>
698
- </div>
699
- ```
700
-
701
- `root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
702
- szándékosan nem tud, annak a helye:
703
-
704
- - **ikon**: ikon-slot (`data-sb4-icon` / `data-sb4-icon-key`), a leképezés az `ImageSettingApi`-ban;
705
- értékfüggő megjelenés egyébként: szerver-származtatott mező, vagy érték-alapú CSS-osztály
706
- (`contact-card-icon-{{channelType}}`);
707
- - **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
708
- logikai mező + `{{?…}}` őr;
709
- - **darabszám** (`parties.size()`): valódi oszlop (`partyCount`);
710
- - **fordítás**: `label` pipe valueSetből, vagy a szerver-fázis `${…}`-e.
1
+ # HTML widget — szerverről küldött html a `@smartbit4all/ng-client` 7.2-ben
2
+
3
+ Ez a dokumentum a **HTML widget**, a **kliens-helyőrzők** (`{{…}}`), a html-be írt **toolbar-slot**
4
+ és **akció-trigger**, a kártya-grid **sor-layoutjainak**, valamint a tábla **html-celláinak**
5
+ referenciája. A Java-oldali builder (`HtmlWidgetBuilder`, `HtmlBuilder.Tag.toolbar/action`,
6
+ `GridBuilder.rowLayout/htmlColumn/contentType`) javadocja
7
+ ide hivatkozik; ha a kettő ellentmond egymásnak, ez a fájl az igazság.
8
+
9
+ ## Mire való
10
+
11
+ A backend egy `HTML` típusú form-widgetet tehet a layoutba, amelynek a tartalma egy darab html. A
12
+ html-t **két fázisban** oldjuk fel, két külön jelöléssel:
13
+
14
+ | Fázis | Jelölés | Ki oldja fel | Mire jó |
15
+ |---|---|---|---|
16
+ | szerver-helyőrző | `${…}` | a backend, a page-ben, mielőtt a html elhagyja | feltétel, iteráció, szkript, lokalizáció — a platform meglévő template-dialektusa |
17
+ | kliens-helyőrző | `{{…}}` | a kliens, a widget **feloldási kontextusán** | ugyanaz a html egyszer utazik és soronként / modell-változásonként újra feloldódik |
18
+
19
+ A kliens a szerver-fázisból semmit nem lát: mire a html megérkezik, a `${…}` már nincs benne.
20
+
21
+ **Bizalmi határ.** A html-t a backend írta, ezért a kliens **változtatás nélkül** írja ki (nincs
22
+ sanitizálás: `data-*`, `style`, `<svg>`, inline `on*` mind él). Amit viszont a kliens helyettesít be
23
+ egy `{{…}}` helyére, az **mindig HTML-escape-en** megy át, kivétel és „raw" forma nélkül. Ha a host
24
+ Trusted Types-ot használ, a html kiírása miatt engedélyeznie kell a megfelelő policy-t. A widget
25
+ stíluslapja (`css`, lásd *Stíluslap*) ugyanígy, változatlanul kerül a dokumentumba.
26
+
27
+ A `DIV` widget változatlanul megmarad; a HTML widget annak a használatnak a rendes helye, amikor a
28
+ `DIV`-be kész html-t tettünk.
29
+
30
+ ## A widget a dróton
31
+
32
+ `SmartWidgetDefinition.type = HTML`, a tulajdonságai a `properties["HTML_properties"]` alatt
33
+ (`HtmlProperties`):
34
+
35
+ | Mező | Jelentés |
36
+ |---|---|
37
+ | `html` | a html, a `${…}` már feloldva, a `{{…}}` benne hagyva |
38
+ | `root` | dot-path a kontextus `data`-jához képest, amihez minden helyőrző relatív; **objektumot** nevez; üres = a `data` maga |
39
+ | `fields` | a widget által olvasott abszolút pathok — a builder származtatja a helyőrzőkből, a kliens **nem** olvassa |
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* |
42
+
43
+ A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
44
+ (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
45
+ semmit nevez, minden helyőrző üres marad, és a kliens egyszer `console.warn`-t ír.
46
+
47
+ A widgetnek nincs kontrollja, címkéje, validálása és saját kattintása sem (lásd *Akció-trigger*).
48
+
49
+ Java-oldalon:
50
+
51
+ ```java
52
+ layoutBuilder(view).vForm(f -> f
53
+ .html(h -> h.root("card").html("<div class=\"card\">{{name}}</div>")))
54
+ ```
55
+
56
+ vagy template-kulcsból (`h.template(ctx, "CONTACT_ENTRY_LIST")`): a kulcs a szerveren oldódik fel,
57
+ a dróton nem utazik.
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
+
73
+ ## Stíluslap
74
+
75
+ A widget a kinézetét is hozhatja: a `css` mezőben egy CSS-szöveget, amit a kliens a dokumentum
76
+ `<head>`-jébe tesz (`<style data-sb4-html-css>`). Így a widget teljes egészében a szerverről jön, a
77
+ hostnak nem kell hozzá stíluslapot szállítania.
78
+
79
+ ```java
80
+ vf.html(h -> h.root("card")
81
+ .html("<div class=\"contact-card\">{{name}}</div>")
82
+ .css(".contact-card { padding: 1rem; border-radius: 12px; }"))
83
+ ```
84
+
85
+ - **Egyszer kerül ki**, akárhány widget, kártya vagy cella hozza ugyanazt a szöveget, és az
86
+ utolsó eltűnésével a kliens kiveszi. A sor-layout és az oszlop-sablon is így működik: a `css` a
87
+ layout, illetve a sablon része, nem soronként utazik.
88
+ - **Globális, nincs hatóköre.** Minden szelektor a html gyökérelemének egy osztályával kezdődjön
89
+ (`.contact-card …`), különben az egész oldalra hat. Két különböző szöveg ugyanarra a
90
+ szelektorra a betöltés sorrendjében nyer: adj widgetenként saját előtagot.
91
+ - **Nem oldódik fel**: a `{{…}}`-nak nincs benne jelentése, modell-értéket nem lehet belevinni. Ami
92
+ a modelltől függ, az a html-ben legyen osztály (`class="status-{{status}}"`), a CSS pedig
93
+ osztályonként szabályozzon.
94
+ - **CSP**: ha a host `style-src`-je nonce-os, adja meg az Angular `CSP_NONCE` tokenjét, a kliens
95
+ ráteszi a `<style>`-ra. Nonce és `'unsafe-inline'` nélkül a böngésző a stíluslapot blokkolja.
96
+ - A builder nem vizsgálja és nem formázza: a `%` és a `{{` sem zavarja.
97
+
98
+ ### Stíluslap a template-kulcs mellől
99
+
100
+ A `template(ctx, KEY)` a html mellé a stíluslapot is behúzza, ugyanazokon a neveken egy `.css`
101
+ szegmenssel: `<viewName>.<KEY>.css`, `<KEY>.css`, … egészen a kulcs utolsó szegmenséig (a puszta
102
+ `css` név sosem jön szóba). A fájl-registryben ez egy sor, pl.
103
+ `CONTACT_ENTRY_LIST.css=templates/contact_entry_list.css`; MDM-ben egy ugyanilyen nevű
104
+ `text/css` template.
105
+
106
+ A sor-layout lánca a grid azonosítójával kezdődik (lásd *Template-kulcs egy tárolt layoutban*):
107
+ `<gridId>.<viewName>.<KEY>` → `<viewName>.<KEY>` → `<KEY>` → a kulcs suffixei, lépésenként
108
+ elölről egy szegmenst elhagyva; a `<gridId>.<KEY>` sosem név, a form-widget lánca marad
109
+ `<viewName>.<KEY>`. A stíluslap ott is a lánc nevei mellett álló összes létező `.css`, előbb a
110
+ fájlok, aztán az MDM, az általánostól a specifikusig.
111
+
112
+ A html-lel ellentétben itt **nem egy nyer, hanem mind összefűződik**: előbb a fájlos stíluslapok,
113
+ aztán az MDM-esek, mindkét rétegen belül az általánostól a specifikusig:
114
+
115
+ ```
116
+ <KEY>.css (fájl) → <viewName>.<KEY>.css (fájl) → <KEY>.css (MDM) → <viewName>.<KEY>.css (MDM)
117
+ ```
118
+
119
+ Mivel egyenlő specificitásnál a később jövő szabály nyer, az MDM a kódban szállított kinézetet
120
+ szabályonként felülírhatja (ugyanazzal a szelektorral) vagy kiegészítheti (új szabállyal). A
121
+ fájlban lévő `!important`-ot csak `!important` írja felül. A template-ből jött stíluslap lecseréli
122
+ az addigi `css(...)`-t, egy későbbi `css(...)` pedig őt; ha a kulcshoz nincs stíluslap, az addigi
123
+ `css` marad. A stíluslap nem értékelődik ki.
124
+
125
+ ### Belépő és kilépő animáció
126
+
127
+ A stíluslap (a widgeté vagy a hosté) animálhatja a widget és a cella tartalmának érkezését és
128
+ távozását:
129
+
130
+ ```css
131
+ .contact-card { animation: contact-card-enter 300ms ease-out backwards; }
132
+ .sb4-html-leaving .contact-card { animation: contact-card-leave 150ms ease-in forwards; }
133
+
134
+ @keyframes contact-card-enter { from { opacity: 0; transform: translateY(12px); } }
135
+ @keyframes contact-card-leave { to { opacity: 0; transform: translateY(-8px); } }
136
+
137
+ @media (prefers-reduced-motion: reduce) {
138
+ .contact-card, .sb4-html-leaving .contact-card { animation: none; }
139
+ }
140
+ ```
141
+
142
+ - **Belépés**: a kiírt html új elemei a szokásos módon animálódnak. Ez minden olyan renderkor
143
+ lefut, amely **új html-t ír**: az első megjelenéskor, és valahányszor a feloldás eredménye
144
+ megváltozik (a modell vagy a sor olyan mezője változott, amit a html olvas). A változatlan
145
+ eredményű push semmit nem ír, ott nem fut.
146
+ - **Kilépés**: mielőtt a kliens a képernyőn lévő html-t lecseréli, a widget, illetve a cella
147
+ elemére (`smart-html-widget`, `smart-html-cell`) felteszi az **`sb4-html-leaving`** osztályt.
148
+ Ha egy stíluslap erre az osztályra animációt vagy transitiont indít, a kliens megvárja a
149
+ végét, és csak utána írja ki az új html-t (és veszi le az osztályt). Ha semmi nem indul, azonnal
150
+ ír: kilépő CSS nélkül minden úgy működik, mint eddig.
151
+ - A várakozás **legfeljebb 600 ms**: egy végtelen vagy túl hosszú kilépő animáció sem tartja fel
152
+ a frissítést. Csak az osztály által **elindított** animáció számít; ami már futott (pl. egy még
153
+ tartó belépő vagy egy végtelen pörgés), az nem.
154
+ - Kilépés közben érkező újabb renderből **az utolsó** html kerül ki. Ha a feloldás közben
155
+ visszatér a képernyőn lévő html-re, a kilépés elmarad, az osztály lekerül.
156
+ - Az első megjelenésnek nincs kilépése, csak belépése. Ugyanígy, ha a nézet layoutja cserélődik,
157
+ a widget újraépül: a régi elem kilépés nélkül tűnik el, az új belép.
158
+ - A kilépés alatt a régi html és a hidratált toolbarjai még élnek és kattinthatók. A kilépő
159
+ animációt ezért érdemes röviden tartani, és `forwards` kitöltéssel a végállapotban hagyni, hogy
160
+ az új html kiírásáig ne villanjon vissza.
161
+ - A `backwards` kitöltés a belépésnél azért jó, mert a vége után a `:hover` és a többi szabály
162
+ ismét mozgathatja az elemet (a `both` vagy a `forwards` animációs értéke felülírná őket).
163
+
164
+ ## A szerver-fázis
165
+
166
+ A szerver-helyőrző a platform template-dialektusa: `${alias:/útvonal#mező?stratégia}`, mellette a
167
+ `data-sb4-context` (iteráció) és a `data-sb4-if` (feltétel) attribútum. A builder a **nyitásáról**
168
+ ismeri fel — `${alias:/` —, a sima `${name}` neki és a motornak is közömbös szöveg. Literálként
169
+ `&#36;{`-nak írd.
170
+
171
+ Négy út oldja fel, és **egyik sem fut magától**:
172
+
173
+ | Út | Mikor | Hiba esetén |
174
+ |---|---|---|
175
+ | `resolveTemplate(ctx, …)`, és az eredmény megy a `html(...)`-be | a build előtt | elnyel: logol, és a kulcsot vagy üres stringet ad vissza |
176
+ | `h.template(ctx, KEY)` | a widget felépítésekor, kulcsból (lásd *Stíluslap a template-kulcs mellől*) | dob |
177
+ | `h.resolveLater()` + `resolveHtml(ctx, layout)` | a **felépített layouton** | dob |
178
+ | `h.templateKey(KEY)` + `resolveHtml(ctx, viewName, layout)` | a **felépített vagy tárolt layouton**, kulcsból (lásd *Template-kulcs egy tárolt layoutban*) | dob |
179
+
180
+ ```java
181
+ SmartComponentLayoutDefinition layout = layoutBuilder(view)
182
+ .vForm(f -> f.html(h -> h.html(CARD_HTML).resolveLater()))
183
+ .build();
184
+ resolveHtml(objectApi.contextObject().set("contact", contact), layout); // PageApiImpl
185
+ ```
186
+
187
+ - A `resolveHtml` bejárja a layout `FORM` / `CONTAINER` fáját és a widgetek gyerek-widgetjeit, és
188
+ minden HTML widget `html`-jét feloldja **ugyanazzal a ctx-szel**. A `css`, a `root` és a `fields`
189
+ marad. A gridekhez nem nyúl — egy sor-layout maga is layout, arra külön hívható. Page-en kívül:
190
+ `ObjectLayouts.resolveHtml(layout, resolver, objectApi, ctx)`; listára is van változata.
191
+ - **Amiben nincs `${alias:/`, ahhoz hozzá sem ér** — az a html nem megy át a motoron, tehát nem is
192
+ normalizálódik. Ezért bármilyen layoutra ráhívható, és a második futás egy már feloldott
193
+ widgeten nem csinál semmit.
194
+ - **Helyben módosít.** Ha az adat változik, **építsd újra a layoutot, ne oldd fel újra**: a
195
+ view-ban tárolt, egyszer már feloldott layoutban nincs mit feloldani, az új ctx-szel hívott
196
+ második futás némán a régi értékeket hagyja. Aki a feloldatlan eredetit meg akarja tartani,
197
+ másolaton hívja.
198
+ - **Hangosan bukik** (`IllegalStateException`): ha van `${alias:/`, de nincs template-modul a
199
+ hostban; és ha a feloldás **után** is maradt — a motor az ismeretlen aliasú helyőrzőt ugyanis
200
+ szó nélkül benne hagyja. A motor másik két csendes esete megmarad: a hiányzó / `null` mező
201
+ helyére szóköz kerül, a mezőszintű feloldási hiba helyére a „Feldolgozási hiba" szöveg.
202
+
203
+ **A `build()` őre.** Az a `${alias:/`, amit a négy út egyike sem oldott fel, régen szó nélkül
204
+ kiment a kliensre, és nyersen megjelent. Most a `HtmlWidgetBuilder.build()` `IllegalStateException`-t
205
+ dob rá, kivéve ha a widget `resolveLater()`-t mondott. Ez a `vf.html(String)`, az
206
+ `ObjectLayoutBuilder.html(...)` és a `GridBuilder.htmlColumn(...)` útjára is igaz.
207
+
208
+ **Üres ctx-szel semmi nem oldódik fel**: a `${…}` literál marad (és a `build()` elbukik rajta), a
209
+ `data-sb4-context` elemek eltűnnek. A grid-szintű sor-layoutnak és az oszlop-sablonnak nincs
210
+ szerver-oldali, soronkénti kontextusa — ott a sorból jövő érték `{{…}}`, a szerveren számolt érték
211
+ pedig a sor egy mezője legyen.
212
+
213
+ ### Template-kulcs egy tárolt layoutban
214
+
215
+ Egy elmentett layout (`ObjectLayoutDescriptor`) nem a page kódjában épül, ezért nem tud
216
+ `template(ctx, KEY)`-t hívni. Ott a widget csak **megnevezi** a kulcsot a
217
+ `HtmlProperties.templateKey` mezőben, és a szerver tölti be, amikor a layout a view-ra kerül:
218
+
219
+ ```java
220
+ // kódból: f.html(h -> h.root("contact").templateKey("CONTACT_CARD"))
221
+ // layout DSL: html card { template: CONTACT_CARD, root: contact }
222
+
223
+ view.putComponentLayoutsItem(Layouts.DEFAULT_LAYOUT, // PageApiImpl
224
+ resolvedCopy(ctx, view.getViewName(), descriptor.getLayouts().get(Layouts.DEFAULT_LAYOUT)));
225
+ ```
226
+
227
+ - **Tárolt layoutot a `resolvedCopy` visz a view-ra**: mély másolatot készít, és azon futtatja a
228
+ `resolveHtml`-t. A `resolveHtml` helyben ír, a descriptor példánya pedig lehet cache-elt — a
229
+ másolat nélkül a betöltött html a descriptorba kerülne. Page-en kívül:
230
+ `ObjectLayouts.resolvedCopy(layout, resolver, objectApi, ctx, viewName)`. Kódból épített
231
+ layoutra (ahol nincs mit megőrizni) elég a `resolveHtml(ctx, viewName, layout)`.
232
+ - **Ugyanazok a szabályok, mint a `template(ctx, KEY)`-nél**: a html a legspecifikusabb névről jön
233
+ (`<viewName>.<KEY>` → `<KEY>` → …), a stíluslapok mind összefűződnek (fájl, aztán MDM), és
234
+ lecserélik a widget saját `css`-ét, ha van belőlük. A betöltött html ugyanazt az ellenőrzést
235
+ kapja, mint a `build()`-ben: a kliens-helyőrzők nyelvtana és az ikon-szótár kulcsai.
236
+ - **A `viewName` kötelező a page-en.** A kulcsos layoutra hívott `resolveHtml(ctx, layout)`
237
+ kivételt dob: név nélkül ugyanaz a kulcs más template-et adhatna, mint a view-n épített widgeté.
238
+ Aki szándékosan nem akar nézetenkénti template-et, `null` nevet ad át.
239
+ - **A kulcs nyer, és a widgetben marad.** A mellette tárolt `html`-t a betöltött felülírja, és a
240
+ kulcsos widget **minden futásnál újratöltődik** — rá tehát nem igaz, hogy a második futás nem
241
+ csinál semmit: az MDM-ben módosított template a következő futásnál megjelenik. A layout DSL
242
+ ezért hibát jelez, ha egy widgeten `template` és `html` is van.
243
+ - **A field-lista: a megadott és a betöltött uniója.** A betöltött html mezői hozzáadódnak ahhoz,
244
+ amit a widget maga megadott (`fields`). Amíg a layout nincs feloldva, a kulcsos widgetnek csak a
245
+ megadott mezői vannak — ha a layout modelljét feloldás előtt kell belőle felépíteni
246
+ (`constructSimpleModel`, object editing), add meg őket kézzel.
247
+ - **Hangosan bukik**, ha nincs template-modul, ha a kulcs egyik neve sem létezik, ha a betöltött
248
+ html-ben a ctx-szel sem oldódott fel egy `${alias:/`, vagy ha a html nem megy át az
249
+ ellenőrzésen.
250
+ - **Aminek a layoutján nem futott feloldás, az üresen megy ki.** A kliens ilyenkor egy
251
+ konzol-figyelmeztetést ír a kulcs nevével. A grid sor-layoutjára ez nem áll: azt a grid
252
+ inicializálása tölti be (lásd lent).
253
+ - **Egy helyben újra feloldott layout csak bővül.** Ha ugyanazon a példányon fut újra a feloldás,
254
+ és a template közben mezőt vagy stíluslapot veszített, a régi mező a listában, a régi `css` a
255
+ widgeten marad. A `resolvedCopy` minden alkalommal a tárolt állapotból indul, ott ez nem fordul
256
+ elő.
257
+
258
+ A platform saját útjai, amelyek tárolt layoutot tesznek view-ra, mind a `resolvedCopy`-n mennek át,
259
+ és **mindegyik megnevezi az adatát** — üres ctx-szel feloldó út nincs, mert az addig „működik", amíg
260
+ az első template adatot nem olvas. A view modellje minden ilyen úton ott van `model` néven
261
+ (`ObjectLayouts.CONTEXT_MODEL`): egy MDM-be feltett template `${model:/#name}`-mel olvassa.
262
+
263
+ | Út | Ctx | Megjegyzés |
264
+ |---|---|---|
265
+ | `ObjectLayoutDescriptorUtilApi.initLayoutDescriptor(view, descriptor, ctx)` | a hívóé + `model` | a `DataSheetPageApiImpl` ezt hívja, a view modelljével `dataSheet` néven is: `${dataSheet:/#name}` |
266
+ | `…initDescriptorState(view, schema, descriptor)` | — | **nem old fel**: csak a descriptor constraintjeit, value setjeit és fájl-widgetjeit teszi a view-ra; annak a page-nek, amely a layoutot maga építette és oldotta fel |
267
+ | MDM bejegyzés-szerkesztő (`MDMEntryListPageApiImpl`) | az entry `model` néven | az `MDM_VIEW.<név>` descriptor layoutja |
268
+ | Object navigation (`NavigationNodeLayoutInternalApi`) | a node adata `node` néven | a view nevével, a body építésekor; a `setView` már nem old fel újra, a descriptor feloldott body-ját teszi ki |
269
+
270
+ Az `ObjectLayoutApi.getObjectDisplay` **nem** old fel: aki annak a layoutját, vagy közvetlenül a
271
+ `descriptor.getLayouts()`-ét teszi a view-ba, a `resolvedCopy`-t maga hívja.
272
+
273
+ **Sor-layout: betöltés a grid inicializálásakor.** A kártya-grid sor-layoutja is lehet kulcsos —
274
+ egy tárolt `SearchPageConfig` grid-nézetében, egy MDM-bejegyzés descriptorának listanézeteiben, a
275
+ `GridBuilder.rowLayout(...)`-ban vagy közvetlenül írt descriptorban. Ezt nem a page tölti be,
276
+ hanem a `GridModelApi.initGridInView`, amin minden grid átmegy, az aktív és az összes választható
277
+ grid-nézetre.
278
+
279
+ - **Csak a betöltetlen kulcs töltődik** (nincs `html`): a kulcs nélküli grid semmibe nem kerül, a
280
+ page által már betöltött layout érintetlenül megy át.
281
+ - **Másolaton dolgozik.** A grid-modellbe a betöltött másolat kerül, a hívó `GridView`-ja (tárolt
282
+ config, cache-elt descriptor) megtartja a kulcsot, így az MDM-ben módosított template a következő
283
+ megnyitáskor megjelenik, újraindítás nélkül.
284
+ - **A név-lánc a gridé** (lásd *Stíluslap a template-kulcs mellől*): `<gridId>.<viewName>.<KEY>`
285
+ elől. A `gridId` az, amivel a gridet inicializálják (a search page-é `RESULT_GRID`): egy grid
286
+ kártyájának egy `<gridId>.<viewName>.<KEY>` nevű MDM-template html-je nyer, a mellette álló `.css`
287
+ a fájlos stíluslapok után rétegződik (Style Sheet Layering); a view összes gridjénél ez a
288
+ `<viewName>.<KEY>`-re igaz.
289
+ - **A szerver-kontextus üres, a `${…}` hangos.** A motor üres kontextussal a `${…}`-t szó szerint
290
+ visszaadja: nem dob és nem helyettesít. Ezért az init a betöltött template szövegét a kiértékelés
291
+ előtt átnézi, és egy `${…}`-ra `IllegalStateException`-nel bukik, a template és a helyőrző
292
+ nevével. Ugyanígy bukik, ha a kulcs egyik neve sem létezik, vagy nincs template-modul a hostban.
293
+ - A `GridBuilder.build()` mező-WARN-ja a kulcsos widgetnek csak a megadott mezőit látja: a
294
+ template mezői csak az initkor jönnek meg.
295
+
296
+ ## A kliens-helyőrzők nyelvtana
297
+
298
+ Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
299
+ `HtmlTemplateScanner` is, ezért a hibás template már a page felépítésekor elbukik.
300
+
301
+ ### Tag és path
302
+
303
+ ```
304
+ {{path}} érték
305
+ {{path | pipe:'arg' | pipe2}} érték formázó pipe-okkal
306
+ {{#path}} … {{/path}} belépő szekció
307
+ {{?path}} … {{/path}} őr: van érték
308
+ {{^path}} … {{/path}} őr: nincs érték
309
+ {{.}} / {{. | pipe}} az aktuális scope maga
310
+ {{../path}} path a körülvevő belépő szekció scope-jából
311
+ ```
312
+
313
+ - A tagen belül a szóköz megengedett (`{{ name }}`, `{{# parties }}`).
314
+ - **Path**: `(../)*azonosító(.azonosító)*`, az azonosító `[A-Za-z_][A-Za-z0-9_]*`. **Nincs** index
315
+ (`list[0]`), abszolút út, `{{..}}` és a path közepén álló `../` (`a/../b`).
316
+ - Helyőrző text-node-ban és attribútum-**értékben** állhat (`class="status-{{status}}"`); tag- vagy
317
+ attribútum-névben nem.
318
+ - **Nincs** raw (`{{{x}}}`, `{{&x}}` → hiba), partial, komment, delimiter-váltás, `@index`.
319
+ - Literál `{{` a html-ben: `&#123;&#123;`.
320
+
321
+ ### Értékek
322
+
323
+ - Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
324
+ - Primitív → `String(v)` (`false` → `false`, `0` → `0`).
325
+ - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`. Egyetlen kivétel: egy **ikon-slot**
326
+ név-attribútumának teljes értékeként az objektum az ikon `ImageResource`-a lesz — lásd
327
+ *Ikon-slot*.
328
+ - Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
329
+ nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
330
+
331
+ ### Szekciók — a jel dönt, nem az érték
332
+
333
+ | Jel | Név | Mit csinál |
334
+ |---|---|---|
335
+ | `{{#p}}` | **belépő** szekció | lista: elemenként egyszer, a scope az elem; objektum: egyszer, a scope az objektum |
336
+ | `{{?p}}` | **őr** | egyszer renderel, ha van érték; **a scope nem mozdul** |
337
+ | `{{^p}}` | hamis-őr | egyszer renderel, ha nincs érték; a scope nem mozdul |
338
+
339
+ - **„Nincs érték"** = hiányzó, `null`, `false`, `""`, üres lista. **A `0` érték** (szándékos eltérés
340
+ a Mustache-től: `{{?count}}` a nullát ne rejtse el).
341
+ - **Szigorú scope**: belépő szekción belül a path csak az aktuális elemben oldódik, a szülő mezőit
342
+ magától nem éri el (nincs Mustache-féle felfelé keresés — attól a szerver mezőlistája az adattól
343
+ függne). A körülvevő scope **kimondva** érhető el: `{{../path}}`.
344
+ - **`../`**: minden `../` egy **belépő** szekcióval lép kijjebb. A `?` és `^` őr nem szint (a scope-ot
345
+ sem mozdítja), átlát rajta. Érték-tagen, pipe-pal, attribútum-értékben és szekció-tagen is állhat
346
+ (`{{#../members}}…{{/../members}}` — a záró tag ugyanazt a pathot ismétli). A widget `root`-ja
347
+ fölé nem megy: több `../`, mint ahány belépő szekció nyitva van, hiba.
348
+ - Szekció-tagen nincs pipe.
349
+ - `{{#p}}` egyetlen értéken (szám, string): a kliens őrként rendereli és `console.warn`-t ír — a
350
+ Java-builder ezt nem látja előre (nincs adata), ilyenkor `{{?p}}` kell. A `../`-nek ez is egy
351
+ szint (a scope-ja a körülvevő), így a kliens és a szerver mezőlistája egyezik.
352
+ - A szekció-tag üres stringre cserélődik, a körülötte lévő whitespace marad.
353
+
354
+ ```html
355
+ <ul>
356
+ {{#parties}}
357
+ <li>{{displayName}} {{#roles}}<span>{{. | label:'BusinessRole'}}</span>{{/roles}}</li>
358
+ {{/parties}}
359
+ </ul>
360
+ {{^parties}}<p>— nincs érintett —</p>{{/parties}}
361
+ {{?note}}<p class="note">{{note}}</p>{{/note}}
362
+ ```
363
+
364
+ A kártya saját mezői a lista elemein belül, `root = "card"` mellett:
365
+
366
+ ```html
367
+ <ul>
368
+ {{#parties}}
369
+ <li data-sb4-action="REMOVE_PARTY" data-sb4-action-identifier="{{../id}}_{{id}}">
370
+ {{displayName}} — {{../title}}
371
+ {{#roles}}<span>{{. | label:'BusinessRole'}} ({{../displayName}})</span>{{/roles}}
372
+ </li>
373
+ {{/parties}}
374
+ </ul>
375
+ ```
376
+
377
+ A származtatott mezők: `card.parties`, `card.id`, `card.parties.id`, `card.parties.displayName`,
378
+ `card.title`, `card.parties.roles` — a `{{../displayName}}` a `roles`-on belülről a
379
+ `card.parties.displayName`, a `{{../title}}` a `card.title`.
380
+
381
+ ### Formázó pipe-ok
382
+
383
+ `| név` vagy `| név:'arg'`; az argumentum egyetlen, **egyszeres idézőjelű** literál (`\'` és `\\`
384
+ escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en megy át. A készlet
385
+ **zárt**:
386
+
387
+ | Pipe | Argumentum | Default | Szemantika |
388
+ |---|---|---|---|
389
+ | `date` | date-fns formátum | `'yyyy.MM.dd'` | ISO string → böngésző-időzóna szerinti dátum |
390
+ | `datetime` | date-fns formátum | `'yyyy.MM.dd H:mm'` | ugyanaz |
391
+ | `time` | date-fns formátum | `'H:mm'` | ugyanaz; a csak-idő (`12:34:56`) értéket is olvassa |
392
+ | `number` | Angular `digitsInfo` | `'1.0-3'` | `Intl.NumberFormat` a session locale-jával; szám és numerikus string |
393
+ | `label` | a valueSet neve (**kötelező**) | — | kód → címke a kontextus `valueSets`-éből |
394
+ | `default` | literál | `''` | üres / hiányzó érték helyett a literál |
395
+ | `upper` / `lower` | nincs | — | kis/nagybetű a session locale-jával |
396
+
397
+ - A defaultok a grid cella-defaultjai: a kártya és a tábla ugyanazt mutatja.
398
+ - **Dátum**: a dróton nem minden dátum zulu (`…Z` / offset, offset nélküli `LocalDateTime`,
399
+ `LocalDate`, `LocalTime`) — mindet olvassa. Nem ISO érték → nyers érték + warn.
400
+ **A date-fns `YYYY` / `DD` tokenje hibás**: `yyyy` / `dd` kell.
401
+ - **`label`**: a kulcs-mező `valueSetData.keyProperty`, különben `objectUri`, különben `uri`; a címke
402
+ `displayValue`, különben `name` — ugyanaz a lánc, mint a form select-jeinél. Nincs valueSet vagy
403
+ nincs találat → a nyers kód. A valueSetnek az oldal `ComponentModel.valueSets`-ében kell lennie.
404
+ - Üres értéket a `default` kivételével minden pipe érintetlenül enged tovább.
405
+
406
+ ### Hibák
407
+
408
+ | Hol | Mi történik |
409
+ |---|---|
410
+ | 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` /
411
+ `data-sb4-icon` / `data-sb4-icon-key`, `data-sb4-icon` és `data-sb4-icon-key` ugyanazon az elemen |
412
+ | Java-builder (`build()`) | `IllegalStateException`: a sablonban álló literál ikon-kulcs, amit az ikon-szótár nem tartalmaz |
413
+ | Java-builder (`build()`), `resolveHtml` | `IllegalStateException`: feloldatlan szerver-helyőrző (`${alias:/`), lásd *A szerver-fázis* |
414
+ | kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
415
+
416
+ Egyetlen szándékos különbség: az ismeretlen pipe-név a kliensen csak warn (az érték formázatlanul
417
+ megy ki), hogy egy régebbi kliens ne törjön el egy újabb szerver pipe-ján.
418
+
419
+ ### Szerzői szabályok a szerver-fázis miatt
420
+
421
+ Ha a html átmegy a szerver template-motorján (Jsoup), akkor:
422
+
423
+ - **egy gyökérelem kötelező** — a gyökér-szintű szöveg és szekció-tag eldobódik;
424
+ - táblázatban **explicit `<tbody>`** kell, különben a parser a `{{#rows}}` után szúr egyet;
425
+ - a whitespace-re (`:empty`, `white-space: pre`, `&nbsp;`) ne építs, a motor átírja;
426
+ - a szerverről behelyettesített **érték ne tartalmazzon `{{`-t**: a motor HTML-escape-eli, de a
427
+ `{{` átmegy rajta, és a kliens helyőrzőnek olvasná (a néző saját adatán oldaná fel);
428
+ - `?stratégia` után is állhat `{{…}}` — a stratégia a `{{`-on nem nyúl át.
429
+
430
+ ## Toolbar-slot
431
+
432
+ ```html
433
+ <smart-ui-action-toolbar data-sb4-toolbar="ROW_TOOLBAR"
434
+ data-sb4-toolbar-direction="HORIZONTAL" data-sb4-toolbar-alignment="END"
435
+ data-sb4-toolbar-scrollable></smart-ui-action-toolbar>
436
+ ```
437
+
438
+ A kliens a marker-elembe egy **valódi toolbart hidratál**, amely a `data-sb4-toolbar` alatti
439
+ címre (`uiAction.toolbar == id`) címzett akciókat rajzolja ki — ugyanazokkal a szabályokkal, mint
440
+ bármely más toolbar: a listát és a végrehajtót a környezetéből húzza (kártyán a sor akcióit,
441
+ nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarProperties)`.
442
+
443
+ - A marker **maga a `<smart-ui-action-toolbar>` tag** legyen: a hostok tag-szelektorral stílusozzák
444
+ a toolbart, egy `<div data-sb4-toolbar>` működne, de a host-CSS nem érné el.
445
+ - Üres tartalommal írd (a hidratálás úgyis kitörli); a saját `class` / `style` attribútumaid
446
+ megmaradnak.
447
+ - `direction`: `HORIZONTAL` | `VERTICAL`, `alignment`: `START` | `END`, a `scrollable` logikai
448
+ attribútum. Ismeretlen érték → warn, a tulajdonság kimarad.
449
+ - A helyőrzők feloldása **megelőzi** a hidratálást, tehát az id is lehet sablon:
450
+ `data-sb4-toolbar="addr_{{id}}_toolbar"` — szekcióban elemenként más toolbar.
451
+ - Üres vagy üresre feloldott id → warn, a slot kimarad. Ismétlődő marker → markerenként saját
452
+ toolbar.
453
+ - A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
454
+ eredménynél a hidratált toolbarok élnek tovább.
455
+
456
+ ## Ikon-slot
457
+
458
+ ```html
459
+ <smart-icon data-sb4-icon="mail" data-sb4-icon-color="warn"></smart-icon>
460
+ <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
461
+ <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
462
+ ```
463
+
464
+ A kliens a marker-elembe egy **valódi `smart-icon`-t hidratál**, ugyanazt a komponenst, amit a
465
+ gomb, a grid-cella és a form használ. Java: `Tag.icon(name)`, `Tag.icon(name, color)`,
466
+ `Tag.icon(imageResource)`, `Tag.iconKey(key)`, a szótár a widget-builderen: `h.icon(key, resource)`
467
+ / `h.icons(map)`.
468
+
469
+ Az ikon címe négyféle lehet, és mindegyiknek megvan a maga esete:
470
+
471
+ | Forma | Mit ad az ikonnak | Mikor |
472
+ |---|---|---|
473
+ | `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** |
474
+ | `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) |
475
+ | `data-sb4-icon="{…}"` (JSON) | `imageResource` input | a page által **egyszer** feloldott erőforrás (`Tag.icon(resource)`) |
476
+ | `data-sb4-icon-key="{{type}}"` | `imageResource` input a **szótárból** | véges típus → ikon leképezés |
477
+
478
+ - **`data-sb4-icon-color`**: a `color` input, vagyis téma- vagy host-osztálynév (`primary`,
479
+ `text-primary`, `warn` …), **nem** CSS-szín. Csak a név-formához való: az `ImageResource` a saját
480
+ `color`-ját hozza. Elhagyva a komponens alapértelmezése (`primary`) marad.
481
+ - **Objektum csak ott**, ahol az attribútum értéke pontosan egy helyőrző. Szöveggel keverve
482
+ (`data-sb4-icon="ic-{{typeIcon}}"`), pipe-pal vagy listán az érték a nyelvtan általános szabálya
483
+ szerint üres marad, warninggal. A feloldó az objektumot egy **renderelésenként újrahúzott,
484
+ véletlen tokennel** adja át a hidratálásnak, hogy adatból ne lehessen másik ikon erőforrására
485
+ hivatkozni.
486
+ - **Üres cím** (nincs ikonja ennek a sornak) → néma kihagyás, az elem üresen marad. A sablonban
487
+ **szó szerint üres** marker (`data-sb4-icon=""`) viszont `build()`-hiba, ahogy a toolbarnál.
488
+ - **Ismeretlen szótárkulcs** → nincs ikon, egy warning. A sablonban álló **literál** kulcsot, ami
489
+ nincs a szótárban, már a `build()` elutasítja.
490
+ - Egy elemen **nem állhat** a `data-sb4-icon` és a `data-sb4-icon-key` egyszerre: `build()`-hiba, a
491
+ kliens a kulcsot használja és warningot ír.
492
+ - **A jelölő bármely elemen állhat** (`<span class="badge" data-sb4-icon="mail"></span>`), nem csak
493
+ `<smart-icon>`-on: a kliens attribútum szerint keres, és a komponenst magába a jelölő-elembe
494
+ rendereli. A `Tag.icon(...)` azért mégis `<smart-icon>` taget generál, mert a hostok tag-szelektorral
495
+ szokták stílusozni az ikont — saját elemnél erre a szelektorra ne számíts.
496
+ - **Üres tartalommal írd**, mint a toolbar-slotot: a jelölő-elem gyerekei helyére a komponens
497
+ kerül. A saját `class`, `style` és `title` attribútuma megmarad, a méretezés a widget CSS-éből
498
+ megy (`.card-icon mat-icon { font-size: … }`). Ha szöveg és ikon is kell egy elemben — például egy
499
+ badge-ben —, az ikon **gyerek**-slot legyen:
500
+ `<span class="pill"><smart-icon data-sb4-icon="bolt"></smart-icon>count 0</span>`.
501
+ - Az ikon a nevét a regiszterben keresi meg, ezért **egy microtaskkal később** jelenik meg — a
502
+ kliens ettől még ugyanabban a renderelésben írja ki a html-t.
503
+
504
+ ### Példa: típus szerinti ikon egy ImageResource-ból
505
+
506
+ A szerveren az `ImageSettingApi` képezi le a típust ikonra (`images-*.properties`):
507
+
508
+ ```properties
509
+ ContactType.EMAIL={"identifier":"mail","color":"accent","tooltip":{"tooltip":"E-mail cím"}}
510
+ ContactType.PHONE={"identifier":"call","color":"primary","tooltip":{"tooltip":"Telefonszám"}}
511
+ ```
512
+
513
+ **Soronként más erőforrás** — az objektum az adatban utazik:
514
+
515
+ ```java
516
+ row.getData().put("typeIcon", imageSettingApi.get("ContactType", type.name()));
517
+ ```
518
+
519
+ ```html
520
+ <div class="pg-row">
521
+ <smart-icon data-sb4-icon="{{typeIcon}}"></smart-icon>
522
+ <b>{{name}}</b>
523
+ </div>
524
+ ```
525
+
526
+ **Véges leképezés** — az erőforrás egyszer utazik, a sor csak a kódot viszi:
527
+
528
+ ```java
529
+ .htmlColumn("type", h -> h
530
+ .icon("EMAIL", imageSettingApi.get("ContactType", "EMAIL"))
531
+ .icon("PHONE", imageSettingApi.get("ContactType", "PHONE"))
532
+ .html("""
533
+ <div class="pg-row">
534
+ <smart-icon data-sb4-icon-key="{{type}}"></smart-icon>
535
+ <b>{{name}}</b>
536
+ </div>"""))
537
+ ```
538
+
539
+ Mindkettőből ugyanaz lesz a DOM-ban:
540
+
541
+ ```html
542
+ <smart-icon data-sb4-icon="…"><mat-icon class="mat-accent material-icons">mail</mat-icon></smart-icon>
543
+ ```
544
+
545
+ A szótár a **sablon** része (`HtmlProperties.icons`), nem a soré: a sima HTML widgetnél is egyszer
546
+ 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
547
+ `GridRow.icons`-szal, ami a **cella értéke mellé** rajzolt ikonokat írja le.
548
+
549
+ ## Akció-trigger
550
+
551
+ ```html
552
+ <div class="card" data-sb4-action="OPEN_ENTRY">…</div>
553
+ <li data-sb4-action="REMOVE_MEMBER" data-sb4-action-identifier="{{id}}">…</li>
554
+ <div data-sb4-action="OPEN_MEMBER" data-sb4-action-identifier="{{id}}" data-sb4-action-direct>…</div>
555
+ ```
556
+
557
+ Bármely elem hordozhat triggert: a kattintás (vagy Enter / Space) azt az akciót futtatja, amelynek
558
+ a `code`-ja — és ha meg van adva, az `identifier`-e — egyezik. Az akciót a kliens **kattintáskor**
559
+ keresi meg ugyanabban a listában, amiből az ott lévő toolbarok húznak, **a toolbar-címtől
560
+ függetlenül**. Hiányzó `data-sb4-action-identifier` = identifier nélküli akció. Java:
561
+ `Tag.action(code)` / `Tag.action(code, identifier)`.
562
+
563
+ - **Lista-szekcióban elemenként egy akció kell a szerveren.** A `{{id}}`-s identifier címez, nem
564
+ paraméterez: a kliens a (kód, identifier) párt keresi a listában, tehát a page minden elemhez
565
+ kínáljon egy akciót a megfelelő `identifier`-rel (kártyán a sor akciói közt, nézeten a view akciói
566
+ közt). Egyetlen, identifier nélküli akció mellett az identifieres triggerek nem interaktívak.
567
+ - **A gyökérelemre tett trigger a `DIV` widget `onClick`-jének megfelelője.** A HTML widgetnek
568
+ nincs saját kattintása.
569
+ - **Csak-trigger akció**: ha egy nézet-szintű akciót csak trigger lőhet, a címe legyen
570
+ `UiActions.NO_TOOLBAR` (`"_none"`) — olyan cím, amit egyetlen toolbar sem visel, így az id nélküli
571
+ toolbarok sem rajzolják ki gombként. Kártyán (sorakciónál) nem kell.
572
+ - A trigger nem enged tovább se klikket, se dupla klikket a körülötte lévő elemeknek: kártyán nem
573
+ jelöl ki és nem futtat default akciót. Beágyazott triggereknél a legbelső nyer.
574
+ - `<a href>`-re tett triggernél az akció nyer, a link nem navigál.
575
+ - **a11y**: nem natív elem (`div`, `span`, `li`…) `role="button"`-t és `tabindex="0"`-t kap, ha a
576
+ szerző nem adott mást; natív elemhez (`button`, `a[href]`, `input`…) a kliens nem nyúl.
577
+ - **Letiltott akció**: `aria-disabled="true"` + `sb4-action-disabled` osztály, a kattintás no-op; az
578
+ állapot a lista változását a html újraírása nélkül követi.
579
+ - **Nem kínált akció** (a sor / nézet nem adja): az elem nem interaktív, `sb4-action-inert` osztályt
580
+ kap, a kliens egyszer warn-ol, a kattintás a körülötte lévő triggerre vagy a kártyára jut.
581
+ - **Direkt trigger** (`data-sb4-action-direct`): akkor is lő, ha a lista nem kínálja az akciót —
582
+ ilyenkor a kliens maga rakja össze a `{ code, identifier }` akciót az attribútumokból. Ha a lista
583
+ kínál egyező akciót, az nyer (letiltás, descriptor a listából jön). Nem inert és nem warn-ol.
584
+ Mivel a kínált-akció lista már nem kapuzza, **a szerver handlere maga ellenőrizze a
585
+ jogosultságot**. Java: `Tag.directAction(code)` / `Tag.directAction(code, identifier)`.
586
+ - Paraméter-attribútum nincs: az identifier és a sor-scope (`params.model` = a sor) fedi az
587
+ eseteket.
588
+
589
+ ## Sor-layoutok a kártya-griden
590
+
591
+ A `GridViewDescriptor.rowLayouts` kulcsonként egy-egy layoutot visz **gridenként egyszer**; a
592
+ kliens soronként rendereli a sor `data`-jával. Csak `kind == CARDS` mellett él.
593
+
594
+ ```java
595
+ gridBuilder.kind(KindEnum.CARDS)
596
+ .rowLayout(layoutBuilder(view).vForm(f -> f.html(h -> h.html(CARD_HTML))).build()) // default
597
+ .rowLayout("DISTRIBUTION_LIST", listLayout); // kulcsos
598
+ ```
599
+
600
+ Melyik layouttal renderel egy sor, sorrendben:
601
+
602
+ 1. a sor saját `layoutDescriptor.componentLayouts.GRID_ROW_LAYOUT`-ja (soronkénti felülbírálás);
603
+ 2. a `rowLayouts[row.rowLayout]`;
604
+ 3. a **default sor-layout**: a `"default"` kulcs (`Layouts.DEFAULT_LAYOUT`), vagy ha csak egy
605
+ bejegyzés van, az;
606
+ 4. a host `<gridId>Card` néven regisztrált kártya-komponense;
607
+ 5. semmi.
608
+
609
+ Ismeretlen kulcs → a default + egy `console.warn` (grid, sor, kulcs): az elgépelt kulcs nem tüntet
610
+ el kártyát, de látszik.
611
+
612
+ **Oszlopok.** A kártyán olvasott mezőket a grid oszlopainak kell adniuk: a `GridBuilder.build()`
613
+ WARN-t ír minden olyan mezőre, amit egy oszlop sem fed (név vagy dot-prefix szerint: a `card`
614
+ oszlop fedi a `card.parties.displayName`-et). A mezőlista ellenőriz, nem választ oszlopot.
615
+ Szerver-származtatott mező (pl. `partyCount`) legyen valódi oszlop.
616
+
617
+ **Kattintás** (csak a layoutból renderelt kártyán; a host-regisztrált kártya maga kezeli a saját
618
+ klikkjeit). Két független tengely, ugyanaz a modell, mint a táblán:
619
+
620
+ - **a kijelölést a `selectionMode` adja** (hiányzó = `NONE` = nincs kijelölés);
621
+ - **a default sor-akció gesztusát a `GridViewDescriptor.defaultRowActionTrigger`**
622
+ (`click` | `doubleClick`), Java: `GridBuilder.defaultRowActionTrigger(...)`.
623
+
624
+ | `defaultRowActionTrigger` | klikk | dupla klikk |
625
+ |---|---|---|
626
+ | nincs megadva (kártyán = `click`) | a sor **első** illő default akciója | — |
627
+ | `click` | default akció; több illő akciónál **menü az egérnél** | — |
628
+ | `doubleClick` | kijelölés, ha a grid és a sor jelölhető | default akció; több illőnél menü |
629
+
630
+ - **A klikkes trigger elviszi a klikket a kijelölés elől**: ha a sornak van default akciója és a
631
+ trigger `click` (vagy nincs megadva), a kártya klikkre nem jelölhető ki, akármi a
632
+ `selectionMode`. Kártya-kijelöléshez `doubleClick` trigger kell, vagy default akció nélküli grid.
633
+ - „Illő" default akció: a grid `defaultRowActions` kódjai közül az, amit a sor `actions`-e kínál —
634
+ a menü tehát soronként más lehet. Megadott trigger mellett egy illő akció azonnal elsül.
635
+ - A dupla klikk második klikkje nem számít külön klikknek: nem nyit kétszer, és nem vonja vissza
636
+ az első klikk kijelölését (dupla klikk = kijelöl + megnyit). Amíg a default akció fut, az újabb
637
+ klikk is eldobódik.
638
+ - A kijelölt kártya hostja `selected` osztályt és egy minimális, felülírható outline-t kap. A
639
+ kijelölés toggle, mint a táblán; `selectionType: CHECKBOX` a kártyán nem rajzol checkboxot.
640
+ - A toolbar-gomb és a trigger megtartja magának a kattintást.
641
+ - A tábla gesztusa változatlanul a dupla klikk, a mező értékétől függetlenül.
642
+
643
+ ## Html a tábla cellájában
644
+
645
+ A tábla egy cellája kétféleképpen kaphat html-t, és a kettő **más bizalmi szinten** áll.
646
+
647
+ | | HTML column | Column template |
648
+ |---|---|---|
649
+ | Honnan jön a html | a cella **értéke**, adatként (`GridRow.data`) | az oszlop **sablonja** (`GridColumnMeta.htmlProperties`) |
650
+ | Ki írta | bármi, ami az adatot előállítja | a backend, mint a HTML widgetnél |
651
+ | Kiírás | **sanitizált** innerHTML (Angular default) | változtatás nélkül, a behelyettesített értékek escape-elve |
652
+ | Helyőrzők, toolbar-slot, trigger | nincs (a `data-*` és az ismeretlen tag kiesik) | mind, mint a widgetben |
653
+
654
+ ### HTML column és `contentType`
655
+
656
+ A `GridColumnMeta.contentType` mondja meg, hogyan kerül ki az érték:
657
+
658
+ - **hiányzó** vagy `html` — sanitizált innerHTML, ahogy a tábla mindig is írta. Semmi nem változik
659
+ annak a hostnak, amelyik nem állítja.
660
+ - `text` — **escape-elt** szöveg: a `<b>` betűkként látszik. Erre való minden adat, amiben
661
+ felhasználói szöveg van és nem jelölésként kell olvasni.
662
+
663
+ A rendezés a szerveren a **nyers** értéken fut, ezért html-oszlophoz adj
664
+ `sortOrderPropertyName`-et. Java:
665
+
666
+ ```java
667
+ gridBuilder.contentType(GridColumnContentType.TEXT, "name", "comment")
668
+ ```
669
+
670
+ ### Column template
671
+
672
+ ```java
673
+ gridBuilder.htmlColumn("name", h -> h.html(t -> t
674
+ .b(b -> b.text("{{name}}"))
675
+ .span(s -> s.action("OPEN").text("megnyitás"))
676
+ .toolbar("ROW_TOOLBAR")))
677
+ ```
678
+
679
+ - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, `icons`, és
680
+ ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
681
+ `valueSets` a gridet mutató oldalé.
682
+ - A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
683
+ checkbox- és ikon-formázást is — a formázás a sablon pipe-jaiba kerül. A sor ikonjai
684
+ (`GridRow.icons`) és az oszlop cella-toolbarja (`columnActions`) mellette megmaradnak. Ha van
685
+ sablon, a `contentType` nem számít.
686
+ - A toolbar-slot és a trigger **a sor akcióit** látja (`row.actions`), pontosan úgy, mint a
687
+ sor-layouttal renderelt kártya: az akció modellje a sor, a scope a grid és a sor. A trigger a
688
+ kattintást és a dupla kattintást megtartja magának, a sor-klikk nem fut.
689
+ - **Sor-menü**: a csak-trigger akció (`UiActions.NO_TOOLBAR`) sosem kerül a sor menüjébe. Ha a
690
+ táblában van oszlop-sablon, akkor a **toolbar-címes** sorakció sem — az egy sablonbeli slot-é.
691
+ Sablon nélküli táblán a címes sorakció a menüben marad, ahogy eddig.
692
+ - A cella a sor minden cseréjekor újra felold; változatlan eredménynél a html és a hidratált
693
+ toolbar él tovább. Teljesítmény-kalap nincs: nagy táblán a sablon soronként egyszer oldódik fel,
694
+ hidratálás csak ott fut, ahol slot van.
695
+ - A `GridBuilder.build()` a sablon mezőit is ellenőrzi az oszlopokkal szemben, ugyanazzal a
696
+ WARN-nal, mint a sor-layoutét (`column '<név>' reads '<mező>' which no column covers`). Az
697
+ ismeretlen oszlopra adott sablon vagy `contentType` szintén WARN.
698
+ - **Kulcsból**: `gridBuilder.htmlColumn("name", h -> h.template(objectApi.contextObject(), KEY))`
699
+ — a page `gridBuilder(...)`-e átadja a template-feloldót, a sablon és a stíluslapja a kulcsról
700
+ jön, mint a widgetnél. Soronkénti szerver-kontextus itt sincs, a sablonban `{{…}}` legyen. A
701
+ halasztott `templateKey(KEY)` oszlopon nem megy (nincs layout, amin a `resolveHtml` futna), a
702
+ builder kivételt dob rá. Sor-layoutnál a `templateKey(KEY)` magában elég: a kulcs a grid
703
+ inicializálásakor töltődik be (lásd *Template-kulcs egy tárolt layoutban*). A page-oldali
704
+ betöltés — `template(ctx, KEY)` építéskor, vagy `templateKey(KEY)` és
705
+ `resolveHtml(ctx, viewName, rowLayout)` a `rowLayout(...)` hívás előtt — továbbra is működik, de
706
+ opcionális: csak a page saját kontextusából feloldott `${…}`-hoz kell, és a lánca ilyenkor a
707
+ form-widgeté, `<viewName>.<KEY>`.
708
+
709
+ ## Példa: egy kártya
710
+
711
+ ```html
712
+ <div class="contact-card contact-card-{{status}}" data-sb4-action="OPEN_CONTACT_ENTRY">
713
+ <div class="contact-card-main">
714
+ <div class="contact-card-icon contact-card-icon-{{channelType}}"></div>
715
+ <div class="contact-card-value">{{value}}</div>
716
+ <div class="contact-card-channel-type">{{channelType | label:'ContactChannelType'}}</div>
717
+ <div class="contact-card-modified">{{modifiedAt | datetime}}</div>
718
+ <smart-ui-action-toolbar data-sb4-toolbar="CONTACT_ROW_TOOLBAR"></smart-ui-action-toolbar>
719
+ </div>
720
+ <div class="contact-card-associations">
721
+ <div class="contact-card-associations-title">Érintettek ({{partyCount}})</div>
722
+ {{#parties}}
723
+ <div class="contact-card-related-party">
724
+ <div class="contact-card-related-party-initials">{{monogram}}</div>
725
+ <div class="contact-card-related-party-company">{{displayName}}</div>
726
+ <div>{{#businessRoles}}<span>{{. | label:'BusinessRole'}}</span> {{/businessRoles}}</div>
727
+ </div>
728
+ {{/parties}}
729
+ {{^parties}}<div class="contact-card-associations-title">— nincs érintett —</div>{{/parties}}
730
+ </div>
731
+ </div>
732
+ ```
733
+
734
+ `root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
735
+ szándékosan nem tud, annak a helye:
736
+
737
+ - **ikon**: ikon-slot (`data-sb4-icon` / `data-sb4-icon-key`), a leképezés az `ImageSettingApi`-ban;
738
+ értékfüggő megjelenés egyébként: szerver-származtatott mező, vagy érték-alapú CSS-osztály
739
+ (`contact-card-icon-{{channelType}}`);
740
+ - **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
741
+ logikai mező + `{{?…}}` őr;
742
+ - **darabszám** (`parties.size()`): valódi oszlop (`partyCount`);
743
+ - **fordítás**: `label` pipe valueSetből, vagy a szerver-fázis `${…}`-e.