@smartbit4all/ng-client 7.2.2 → 7.2.4

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,526 +1,526 @@
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
-
42
- A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
43
- (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
44
- semmit nevez, minden helyőrző üres marad, és a kliens egyszer `console.warn`-t ír.
45
-
46
- A widgetnek nincs kontrollja, címkéje, validálása és saját kattintása sem (lásd *Akció-trigger*).
47
-
48
- Java-oldalon:
49
-
50
- ```java
51
- layoutBuilder(view).vForm(f -> f
52
- .html(h -> h.root("card").html("<div class=\"card\">{{name}}</div>")))
53
- ```
54
-
55
- vagy template-kulcsból (`h.template(ctx, "CONTACT_ENTRY_LIST")`): a kulcs a szerveren oldódik fel,
56
- a dróton nem utazik.
57
-
58
- ## Stíluslap
59
-
60
- A widget a kinézetét is hozhatja: a `css` mezőben egy CSS-szöveget, amit a kliens a dokumentum
61
- `<head>`-jébe tesz (`<style data-sb4-html-css>`). Így a widget teljes egészében a szerverről jön, a
62
- hostnak nem kell hozzá stíluslapot szállítania.
63
-
64
- ```java
65
- vf.html(h -> h.root("card")
66
- .html("<div class=\"contact-card\">{{name}}</div>")
67
- .css(".contact-card { padding: 1rem; border-radius: 12px; }"))
68
- ```
69
-
70
- - **Egyszer kerül ki**, akárhány widget, kártya vagy cella hozza ugyanazt a szöveget, és az
71
- utolsó eltűnésével a kliens kiveszi. A sor-layout és az oszlop-sablon is így működik: a `css` a
72
- layout, illetve a sablon része, nem soronként utazik.
73
- - **Globális, nincs hatóköre.** Minden szelektor a html gyökérelemének egy osztályával kezdődjön
74
- (`.contact-card …`), különben az egész oldalra hat. Két különböző szöveg ugyanarra a
75
- szelektorra a betöltés sorrendjében nyer: adj widgetenként saját előtagot.
76
- - **Nem oldódik fel**: a `{{…}}`-nak nincs benne jelentése, modell-értéket nem lehet belevinni. Ami
77
- a modelltől függ, az a html-ben legyen osztály (`class="status-{{status}}"`), a CSS pedig
78
- osztályonként szabályozzon.
79
- - **CSP**: ha a host `style-src`-je nonce-os, adja meg az Angular `CSP_NONCE` tokenjét, a kliens
80
- ráteszi a `<style>`-ra. Nonce és `'unsafe-inline'` nélkül a böngésző a stíluslapot blokkolja.
81
- - A builder nem vizsgálja és nem formázza: a `%` és a `{{` sem zavarja.
82
-
83
- ### Stíluslap a template-kulcs mellől
84
-
85
- A `template(ctx, KEY)` a html mellé a stíluslapot is behúzza, ugyanazokon a neveken egy `.css`
86
- szegmenssel: `<viewName>.<KEY>.css`, `<KEY>.css`, … egészen a kulcs utolsó szegmenséig (a puszta
87
- `css` név sosem jön szóba). A fájl-registryben ez egy sor, pl.
88
- `CONTACT_ENTRY_LIST.css=templates/contact_entry_list.css`; MDM-ben egy ugyanilyen nevű
89
- `text/css` template.
90
-
91
- A html-lel ellentétben itt **nem egy nyer, hanem mind összefűződik**: előbb a fájlos stíluslapok,
92
- aztán az MDM-esek, mindkét rétegen belül az általánostól a specifikusig:
93
-
94
- ```
95
- <KEY>.css (fájl) → <viewName>.<KEY>.css (fájl) → <KEY>.css (MDM) → <viewName>.<KEY>.css (MDM)
96
- ```
97
-
98
- Mivel egyenlő specificitásnál a később jövő szabály nyer, az MDM a kódban szállított kinézetet
99
- szabályonként felülírhatja (ugyanazzal a szelektorral) vagy kiegészítheti (új szabállyal). A
100
- fájlban lévő `!important`-ot csak `!important` írja felül. A template-ből jött stíluslap lecseréli
101
- az addigi `css(...)`-t, egy későbbi `css(...)` pedig őt; ha a kulcshoz nincs stíluslap, az addigi
102
- `css` marad. A stíluslap nem értékelődik ki.
103
-
104
- ### Belépő és kilépő animáció
105
-
106
- A stíluslap (a widgeté vagy a hosté) animálhatja a widget és a cella tartalmának érkezését és
107
- távozását:
108
-
109
- ```css
110
- .contact-card { animation: contact-card-enter 300ms ease-out backwards; }
111
- .sb4-html-leaving .contact-card { animation: contact-card-leave 150ms ease-in forwards; }
112
-
113
- @keyframes contact-card-enter { from { opacity: 0; transform: translateY(12px); } }
114
- @keyframes contact-card-leave { to { opacity: 0; transform: translateY(-8px); } }
115
-
116
- @media (prefers-reduced-motion: reduce) {
117
- .contact-card, .sb4-html-leaving .contact-card { animation: none; }
118
- }
119
- ```
120
-
121
- - **Belépés**: a kiírt html új elemei a szokásos módon animálódnak. Ez minden olyan renderkor
122
- lefut, amely **új html-t ír**: az első megjelenéskor, és valahányszor a feloldás eredménye
123
- megváltozik (a modell vagy a sor olyan mezője változott, amit a html olvas). A változatlan
124
- eredményű push semmit nem ír, ott nem fut.
125
- - **Kilépés**: mielőtt a kliens a képernyőn lévő html-t lecseréli, a widget, illetve a cella
126
- elemére (`smart-html-widget`, `smart-html-cell`) felteszi az **`sb4-html-leaving`** osztályt.
127
- Ha egy stíluslap erre az osztályra animációt vagy transitiont indít, a kliens megvárja a
128
- végét, és csak utána írja ki az új html-t (és veszi le az osztályt). Ha semmi nem indul, azonnal
129
- ír: kilépő CSS nélkül minden úgy működik, mint eddig.
130
- - A várakozás **legfeljebb 600 ms**: egy végtelen vagy túl hosszú kilépő animáció sem tartja fel
131
- a frissítést. Csak az osztály által **elindított** animáció számít; ami már futott (pl. egy még
132
- tartó belépő vagy egy végtelen pörgés), az nem.
133
- - Kilépés közben érkező újabb renderből **az utolsó** html kerül ki. Ha a feloldás közben
134
- visszatér a képernyőn lévő html-re, a kilépés elmarad, az osztály lekerül.
135
- - Az első megjelenésnek nincs kilépése, csak belépése. Ugyanígy, ha a nézet layoutja cserélődik,
136
- a widget újraépül: a régi elem kilépés nélkül tűnik el, az új belép.
137
- - A kilépés alatt a régi html és a hidratált toolbarjai még élnek és kattinthatók. A kilépő
138
- animációt ezért érdemes röviden tartani, és `forwards` kitöltéssel a végállapotban hagyni, hogy
139
- az új html kiírásáig ne villanjon vissza.
140
- - 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
141
- ismét mozgathatja az elemet (a `both` vagy a `forwards` animációs értéke felülírná őket).
142
-
143
- ## A szerver-fázis
144
-
145
- A szerver-helyőrző a platform template-dialektusa: `${alias:/útvonal#mező?stratégia}`, mellette a
146
- `data-sb4-context` (iteráció) és a `data-sb4-if` (feltétel) attribútum. A builder a **nyitásáról**
147
- ismeri fel — `${alias:/` —, a sima `${name}` neki és a motornak is közömbös szöveg. Literálként
148
- `&#36;{`-nak írd.
149
-
150
- Három út oldja fel, és **egyik sem fut magától**:
151
-
152
- | Út | Mikor | Hiba esetén |
153
- |---|---|---|
154
- | `resolveTemplate(ctx, …)`, és az eredmény megy a `html(...)`-be | a build előtt | elnyel: logol, és a kulcsot vagy üres stringet ad vissza |
155
- | `h.template(ctx, KEY)` | a widget felépítésekor, kulcsból (lásd *Stíluslap a template-kulcs mellől*) | dob |
156
- | `h.resolveLater()` + `resolveHtml(ctx, layout)` | a **felépített layouton** | dob |
157
-
158
- ```java
159
- SmartComponentLayoutDefinition layout = layoutBuilder(view)
160
- .vForm(f -> f.html(h -> h.html(CARD_HTML).resolveLater()))
161
- .build();
162
- resolveHtml(objectApi.contextObject().set("contact", contact), layout); // PageApiImpl
163
- ```
164
-
165
- - A `resolveHtml` bejárja a layout `FORM` / `CONTAINER` fáját és a widgetek gyerek-widgetjeit, és
166
- minden HTML widget `html`-jét feloldja **ugyanazzal a ctx-szel**. A `css`, a `root` és a `fields`
167
- marad. A gridekhez nem nyúl — egy sor-layout maga is layout, arra külön hívható. Page-en kívül:
168
- `ObjectLayouts.resolveHtml(layout, resolver, objectApi, ctx)`; listára is van változata.
169
- - **Amiben nincs `${alias:/`, ahhoz hozzá sem ér** — az a html nem megy át a motoron, tehát nem is
170
- normalizálódik. Ezért bármilyen layoutra ráhívható, és a második futás egy már feloldott
171
- widgeten nem csinál semmit.
172
- - **Helyben módosít.** Ha az adat változik, **építsd újra a layoutot, ne oldd fel újra**: a
173
- view-ban tárolt, egyszer már feloldott layoutban nincs mit feloldani, az új ctx-szel hívott
174
- második futás némán a régi értékeket hagyja. Aki a feloldatlan eredetit meg akarja tartani,
175
- másolaton hívja.
176
- - **Hangosan bukik** (`IllegalStateException`): ha van `${alias:/`, de nincs template-modul a
177
- hostban; és ha a feloldás **után** is maradt — a motor az ismeretlen aliasú helyőrzőt ugyanis
178
- szó nélkül benne hagyja. A motor másik két csendes esete megmarad: a hiányzó / `null` mező
179
- helyére szóköz kerül, a mezőszintű feloldási hiba helyére a „Feldolgozási hiba" szöveg.
180
-
181
- **A `build()` őre.** Az a `${alias:/`, amit a három út egyike sem oldott fel, régen szó nélkül
182
- kiment a kliensre, és nyersen megjelent. Most a `HtmlWidgetBuilder.build()` `IllegalStateException`-t
183
- dob rá, kivéve ha a widget `resolveLater()`-t mondott. Ez a `vf.html(String)`, az
184
- `ObjectLayoutBuilder.html(...)` és a `GridBuilder.htmlColumn(...)` útjára is igaz.
185
-
186
- **Üres ctx-szel semmi nem oldódik fel**: a `${…}` literál marad (és a `build()` elbukik rajta), a
187
- `data-sb4-context` elemek eltűnnek. A grid-szintű sor-layoutnak és az oszlop-sablonnak nincs
188
- szerver-oldali, soronkénti kontextusa — ott a sorból jövő érték `{{…}}`, a szerveren számolt érték
189
- pedig a sor egy mezője legyen.
190
-
191
- ## A kliens-helyőrzők nyelvtana
192
-
193
- Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
194
- `HtmlTemplateScanner` is, ezért a hibás template már a page felépítésekor elbukik.
195
-
196
- ### Tag és path
197
-
198
- ```
199
- {{path}} érték
200
- {{path | pipe:'arg' | pipe2}} érték formázó pipe-okkal
201
- {{#path}} … {{/path}} belépő szekció
202
- {{?path}} … {{/path}} őr: van érték
203
- {{^path}} … {{/path}} őr: nincs érték
204
- {{.}} / {{. | pipe}} az aktuális scope maga
205
- {{../path}} path a körülvevő belépő szekció scope-jából
206
- ```
207
-
208
- - A tagen belül a szóköz megengedett (`{{ name }}`, `{{# parties }}`).
209
- - **Path**: `(../)*azonosító(.azonosító)*`, az azonosító `[A-Za-z_][A-Za-z0-9_]*`. **Nincs** index
210
- (`list[0]`), abszolút út, `{{..}}` és a path közepén álló `../` (`a/../b`).
211
- - Helyőrző text-node-ban és attribútum-**értékben** állhat (`class="status-{{status}}"`); tag- vagy
212
- attribútum-névben nem.
213
- - **Nincs** raw (`{{{x}}}`, `{{&x}}` → hiba), partial, komment, delimiter-váltás, `@index`.
214
- - Literál `{{` a html-ben: `&#123;&#123;`.
215
-
216
- ### Értékek
217
-
218
- - Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
219
- - Primitív → `String(v)` (`false` → `false`, `0` → `0`).
220
- - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`.
221
- - Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
222
- nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
223
-
224
- ### Szekciók — a jel dönt, nem az érték
225
-
226
- | Jel | Név | Mit csinál |
227
- |---|---|---|
228
- | `{{#p}}` | **belépő** szekció | lista: elemenként egyszer, a scope az elem; objektum: egyszer, a scope az objektum |
229
- | `{{?p}}` | **őr** | egyszer renderel, ha van érték; **a scope nem mozdul** |
230
- | `{{^p}}` | hamis-őr | egyszer renderel, ha nincs érték; a scope nem mozdul |
231
-
232
- - **„Nincs érték"** = hiányzó, `null`, `false`, `""`, üres lista. **A `0` érték** (szándékos eltérés
233
- a Mustache-től: `{{?count}}` a nullát ne rejtse el).
234
- - **Szigorú scope**: belépő szekción belül a path csak az aktuális elemben oldódik, a szülő mezőit
235
- magától nem éri el (nincs Mustache-féle felfelé keresés — attól a szerver mezőlistája az adattól
236
- függne). A körülvevő scope **kimondva** érhető el: `{{../path}}`.
237
- - **`../`**: minden `../` egy **belépő** szekcióval lép kijjebb. A `?` és `^` őr nem szint (a scope-ot
238
- sem mozdítja), átlát rajta. Érték-tagen, pipe-pal, attribútum-értékben és szekció-tagen is állhat
239
- (`{{#../members}}…{{/../members}}` — a záró tag ugyanazt a pathot ismétli). A widget `root`-ja
240
- fölé nem megy: több `../`, mint ahány belépő szekció nyitva van, hiba.
241
- - Szekció-tagen nincs pipe.
242
- - `{{#p}}` egyetlen értéken (szám, string): a kliens őrként rendereli és `console.warn`-t ír — a
243
- Java-builder ezt nem látja előre (nincs adata), ilyenkor `{{?p}}` kell. A `../`-nek ez is egy
244
- szint (a scope-ja a körülvevő), így a kliens és a szerver mezőlistája egyezik.
245
- - A szekció-tag üres stringre cserélődik, a körülötte lévő whitespace marad.
246
-
247
- ```html
248
- <ul>
249
- {{#parties}}
250
- <li>{{displayName}} {{#roles}}<span>{{. | label:'BusinessRole'}}</span>{{/roles}}</li>
251
- {{/parties}}
252
- </ul>
253
- {{^parties}}<p>— nincs érintett —</p>{{/parties}}
254
- {{?note}}<p class="note">{{note}}</p>{{/note}}
255
- ```
256
-
257
- A kártya saját mezői a lista elemein belül, `root = "card"` mellett:
258
-
259
- ```html
260
- <ul>
261
- {{#parties}}
262
- <li data-sb4-action="REMOVE_PARTY" data-sb4-action-identifier="{{../id}}_{{id}}">
263
- {{displayName}} — {{../title}}
264
- {{#roles}}<span>{{. | label:'BusinessRole'}} ({{../displayName}})</span>{{/roles}}
265
- </li>
266
- {{/parties}}
267
- </ul>
268
- ```
269
-
270
- A származtatott mezők: `card.parties`, `card.id`, `card.parties.id`, `card.parties.displayName`,
271
- `card.title`, `card.parties.roles` — a `{{../displayName}}` a `roles`-on belülről a
272
- `card.parties.displayName`, a `{{../title}}` a `card.title`.
273
-
274
- ### Formázó pipe-ok
275
-
276
- `| név` vagy `| név:'arg'`; az argumentum egyetlen, **egyszeres idézőjelű** literál (`\'` és `\\`
277
- escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en megy át. A készlet
278
- **zárt**:
279
-
280
- | Pipe | Argumentum | Default | Szemantika |
281
- |---|---|---|---|
282
- | `date` | date-fns formátum | `'yyyy.MM.dd'` | ISO string → böngésző-időzóna szerinti dátum |
283
- | `datetime` | date-fns formátum | `'yyyy.MM.dd H:mm'` | ugyanaz |
284
- | `time` | date-fns formátum | `'H:mm'` | ugyanaz; a csak-idő (`12:34:56`) értéket is olvassa |
285
- | `number` | Angular `digitsInfo` | `'1.0-3'` | `Intl.NumberFormat` a session locale-jával; szám és numerikus string |
286
- | `label` | a valueSet neve (**kötelező**) | — | kód → címke a kontextus `valueSets`-éből |
287
- | `default` | literál | `''` | üres / hiányzó érték helyett a literál |
288
- | `upper` / `lower` | nincs | — | kis/nagybetű a session locale-jával |
289
-
290
- - A defaultok a grid cella-defaultjai: a kártya és a tábla ugyanazt mutatja.
291
- - **Dátum**: a dróton nem minden dátum zulu (`…Z` / offset, offset nélküli `LocalDateTime`,
292
- `LocalDate`, `LocalTime`) — mindet olvassa. Nem ISO érték → nyers érték + warn.
293
- **A date-fns `YYYY` / `DD` tokenje hibás**: `yyyy` / `dd` kell.
294
- - **`label`**: a kulcs-mező `valueSetData.keyProperty`, különben `objectUri`, különben `uri`; a címke
295
- `displayValue`, különben `name` — ugyanaz a lánc, mint a form select-jeinél. Nincs valueSet vagy
296
- nincs találat → a nyers kód. A valueSetnek az oldal `ComponentModel.valueSets`-ében kell lennie.
297
- - Üres értéket a `default` kivételével minden pipe érintetlenül enged tovább.
298
-
299
- ### Hibák
300
-
301
- | Hol | Mi történik |
302
- |---|---|
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` |
304
- | Java-builder (`build()`), `resolveHtml` | `IllegalStateException`: feloldatlan szerver-helyőrző (`${alias:/`), lásd *A szerver-fázis* |
305
- | kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
306
-
307
- Egyetlen szándékos különbség: az ismeretlen pipe-név a kliensen csak warn (az érték formázatlanul
308
- megy ki), hogy egy régebbi kliens ne törjön el egy újabb szerver pipe-ján.
309
-
310
- ### Szerzői szabályok a szerver-fázis miatt
311
-
312
- Ha a html átmegy a szerver template-motorján (Jsoup), akkor:
313
-
314
- - **egy gyökérelem kötelező** — a gyökér-szintű szöveg és szekció-tag eldobódik;
315
- - táblázatban **explicit `<tbody>`** kell, különben a parser a `{{#rows}}` után szúr egyet;
316
- - a whitespace-re (`:empty`, `white-space: pre`, `&nbsp;`) ne építs, a motor átírja;
317
- - a szerverről behelyettesített **érték ne tartalmazzon `{{`-t**: a motor HTML-escape-eli, de a
318
- `{{` átmegy rajta, és a kliens helyőrzőnek olvasná (a néző saját adatán oldaná fel);
319
- - `?stratégia` után is állhat `{{…}}` — a stratégia a `{{`-on nem nyúl át.
320
-
321
- ## Toolbar-slot
322
-
323
- ```html
324
- <smart-ui-action-toolbar data-sb4-toolbar="ROW_TOOLBAR"
325
- data-sb4-toolbar-direction="HORIZONTAL" data-sb4-toolbar-alignment="END"
326
- data-sb4-toolbar-scrollable></smart-ui-action-toolbar>
327
- ```
328
-
329
- A kliens a marker-elembe egy **valódi toolbart hidratál**, amely a `data-sb4-toolbar` alatti
330
- címre (`uiAction.toolbar == id`) címzett akciókat rajzolja ki — ugyanazokkal a szabályokkal, mint
331
- 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,
332
- nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarProperties)`.
333
-
334
- - A marker **maga a `<smart-ui-action-toolbar>` tag** legyen: a hostok tag-szelektorral stílusozzák
335
- a toolbart, egy `<div data-sb4-toolbar>` működne, de a host-CSS nem érné el.
336
- - Üres tartalommal írd (a hidratálás úgyis kitörli); a saját `class` / `style` attribútumaid
337
- megmaradnak.
338
- - `direction`: `HORIZONTAL` | `VERTICAL`, `alignment`: `START` | `END`, a `scrollable` logikai
339
- attribútum. Ismeretlen érték → warn, a tulajdonság kimarad.
340
- - A helyőrzők feloldása **megelőzi** a hidratálást, tehát az id is lehet sablon:
341
- `data-sb4-toolbar="addr_{{id}}_toolbar"` — szekcióban elemenként más toolbar.
342
- - Üres vagy üresre feloldott id → warn, a slot kimarad. Ismétlődő marker → markerenként saját
343
- toolbar.
344
- - A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
345
- eredménynél a hidratált toolbarok élnek tovább.
346
-
347
- ## Akció-trigger
348
-
349
- ```html
350
- <div class="card" data-sb4-action="OPEN_ENTRY">…</div>
351
- <li data-sb4-action="REMOVE_MEMBER" data-sb4-action-identifier="{{id}}">…</li>
352
- ```
353
-
354
- Bármely elem hordozhat triggert: a kattintás (vagy Enter / Space) azt az akciót futtatja, amelynek
355
- a `code`-ja — és ha meg van adva, az `identifier`-e — egyezik. Az akciót a kliens **kattintáskor**
356
- keresi meg ugyanabban a listában, amiből az ott lévő toolbarok húznak, **a toolbar-címtől
357
- függetlenül**. Hiányzó `data-sb4-action-identifier` = identifier nélküli akció. Java:
358
- `Tag.action(code)` / `Tag.action(code, identifier)`.
359
-
360
- - **Lista-szekcióban elemenként egy akció kell a szerveren.** A `{{id}}`-s identifier címez, nem
361
- paraméterez: a kliens a (kód, identifier) párt keresi a listában, tehát a page minden elemhez
362
- 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
363
- közt). Egyetlen, identifier nélküli akció mellett az identifieres triggerek nem interaktívak.
364
- - **A gyökérelemre tett trigger a `DIV` widget `onClick`-jének megfelelője.** A HTML widgetnek
365
- nincs saját kattintása.
366
- - **Csak-trigger akció**: ha egy nézet-szintű akciót csak trigger lőhet, a címe legyen
367
- `UiActions.NO_TOOLBAR` (`"_none"`) — olyan cím, amit egyetlen toolbar sem visel, így az id nélküli
368
- toolbarok sem rajzolják ki gombként. Kártyán (sorakciónál) nem kell.
369
- - A trigger nem enged tovább se klikket, se dupla klikket a körülötte lévő elemeknek: kártyán nem
370
- jelöl ki és nem futtat default akciót. Beágyazott triggereknél a legbelső nyer.
371
- - `<a href>`-re tett triggernél az akció nyer, a link nem navigál.
372
- - **a11y**: nem natív elem (`div`, `span`, `li`…) `role="button"`-t és `tabindex="0"`-t kap, ha a
373
- szerző nem adott mást; natív elemhez (`button`, `a[href]`, `input`…) a kliens nem nyúl.
374
- - **Letiltott akció**: `aria-disabled="true"` + `sb4-action-disabled` osztály, a kattintás no-op; az
375
- állapot a lista változását a html újraírása nélkül követi.
376
- - **Nem kínált akció** (a sor / nézet nem adja): az elem nem interaktív, `sb4-action-inert` osztályt
377
- kap, a kliens egyszer warn-ol, a kattintás a körülötte lévő triggerre vagy a kártyára jut.
378
- - Paraméter-attribútum nincs: az identifier és a sor-scope (`params.model` = a sor) fedi az
379
- eseteket.
380
-
381
- ## Sor-layoutok a kártya-griden
382
-
383
- A `GridViewDescriptor.rowLayouts` kulcsonként egy-egy layoutot visz **gridenként egyszer**; a
384
- kliens soronként rendereli a sor `data`-jával. Csak `kind == CARDS` mellett él.
385
-
386
- ```java
387
- gridBuilder.kind(KindEnum.CARDS)
388
- .rowLayout(layoutBuilder(view).vForm(f -> f.html(h -> h.html(CARD_HTML))).build()) // default
389
- .rowLayout("DISTRIBUTION_LIST", listLayout); // kulcsos
390
- ```
391
-
392
- Melyik layouttal renderel egy sor, sorrendben:
393
-
394
- 1. a sor saját `layoutDescriptor.componentLayouts.GRID_ROW_LAYOUT`-ja (soronkénti felülbírálás);
395
- 2. a `rowLayouts[row.rowLayout]`;
396
- 3. a **default sor-layout**: a `"default"` kulcs (`Layouts.DEFAULT_LAYOUT`), vagy ha csak egy
397
- bejegyzés van, az;
398
- 4. a host `<gridId>Card` néven regisztrált kártya-komponense;
399
- 5. semmi.
400
-
401
- Ismeretlen kulcs → a default + egy `console.warn` (grid, sor, kulcs): az elgépelt kulcs nem tüntet
402
- el kártyát, de látszik.
403
-
404
- **Oszlopok.** A kártyán olvasott mezőket a grid oszlopainak kell adniuk: a `GridBuilder.build()`
405
- WARN-t ír minden olyan mezőre, amit egy oszlop sem fed (név vagy dot-prefix szerint: a `card`
406
- oszlop fedi a `card.parties.displayName`-et). A mezőlista ellenőriz, nem választ oszlopot.
407
- Szerver-származtatott mező (pl. `partyCount`) legyen valódi oszlop.
408
-
409
- **Kattintás** (csak a layoutból renderelt kártyán; a host-regisztrált kártya maga kezeli a saját
410
- klikkjeit). Két független tengely, ugyanaz a modell, mint a táblán:
411
-
412
- - **a kijelölést a `selectionMode` adja** (hiányzó = `NONE` = nincs kijelölés);
413
- - **a default sor-akció gesztusát a `GridViewDescriptor.defaultRowActionTrigger`**
414
- (`click` | `doubleClick`), Java: `GridBuilder.defaultRowActionTrigger(...)`.
415
-
416
- | `defaultRowActionTrigger` | klikk | dupla klikk |
417
- |---|---|---|
418
- | nincs megadva (kártyán = `click`) | a sor **első** illő default akciója | — |
419
- | `click` | default akció; több illő akciónál **menü az egérnél** | — |
420
- | `doubleClick` | kijelölés, ha a grid és a sor jelölhető | default akció; több illőnél menü |
421
-
422
- - **A klikkes trigger elviszi a klikket a kijelölés elől**: ha a sornak van default akciója és a
423
- trigger `click` (vagy nincs megadva), a kártya klikkre nem jelölhető ki, akármi a
424
- `selectionMode`. Kártya-kijelöléshez `doubleClick` trigger kell, vagy default akció nélküli grid.
425
- - „Illő" default akció: a grid `defaultRowActions` kódjai közül az, amit a sor `actions`-e kínál —
426
- a menü tehát soronként más lehet. Megadott trigger mellett egy illő akció azonnal elsül.
427
- - A dupla klikk második klikkje nem számít külön klikknek: nem nyit kétszer, és nem vonja vissza
428
- az első klikk kijelölését (dupla klikk = kijelöl + megnyit). Amíg a default akció fut, az újabb
429
- klikk is eldobódik.
430
- - A kijelölt kártya hostja `selected` osztályt és egy minimális, felülírható outline-t kap. A
431
- kijelölés toggle, mint a táblán; `selectionType: CHECKBOX` a kártyán nem rajzol checkboxot.
432
- - A toolbar-gomb és a trigger megtartja magának a kattintást.
433
- - A tábla gesztusa változatlanul a dupla klikk, a mező értékétől függetlenül.
434
-
435
- ## Html a tábla cellájában
436
-
437
- A tábla egy cellája kétféleképpen kaphat html-t, és a kettő **más bizalmi szinten** áll.
438
-
439
- | | HTML column | Column template |
440
- |---|---|---|
441
- | Honnan jön a html | a cella **értéke**, adatként (`GridRow.data`) | az oszlop **sablonja** (`GridColumnMeta.htmlProperties`) |
442
- | Ki írta | bármi, ami az adatot előállítja | a backend, mint a HTML widgetnél |
443
- | Kiírás | **sanitizált** innerHTML (Angular default) | változtatás nélkül, a behelyettesített értékek escape-elve |
444
- | Helyőrzők, toolbar-slot, trigger | nincs (a `data-*` és az ismeretlen tag kiesik) | mind, mint a widgetben |
445
-
446
- ### HTML column és `contentType`
447
-
448
- A `GridColumnMeta.contentType` mondja meg, hogyan kerül ki az érték:
449
-
450
- - **hiányzó** vagy `html` — sanitizált innerHTML, ahogy a tábla mindig is írta. Semmi nem változik
451
- annak a hostnak, amelyik nem állítja.
452
- - `text` — **escape-elt** szöveg: a `<b>` betűkként látszik. Erre való minden adat, amiben
453
- felhasználói szöveg van és nem jelölésként kell olvasni.
454
-
455
- A rendezés a szerveren a **nyers** értéken fut, ezért html-oszlophoz adj
456
- `sortOrderPropertyName`-et. Java:
457
-
458
- ```java
459
- gridBuilder.contentType(GridColumnContentType.TEXT, "name", "comment")
460
- ```
461
-
462
- ### Column template
463
-
464
- ```java
465
- gridBuilder.htmlColumn("name", h -> h.html(t -> t
466
- .b(b -> b.text("{{name}}"))
467
- .span(s -> s.action("OPEN").text("megnyitás"))
468
- .toolbar("ROW_TOOLBAR")))
469
- ```
470
-
471
- - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, és
472
- ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
473
- `valueSets` a gridet mutató oldalé.
474
- - A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
475
- checkbox- és ikon-formázást is — a formázás a sablon pipe-jaiba kerül. A sor ikonjai
476
- (`GridRow.icons`) és az oszlop cella-toolbarja (`columnActions`) mellette megmaradnak. Ha van
477
- sablon, a `contentType` nem számít.
478
- - A toolbar-slot és a trigger **a sor akcióit** látja (`row.actions`), pontosan úgy, mint a
479
- sor-layouttal renderelt kártya: az akció modellje a sor, a scope a grid és a sor. A trigger a
480
- kattintást és a dupla kattintást megtartja magának, a sor-klikk nem fut.
481
- - **Sor-menü**: a csak-trigger akció (`UiActions.NO_TOOLBAR`) sosem kerül a sor menüjébe. Ha a
482
- táblában van oszlop-sablon, akkor a **toolbar-címes** sorakció sem — az egy sablonbeli slot-é.
483
- Sablon nélküli táblán a címes sorakció a menüben marad, ahogy eddig.
484
- - A cella a sor minden cseréjekor újra felold; változatlan eredménynél a html és a hidratált
485
- toolbar él tovább. Teljesítmény-kalap nincs: nagy táblán a sablon soronként egyszer oldódik fel,
486
- hidratálás csak ott fut, ahol slot van.
487
- - A `GridBuilder.build()` a sablon mezőit is ellenőrzi az oszlopokkal szemben, ugyanazzal a
488
- WARN-nal, mint a sor-layoutét (`column '<név>' reads '<mező>' which no column covers`). Az
489
- ismeretlen oszlopra adott sablon vagy `contentType` szintén WARN.
490
- - A `GridBuilder`-nek nincs template-feloldója: a `template(ctx, KEY)` itt nem működik. Kulcsból
491
- épített sablonhoz építsd a widgetet a page layout-builderével, és add át a `properties(...)`-szel.
492
-
493
- ## Példa: egy kártya
494
-
495
- ```html
496
- <div class="contact-card contact-card-{{status}}" data-sb4-action="OPEN_CONTACT_ENTRY">
497
- <div class="contact-card-main">
498
- <div class="contact-card-icon contact-card-icon-{{channelType}}"></div>
499
- <div class="contact-card-value">{{value}}</div>
500
- <div class="contact-card-channel-type">{{channelType | label:'ContactChannelType'}}</div>
501
- <div class="contact-card-modified">{{modifiedAt | datetime}}</div>
502
- <smart-ui-action-toolbar data-sb4-toolbar="CONTACT_ROW_TOOLBAR"></smart-ui-action-toolbar>
503
- </div>
504
- <div class="contact-card-associations">
505
- <div class="contact-card-associations-title">Érintettek ({{partyCount}})</div>
506
- {{#parties}}
507
- <div class="contact-card-related-party">
508
- <div class="contact-card-related-party-initials">{{monogram}}</div>
509
- <div class="contact-card-related-party-company">{{displayName}}</div>
510
- <div>{{#businessRoles}}<span>{{. | label:'BusinessRole'}}</span> {{/businessRoles}}</div>
511
- </div>
512
- {{/parties}}
513
- {{^parties}}<div class="contact-card-associations-title">— nincs érintett —</div>{{/parties}}
514
- </div>
515
- </div>
516
- ```
517
-
518
- `root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
519
- szándékosan nem tud, annak a helye:
520
-
521
- - **ikon / értékfüggő megjelenés**: szerver-származtatott mező, vagy érték-alapú CSS-osztály
522
- (`contact-card-icon-{{channelType}}`);
523
- - **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
524
- logikai mező + `{{?…}}` őr;
525
- - **darabszám** (`parties.size()`): valódi oszlop (`partyCount`);
526
- - **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
+
42
+ A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
43
+ (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
44
+ semmit nevez, minden helyőrző üres marad, és a kliens egyszer `console.warn`-t ír.
45
+
46
+ A widgetnek nincs kontrollja, címkéje, validálása és saját kattintása sem (lásd *Akció-trigger*).
47
+
48
+ Java-oldalon:
49
+
50
+ ```java
51
+ layoutBuilder(view).vForm(f -> f
52
+ .html(h -> h.root("card").html("<div class=\"card\">{{name}}</div>")))
53
+ ```
54
+
55
+ vagy template-kulcsból (`h.template(ctx, "CONTACT_ENTRY_LIST")`): a kulcs a szerveren oldódik fel,
56
+ a dróton nem utazik.
57
+
58
+ ## Stíluslap
59
+
60
+ A widget a kinézetét is hozhatja: a `css` mezőben egy CSS-szöveget, amit a kliens a dokumentum
61
+ `<head>`-jébe tesz (`<style data-sb4-html-css>`). Így a widget teljes egészében a szerverről jön, a
62
+ hostnak nem kell hozzá stíluslapot szállítania.
63
+
64
+ ```java
65
+ vf.html(h -> h.root("card")
66
+ .html("<div class=\"contact-card\">{{name}}</div>")
67
+ .css(".contact-card { padding: 1rem; border-radius: 12px; }"))
68
+ ```
69
+
70
+ - **Egyszer kerül ki**, akárhány widget, kártya vagy cella hozza ugyanazt a szöveget, és az
71
+ utolsó eltűnésével a kliens kiveszi. A sor-layout és az oszlop-sablon is így működik: a `css` a
72
+ layout, illetve a sablon része, nem soronként utazik.
73
+ - **Globális, nincs hatóköre.** Minden szelektor a html gyökérelemének egy osztályával kezdődjön
74
+ (`.contact-card …`), különben az egész oldalra hat. Két különböző szöveg ugyanarra a
75
+ szelektorra a betöltés sorrendjében nyer: adj widgetenként saját előtagot.
76
+ - **Nem oldódik fel**: a `{{…}}`-nak nincs benne jelentése, modell-értéket nem lehet belevinni. Ami
77
+ a modelltől függ, az a html-ben legyen osztály (`class="status-{{status}}"`), a CSS pedig
78
+ osztályonként szabályozzon.
79
+ - **CSP**: ha a host `style-src`-je nonce-os, adja meg az Angular `CSP_NONCE` tokenjét, a kliens
80
+ ráteszi a `<style>`-ra. Nonce és `'unsafe-inline'` nélkül a böngésző a stíluslapot blokkolja.
81
+ - A builder nem vizsgálja és nem formázza: a `%` és a `{{` sem zavarja.
82
+
83
+ ### Stíluslap a template-kulcs mellől
84
+
85
+ A `template(ctx, KEY)` a html mellé a stíluslapot is behúzza, ugyanazokon a neveken egy `.css`
86
+ szegmenssel: `<viewName>.<KEY>.css`, `<KEY>.css`, … egészen a kulcs utolsó szegmenséig (a puszta
87
+ `css` név sosem jön szóba). A fájl-registryben ez egy sor, pl.
88
+ `CONTACT_ENTRY_LIST.css=templates/contact_entry_list.css`; MDM-ben egy ugyanilyen nevű
89
+ `text/css` template.
90
+
91
+ A html-lel ellentétben itt **nem egy nyer, hanem mind összefűződik**: előbb a fájlos stíluslapok,
92
+ aztán az MDM-esek, mindkét rétegen belül az általánostól a specifikusig:
93
+
94
+ ```
95
+ <KEY>.css (fájl) → <viewName>.<KEY>.css (fájl) → <KEY>.css (MDM) → <viewName>.<KEY>.css (MDM)
96
+ ```
97
+
98
+ Mivel egyenlő specificitásnál a később jövő szabály nyer, az MDM a kódban szállított kinézetet
99
+ szabályonként felülírhatja (ugyanazzal a szelektorral) vagy kiegészítheti (új szabállyal). A
100
+ fájlban lévő `!important`-ot csak `!important` írja felül. A template-ből jött stíluslap lecseréli
101
+ az addigi `css(...)`-t, egy későbbi `css(...)` pedig őt; ha a kulcshoz nincs stíluslap, az addigi
102
+ `css` marad. A stíluslap nem értékelődik ki.
103
+
104
+ ### Belépő és kilépő animáció
105
+
106
+ A stíluslap (a widgeté vagy a hosté) animálhatja a widget és a cella tartalmának érkezését és
107
+ távozását:
108
+
109
+ ```css
110
+ .contact-card { animation: contact-card-enter 300ms ease-out backwards; }
111
+ .sb4-html-leaving .contact-card { animation: contact-card-leave 150ms ease-in forwards; }
112
+
113
+ @keyframes contact-card-enter { from { opacity: 0; transform: translateY(12px); } }
114
+ @keyframes contact-card-leave { to { opacity: 0; transform: translateY(-8px); } }
115
+
116
+ @media (prefers-reduced-motion: reduce) {
117
+ .contact-card, .sb4-html-leaving .contact-card { animation: none; }
118
+ }
119
+ ```
120
+
121
+ - **Belépés**: a kiírt html új elemei a szokásos módon animálódnak. Ez minden olyan renderkor
122
+ lefut, amely **új html-t ír**: az első megjelenéskor, és valahányszor a feloldás eredménye
123
+ megváltozik (a modell vagy a sor olyan mezője változott, amit a html olvas). A változatlan
124
+ eredményű push semmit nem ír, ott nem fut.
125
+ - **Kilépés**: mielőtt a kliens a képernyőn lévő html-t lecseréli, a widget, illetve a cella
126
+ elemére (`smart-html-widget`, `smart-html-cell`) felteszi az **`sb4-html-leaving`** osztályt.
127
+ Ha egy stíluslap erre az osztályra animációt vagy transitiont indít, a kliens megvárja a
128
+ végét, és csak utána írja ki az új html-t (és veszi le az osztályt). Ha semmi nem indul, azonnal
129
+ ír: kilépő CSS nélkül minden úgy működik, mint eddig.
130
+ - A várakozás **legfeljebb 600 ms**: egy végtelen vagy túl hosszú kilépő animáció sem tartja fel
131
+ a frissítést. Csak az osztály által **elindított** animáció számít; ami már futott (pl. egy még
132
+ tartó belépő vagy egy végtelen pörgés), az nem.
133
+ - Kilépés közben érkező újabb renderből **az utolsó** html kerül ki. Ha a feloldás közben
134
+ visszatér a képernyőn lévő html-re, a kilépés elmarad, az osztály lekerül.
135
+ - Az első megjelenésnek nincs kilépése, csak belépése. Ugyanígy, ha a nézet layoutja cserélődik,
136
+ a widget újraépül: a régi elem kilépés nélkül tűnik el, az új belép.
137
+ - A kilépés alatt a régi html és a hidratált toolbarjai még élnek és kattinthatók. A kilépő
138
+ animációt ezért érdemes röviden tartani, és `forwards` kitöltéssel a végállapotban hagyni, hogy
139
+ az új html kiírásáig ne villanjon vissza.
140
+ - 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
141
+ ismét mozgathatja az elemet (a `both` vagy a `forwards` animációs értéke felülírná őket).
142
+
143
+ ## A szerver-fázis
144
+
145
+ A szerver-helyőrző a platform template-dialektusa: `${alias:/útvonal#mező?stratégia}`, mellette a
146
+ `data-sb4-context` (iteráció) és a `data-sb4-if` (feltétel) attribútum. A builder a **nyitásáról**
147
+ ismeri fel — `${alias:/` —, a sima `${name}` neki és a motornak is közömbös szöveg. Literálként
148
+ `&#36;{`-nak írd.
149
+
150
+ Három út oldja fel, és **egyik sem fut magától**:
151
+
152
+ | Út | Mikor | Hiba esetén |
153
+ |---|---|---|
154
+ | `resolveTemplate(ctx, …)`, és az eredmény megy a `html(...)`-be | a build előtt | elnyel: logol, és a kulcsot vagy üres stringet ad vissza |
155
+ | `h.template(ctx, KEY)` | a widget felépítésekor, kulcsból (lásd *Stíluslap a template-kulcs mellől*) | dob |
156
+ | `h.resolveLater()` + `resolveHtml(ctx, layout)` | a **felépített layouton** | dob |
157
+
158
+ ```java
159
+ SmartComponentLayoutDefinition layout = layoutBuilder(view)
160
+ .vForm(f -> f.html(h -> h.html(CARD_HTML).resolveLater()))
161
+ .build();
162
+ resolveHtml(objectApi.contextObject().set("contact", contact), layout); // PageApiImpl
163
+ ```
164
+
165
+ - A `resolveHtml` bejárja a layout `FORM` / `CONTAINER` fáját és a widgetek gyerek-widgetjeit, és
166
+ minden HTML widget `html`-jét feloldja **ugyanazzal a ctx-szel**. A `css`, a `root` és a `fields`
167
+ marad. A gridekhez nem nyúl — egy sor-layout maga is layout, arra külön hívható. Page-en kívül:
168
+ `ObjectLayouts.resolveHtml(layout, resolver, objectApi, ctx)`; listára is van változata.
169
+ - **Amiben nincs `${alias:/`, ahhoz hozzá sem ér** — az a html nem megy át a motoron, tehát nem is
170
+ normalizálódik. Ezért bármilyen layoutra ráhívható, és a második futás egy már feloldott
171
+ widgeten nem csinál semmit.
172
+ - **Helyben módosít.** Ha az adat változik, **építsd újra a layoutot, ne oldd fel újra**: a
173
+ view-ban tárolt, egyszer már feloldott layoutban nincs mit feloldani, az új ctx-szel hívott
174
+ második futás némán a régi értékeket hagyja. Aki a feloldatlan eredetit meg akarja tartani,
175
+ másolaton hívja.
176
+ - **Hangosan bukik** (`IllegalStateException`): ha van `${alias:/`, de nincs template-modul a
177
+ hostban; és ha a feloldás **után** is maradt — a motor az ismeretlen aliasú helyőrzőt ugyanis
178
+ szó nélkül benne hagyja. A motor másik két csendes esete megmarad: a hiányzó / `null` mező
179
+ helyére szóköz kerül, a mezőszintű feloldási hiba helyére a „Feldolgozási hiba" szöveg.
180
+
181
+ **A `build()` őre.** Az a `${alias:/`, amit a három út egyike sem oldott fel, régen szó nélkül
182
+ kiment a kliensre, és nyersen megjelent. Most a `HtmlWidgetBuilder.build()` `IllegalStateException`-t
183
+ dob rá, kivéve ha a widget `resolveLater()`-t mondott. Ez a `vf.html(String)`, az
184
+ `ObjectLayoutBuilder.html(...)` és a `GridBuilder.htmlColumn(...)` útjára is igaz.
185
+
186
+ **Üres ctx-szel semmi nem oldódik fel**: a `${…}` literál marad (és a `build()` elbukik rajta), a
187
+ `data-sb4-context` elemek eltűnnek. A grid-szintű sor-layoutnak és az oszlop-sablonnak nincs
188
+ szerver-oldali, soronkénti kontextusa — ott a sorból jövő érték `{{…}}`, a szerveren számolt érték
189
+ pedig a sor egy mezője legyen.
190
+
191
+ ## A kliens-helyőrzők nyelvtana
192
+
193
+ Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
194
+ `HtmlTemplateScanner` is, ezért a hibás template már a page felépítésekor elbukik.
195
+
196
+ ### Tag és path
197
+
198
+ ```
199
+ {{path}} érték
200
+ {{path | pipe:'arg' | pipe2}} érték formázó pipe-okkal
201
+ {{#path}} … {{/path}} belépő szekció
202
+ {{?path}} … {{/path}} őr: van érték
203
+ {{^path}} … {{/path}} őr: nincs érték
204
+ {{.}} / {{. | pipe}} az aktuális scope maga
205
+ {{../path}} path a körülvevő belépő szekció scope-jából
206
+ ```
207
+
208
+ - A tagen belül a szóköz megengedett (`{{ name }}`, `{{# parties }}`).
209
+ - **Path**: `(../)*azonosító(.azonosító)*`, az azonosító `[A-Za-z_][A-Za-z0-9_]*`. **Nincs** index
210
+ (`list[0]`), abszolút út, `{{..}}` és a path közepén álló `../` (`a/../b`).
211
+ - Helyőrző text-node-ban és attribútum-**értékben** állhat (`class="status-{{status}}"`); tag- vagy
212
+ attribútum-névben nem.
213
+ - **Nincs** raw (`{{{x}}}`, `{{&x}}` → hiba), partial, komment, delimiter-váltás, `@index`.
214
+ - Literál `{{` a html-ben: `&#123;&#123;`.
215
+
216
+ ### Értékek
217
+
218
+ - Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
219
+ - Primitív → `String(v)` (`false` → `false`, `0` → `0`).
220
+ - Objektum vagy lista `{{p}}`-ként → üres + `console.warn`.
221
+ - Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
222
+ nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
223
+
224
+ ### Szekciók — a jel dönt, nem az érték
225
+
226
+ | Jel | Név | Mit csinál |
227
+ |---|---|---|
228
+ | `{{#p}}` | **belépő** szekció | lista: elemenként egyszer, a scope az elem; objektum: egyszer, a scope az objektum |
229
+ | `{{?p}}` | **őr** | egyszer renderel, ha van érték; **a scope nem mozdul** |
230
+ | `{{^p}}` | hamis-őr | egyszer renderel, ha nincs érték; a scope nem mozdul |
231
+
232
+ - **„Nincs érték"** = hiányzó, `null`, `false`, `""`, üres lista. **A `0` érték** (szándékos eltérés
233
+ a Mustache-től: `{{?count}}` a nullát ne rejtse el).
234
+ - **Szigorú scope**: belépő szekción belül a path csak az aktuális elemben oldódik, a szülő mezőit
235
+ magától nem éri el (nincs Mustache-féle felfelé keresés — attól a szerver mezőlistája az adattól
236
+ függne). A körülvevő scope **kimondva** érhető el: `{{../path}}`.
237
+ - **`../`**: minden `../` egy **belépő** szekcióval lép kijjebb. A `?` és `^` őr nem szint (a scope-ot
238
+ sem mozdítja), átlát rajta. Érték-tagen, pipe-pal, attribútum-értékben és szekció-tagen is állhat
239
+ (`{{#../members}}…{{/../members}}` — a záró tag ugyanazt a pathot ismétli). A widget `root`-ja
240
+ fölé nem megy: több `../`, mint ahány belépő szekció nyitva van, hiba.
241
+ - Szekció-tagen nincs pipe.
242
+ - `{{#p}}` egyetlen értéken (szám, string): a kliens őrként rendereli és `console.warn`-t ír — a
243
+ Java-builder ezt nem látja előre (nincs adata), ilyenkor `{{?p}}` kell. A `../`-nek ez is egy
244
+ szint (a scope-ja a körülvevő), így a kliens és a szerver mezőlistája egyezik.
245
+ - A szekció-tag üres stringre cserélődik, a körülötte lévő whitespace marad.
246
+
247
+ ```html
248
+ <ul>
249
+ {{#parties}}
250
+ <li>{{displayName}} {{#roles}}<span>{{. | label:'BusinessRole'}}</span>{{/roles}}</li>
251
+ {{/parties}}
252
+ </ul>
253
+ {{^parties}}<p>— nincs érintett —</p>{{/parties}}
254
+ {{?note}}<p class="note">{{note}}</p>{{/note}}
255
+ ```
256
+
257
+ A kártya saját mezői a lista elemein belül, `root = "card"` mellett:
258
+
259
+ ```html
260
+ <ul>
261
+ {{#parties}}
262
+ <li data-sb4-action="REMOVE_PARTY" data-sb4-action-identifier="{{../id}}_{{id}}">
263
+ {{displayName}} — {{../title}}
264
+ {{#roles}}<span>{{. | label:'BusinessRole'}} ({{../displayName}})</span>{{/roles}}
265
+ </li>
266
+ {{/parties}}
267
+ </ul>
268
+ ```
269
+
270
+ A származtatott mezők: `card.parties`, `card.id`, `card.parties.id`, `card.parties.displayName`,
271
+ `card.title`, `card.parties.roles` — a `{{../displayName}}` a `roles`-on belülről a
272
+ `card.parties.displayName`, a `{{../title}}` a `card.title`.
273
+
274
+ ### Formázó pipe-ok
275
+
276
+ `| név` vagy `| név:'arg'`; az argumentum egyetlen, **egyszeres idézőjelű** literál (`\'` és `\\`
277
+ escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en megy át. A készlet
278
+ **zárt**:
279
+
280
+ | Pipe | Argumentum | Default | Szemantika |
281
+ |---|---|---|---|
282
+ | `date` | date-fns formátum | `'yyyy.MM.dd'` | ISO string → böngésző-időzóna szerinti dátum |
283
+ | `datetime` | date-fns formátum | `'yyyy.MM.dd H:mm'` | ugyanaz |
284
+ | `time` | date-fns formátum | `'H:mm'` | ugyanaz; a csak-idő (`12:34:56`) értéket is olvassa |
285
+ | `number` | Angular `digitsInfo` | `'1.0-3'` | `Intl.NumberFormat` a session locale-jával; szám és numerikus string |
286
+ | `label` | a valueSet neve (**kötelező**) | — | kód → címke a kontextus `valueSets`-éből |
287
+ | `default` | literál | `''` | üres / hiányzó érték helyett a literál |
288
+ | `upper` / `lower` | nincs | — | kis/nagybetű a session locale-jával |
289
+
290
+ - A defaultok a grid cella-defaultjai: a kártya és a tábla ugyanazt mutatja.
291
+ - **Dátum**: a dróton nem minden dátum zulu (`…Z` / offset, offset nélküli `LocalDateTime`,
292
+ `LocalDate`, `LocalTime`) — mindet olvassa. Nem ISO érték → nyers érték + warn.
293
+ **A date-fns `YYYY` / `DD` tokenje hibás**: `yyyy` / `dd` kell.
294
+ - **`label`**: a kulcs-mező `valueSetData.keyProperty`, különben `objectUri`, különben `uri`; a címke
295
+ `displayValue`, különben `name` — ugyanaz a lánc, mint a form select-jeinél. Nincs valueSet vagy
296
+ nincs találat → a nyers kód. A valueSetnek az oldal `ComponentModel.valueSets`-ében kell lennie.
297
+ - Üres értéket a `default` kivételével minden pipe érintetlenül enged tovább.
298
+
299
+ ### Hibák
300
+
301
+ | Hol | Mi történik |
302
+ |---|---|
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` |
304
+ | Java-builder (`build()`), `resolveHtml` | `IllegalStateException`: feloldatlan szerver-helyőrző (`${alias:/`), lásd *A szerver-fázis* |
305
+ | kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
306
+
307
+ Egyetlen szándékos különbség: az ismeretlen pipe-név a kliensen csak warn (az érték formázatlanul
308
+ megy ki), hogy egy régebbi kliens ne törjön el egy újabb szerver pipe-ján.
309
+
310
+ ### Szerzői szabályok a szerver-fázis miatt
311
+
312
+ Ha a html átmegy a szerver template-motorján (Jsoup), akkor:
313
+
314
+ - **egy gyökérelem kötelező** — a gyökér-szintű szöveg és szekció-tag eldobódik;
315
+ - táblázatban **explicit `<tbody>`** kell, különben a parser a `{{#rows}}` után szúr egyet;
316
+ - a whitespace-re (`:empty`, `white-space: pre`, `&nbsp;`) ne építs, a motor átírja;
317
+ - a szerverről behelyettesített **érték ne tartalmazzon `{{`-t**: a motor HTML-escape-eli, de a
318
+ `{{` átmegy rajta, és a kliens helyőrzőnek olvasná (a néző saját adatán oldaná fel);
319
+ - `?stratégia` után is állhat `{{…}}` — a stratégia a `{{`-on nem nyúl át.
320
+
321
+ ## Toolbar-slot
322
+
323
+ ```html
324
+ <smart-ui-action-toolbar data-sb4-toolbar="ROW_TOOLBAR"
325
+ data-sb4-toolbar-direction="HORIZONTAL" data-sb4-toolbar-alignment="END"
326
+ data-sb4-toolbar-scrollable></smart-ui-action-toolbar>
327
+ ```
328
+
329
+ A kliens a marker-elembe egy **valódi toolbart hidratál**, amely a `data-sb4-toolbar` alatti
330
+ címre (`uiAction.toolbar == id`) címzett akciókat rajzolja ki — ugyanazokkal a szabályokkal, mint
331
+ 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,
332
+ nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarProperties)`.
333
+
334
+ - A marker **maga a `<smart-ui-action-toolbar>` tag** legyen: a hostok tag-szelektorral stílusozzák
335
+ a toolbart, egy `<div data-sb4-toolbar>` működne, de a host-CSS nem érné el.
336
+ - Üres tartalommal írd (a hidratálás úgyis kitörli); a saját `class` / `style` attribútumaid
337
+ megmaradnak.
338
+ - `direction`: `HORIZONTAL` | `VERTICAL`, `alignment`: `START` | `END`, a `scrollable` logikai
339
+ attribútum. Ismeretlen érték → warn, a tulajdonság kimarad.
340
+ - A helyőrzők feloldása **megelőzi** a hidratálást, tehát az id is lehet sablon:
341
+ `data-sb4-toolbar="addr_{{id}}_toolbar"` — szekcióban elemenként más toolbar.
342
+ - Üres vagy üresre feloldott id → warn, a slot kimarad. Ismétlődő marker → markerenként saját
343
+ toolbar.
344
+ - A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
345
+ eredménynél a hidratált toolbarok élnek tovább.
346
+
347
+ ## Akció-trigger
348
+
349
+ ```html
350
+ <div class="card" data-sb4-action="OPEN_ENTRY">…</div>
351
+ <li data-sb4-action="REMOVE_MEMBER" data-sb4-action-identifier="{{id}}">…</li>
352
+ ```
353
+
354
+ Bármely elem hordozhat triggert: a kattintás (vagy Enter / Space) azt az akciót futtatja, amelynek
355
+ a `code`-ja — és ha meg van adva, az `identifier`-e — egyezik. Az akciót a kliens **kattintáskor**
356
+ keresi meg ugyanabban a listában, amiből az ott lévő toolbarok húznak, **a toolbar-címtől
357
+ függetlenül**. Hiányzó `data-sb4-action-identifier` = identifier nélküli akció. Java:
358
+ `Tag.action(code)` / `Tag.action(code, identifier)`.
359
+
360
+ - **Lista-szekcióban elemenként egy akció kell a szerveren.** A `{{id}}`-s identifier címez, nem
361
+ paraméterez: a kliens a (kód, identifier) párt keresi a listában, tehát a page minden elemhez
362
+ 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
363
+ közt). Egyetlen, identifier nélküli akció mellett az identifieres triggerek nem interaktívak.
364
+ - **A gyökérelemre tett trigger a `DIV` widget `onClick`-jének megfelelője.** A HTML widgetnek
365
+ nincs saját kattintása.
366
+ - **Csak-trigger akció**: ha egy nézet-szintű akciót csak trigger lőhet, a címe legyen
367
+ `UiActions.NO_TOOLBAR` (`"_none"`) — olyan cím, amit egyetlen toolbar sem visel, így az id nélküli
368
+ toolbarok sem rajzolják ki gombként. Kártyán (sorakciónál) nem kell.
369
+ - A trigger nem enged tovább se klikket, se dupla klikket a körülötte lévő elemeknek: kártyán nem
370
+ jelöl ki és nem futtat default akciót. Beágyazott triggereknél a legbelső nyer.
371
+ - `<a href>`-re tett triggernél az akció nyer, a link nem navigál.
372
+ - **a11y**: nem natív elem (`div`, `span`, `li`…) `role="button"`-t és `tabindex="0"`-t kap, ha a
373
+ szerző nem adott mást; natív elemhez (`button`, `a[href]`, `input`…) a kliens nem nyúl.
374
+ - **Letiltott akció**: `aria-disabled="true"` + `sb4-action-disabled` osztály, a kattintás no-op; az
375
+ állapot a lista változását a html újraírása nélkül követi.
376
+ - **Nem kínált akció** (a sor / nézet nem adja): az elem nem interaktív, `sb4-action-inert` osztályt
377
+ kap, a kliens egyszer warn-ol, a kattintás a körülötte lévő triggerre vagy a kártyára jut.
378
+ - Paraméter-attribútum nincs: az identifier és a sor-scope (`params.model` = a sor) fedi az
379
+ eseteket.
380
+
381
+ ## Sor-layoutok a kártya-griden
382
+
383
+ A `GridViewDescriptor.rowLayouts` kulcsonként egy-egy layoutot visz **gridenként egyszer**; a
384
+ kliens soronként rendereli a sor `data`-jával. Csak `kind == CARDS` mellett él.
385
+
386
+ ```java
387
+ gridBuilder.kind(KindEnum.CARDS)
388
+ .rowLayout(layoutBuilder(view).vForm(f -> f.html(h -> h.html(CARD_HTML))).build()) // default
389
+ .rowLayout("DISTRIBUTION_LIST", listLayout); // kulcsos
390
+ ```
391
+
392
+ Melyik layouttal renderel egy sor, sorrendben:
393
+
394
+ 1. a sor saját `layoutDescriptor.componentLayouts.GRID_ROW_LAYOUT`-ja (soronkénti felülbírálás);
395
+ 2. a `rowLayouts[row.rowLayout]`;
396
+ 3. a **default sor-layout**: a `"default"` kulcs (`Layouts.DEFAULT_LAYOUT`), vagy ha csak egy
397
+ bejegyzés van, az;
398
+ 4. a host `<gridId>Card` néven regisztrált kártya-komponense;
399
+ 5. semmi.
400
+
401
+ Ismeretlen kulcs → a default + egy `console.warn` (grid, sor, kulcs): az elgépelt kulcs nem tüntet
402
+ el kártyát, de látszik.
403
+
404
+ **Oszlopok.** A kártyán olvasott mezőket a grid oszlopainak kell adniuk: a `GridBuilder.build()`
405
+ WARN-t ír minden olyan mezőre, amit egy oszlop sem fed (név vagy dot-prefix szerint: a `card`
406
+ oszlop fedi a `card.parties.displayName`-et). A mezőlista ellenőriz, nem választ oszlopot.
407
+ Szerver-származtatott mező (pl. `partyCount`) legyen valódi oszlop.
408
+
409
+ **Kattintás** (csak a layoutból renderelt kártyán; a host-regisztrált kártya maga kezeli a saját
410
+ klikkjeit). Két független tengely, ugyanaz a modell, mint a táblán:
411
+
412
+ - **a kijelölést a `selectionMode` adja** (hiányzó = `NONE` = nincs kijelölés);
413
+ - **a default sor-akció gesztusát a `GridViewDescriptor.defaultRowActionTrigger`**
414
+ (`click` | `doubleClick`), Java: `GridBuilder.defaultRowActionTrigger(...)`.
415
+
416
+ | `defaultRowActionTrigger` | klikk | dupla klikk |
417
+ |---|---|---|
418
+ | nincs megadva (kártyán = `click`) | a sor **első** illő default akciója | — |
419
+ | `click` | default akció; több illő akciónál **menü az egérnél** | — |
420
+ | `doubleClick` | kijelölés, ha a grid és a sor jelölhető | default akció; több illőnél menü |
421
+
422
+ - **A klikkes trigger elviszi a klikket a kijelölés elől**: ha a sornak van default akciója és a
423
+ trigger `click` (vagy nincs megadva), a kártya klikkre nem jelölhető ki, akármi a
424
+ `selectionMode`. Kártya-kijelöléshez `doubleClick` trigger kell, vagy default akció nélküli grid.
425
+ - „Illő" default akció: a grid `defaultRowActions` kódjai közül az, amit a sor `actions`-e kínál —
426
+ a menü tehát soronként más lehet. Megadott trigger mellett egy illő akció azonnal elsül.
427
+ - A dupla klikk második klikkje nem számít külön klikknek: nem nyit kétszer, és nem vonja vissza
428
+ az első klikk kijelölését (dupla klikk = kijelöl + megnyit). Amíg a default akció fut, az újabb
429
+ klikk is eldobódik.
430
+ - A kijelölt kártya hostja `selected` osztályt és egy minimális, felülírható outline-t kap. A
431
+ kijelölés toggle, mint a táblán; `selectionType: CHECKBOX` a kártyán nem rajzol checkboxot.
432
+ - A toolbar-gomb és a trigger megtartja magának a kattintást.
433
+ - A tábla gesztusa változatlanul a dupla klikk, a mező értékétől függetlenül.
434
+
435
+ ## Html a tábla cellájában
436
+
437
+ A tábla egy cellája kétféleképpen kaphat html-t, és a kettő **más bizalmi szinten** áll.
438
+
439
+ | | HTML column | Column template |
440
+ |---|---|---|
441
+ | Honnan jön a html | a cella **értéke**, adatként (`GridRow.data`) | az oszlop **sablonja** (`GridColumnMeta.htmlProperties`) |
442
+ | Ki írta | bármi, ami az adatot előállítja | a backend, mint a HTML widgetnél |
443
+ | Kiírás | **sanitizált** innerHTML (Angular default) | változtatás nélkül, a behelyettesített értékek escape-elve |
444
+ | Helyőrzők, toolbar-slot, trigger | nincs (a `data-*` és az ismeretlen tag kiesik) | mind, mint a widgetben |
445
+
446
+ ### HTML column és `contentType`
447
+
448
+ A `GridColumnMeta.contentType` mondja meg, hogyan kerül ki az érték:
449
+
450
+ - **hiányzó** vagy `html` — sanitizált innerHTML, ahogy a tábla mindig is írta. Semmi nem változik
451
+ annak a hostnak, amelyik nem állítja.
452
+ - `text` — **escape-elt** szöveg: a `<b>` betűkként látszik. Erre való minden adat, amiben
453
+ felhasználói szöveg van és nem jelölésként kell olvasni.
454
+
455
+ A rendezés a szerveren a **nyers** értéken fut, ezért html-oszlophoz adj
456
+ `sortOrderPropertyName`-et. Java:
457
+
458
+ ```java
459
+ gridBuilder.contentType(GridColumnContentType.TEXT, "name", "comment")
460
+ ```
461
+
462
+ ### Column template
463
+
464
+ ```java
465
+ gridBuilder.htmlColumn("name", h -> h.html(t -> t
466
+ .b(b -> b.text("{{name}}"))
467
+ .span(s -> s.action("OPEN").text("megnyitás"))
468
+ .toolbar("ROW_TOOLBAR")))
469
+ ```
470
+
471
+ - **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, és
472
+ ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
473
+ `valueSets` a gridet mutató oldalé.
474
+ - A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
475
+ checkbox- és ikon-formázást is — a formázás a sablon pipe-jaiba kerül. A sor ikonjai
476
+ (`GridRow.icons`) és az oszlop cella-toolbarja (`columnActions`) mellette megmaradnak. Ha van
477
+ sablon, a `contentType` nem számít.
478
+ - A toolbar-slot és a trigger **a sor akcióit** látja (`row.actions`), pontosan úgy, mint a
479
+ sor-layouttal renderelt kártya: az akció modellje a sor, a scope a grid és a sor. A trigger a
480
+ kattintást és a dupla kattintást megtartja magának, a sor-klikk nem fut.
481
+ - **Sor-menü**: a csak-trigger akció (`UiActions.NO_TOOLBAR`) sosem kerül a sor menüjébe. Ha a
482
+ táblában van oszlop-sablon, akkor a **toolbar-címes** sorakció sem — az egy sablonbeli slot-é.
483
+ Sablon nélküli táblán a címes sorakció a menüben marad, ahogy eddig.
484
+ - A cella a sor minden cseréjekor újra felold; változatlan eredménynél a html és a hidratált
485
+ toolbar él tovább. Teljesítmény-kalap nincs: nagy táblán a sablon soronként egyszer oldódik fel,
486
+ hidratálás csak ott fut, ahol slot van.
487
+ - A `GridBuilder.build()` a sablon mezőit is ellenőrzi az oszlopokkal szemben, ugyanazzal a
488
+ WARN-nal, mint a sor-layoutét (`column '<név>' reads '<mező>' which no column covers`). Az
489
+ ismeretlen oszlopra adott sablon vagy `contentType` szintén WARN.
490
+ - A `GridBuilder`-nek nincs template-feloldója: a `template(ctx, KEY)` itt nem működik. Kulcsból
491
+ épített sablonhoz építsd a widgetet a page layout-builderével, és add át a `properties(...)`-szel.
492
+
493
+ ## Példa: egy kártya
494
+
495
+ ```html
496
+ <div class="contact-card contact-card-{{status}}" data-sb4-action="OPEN_CONTACT_ENTRY">
497
+ <div class="contact-card-main">
498
+ <div class="contact-card-icon contact-card-icon-{{channelType}}"></div>
499
+ <div class="contact-card-value">{{value}}</div>
500
+ <div class="contact-card-channel-type">{{channelType | label:'ContactChannelType'}}</div>
501
+ <div class="contact-card-modified">{{modifiedAt | datetime}}</div>
502
+ <smart-ui-action-toolbar data-sb4-toolbar="CONTACT_ROW_TOOLBAR"></smart-ui-action-toolbar>
503
+ </div>
504
+ <div class="contact-card-associations">
505
+ <div class="contact-card-associations-title">Érintettek ({{partyCount}})</div>
506
+ {{#parties}}
507
+ <div class="contact-card-related-party">
508
+ <div class="contact-card-related-party-initials">{{monogram}}</div>
509
+ <div class="contact-card-related-party-company">{{displayName}}</div>
510
+ <div>{{#businessRoles}}<span>{{. | label:'BusinessRole'}}</span> {{/businessRoles}}</div>
511
+ </div>
512
+ {{/parties}}
513
+ {{^parties}}<div class="contact-card-associations-title">— nincs érintett —</div>{{/parties}}
514
+ </div>
515
+ </div>
516
+ ```
517
+
518
+ `root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
519
+ szándékosan nem tud, annak a helye:
520
+
521
+ - **ikon / értékfüggő megjelenés**: szerver-származtatott mező, vagy érték-alapú CSS-osztály
522
+ (`contact-card-icon-{{channelType}}`);
523
+ - **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
524
+ logikai mező + `{{?…}}` őr;
525
+ - **darabszám** (`parties.size()`): valódi oszlop (`partyCount`);
526
+ - **fordítás**: `label` pipe valueSetből, vagy a szerver-fázis `${…}`-e.