@smartbit4all/ng-client 7.1.1 → 7.2.0
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 +458 -0
- package/MIGRATION-7.0.md +45 -1
- package/WIDGETS.md +10 -1
- package/fesm2022/smartbit4all-ng-client.mjs +1133 -183
- package/fesm2022/smartbit4all-ng-client.mjs.map +1 -1
- package/package.json +1 -1
- package/smartbit4all-ng-client-7.2.0.tgz +0 -0
- package/types/smartbit4all-ng-client.d.ts +137 -42
- package/smartbit4all-ng-client-7.1.1.tgz +0 -0
package/HTML-WIDGET.md
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
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 kliens-helyőrzők nyelvtana
|
|
144
|
+
|
|
145
|
+
Logika-mentes Mustache-részhalmaz. Ugyanezt a nyelvtant olvassa a Java-oldali
|
|
146
|
+
`HtmlTemplateScanner` is, ezért a hibás template már a page felépítésekor elbukik.
|
|
147
|
+
|
|
148
|
+
### Tag és path
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
{{path}} érték
|
|
152
|
+
{{path | pipe:'arg' | pipe2}} érték formázó pipe-okkal
|
|
153
|
+
{{#path}} … {{/path}} belépő szekció
|
|
154
|
+
{{?path}} … {{/path}} őr: van érték
|
|
155
|
+
{{^path}} … {{/path}} őr: nincs érték
|
|
156
|
+
{{.}} / {{. | pipe}} az aktuális scope maga
|
|
157
|
+
{{../path}} path a körülvevő belépő szekció scope-jából
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- A tagen belül a szóköz megengedett (`{{ name }}`, `{{# parties }}`).
|
|
161
|
+
- **Path**: `(../)*azonosító(.azonosító)*`, az azonosító `[A-Za-z_][A-Za-z0-9_]*`. **Nincs** index
|
|
162
|
+
(`list[0]`), abszolút út, `{{..}}` és a path közepén álló `../` (`a/../b`).
|
|
163
|
+
- Helyőrző text-node-ban és attribútum-**értékben** állhat (`class="status-{{status}}"`); tag- vagy
|
|
164
|
+
attribútum-névben nem.
|
|
165
|
+
- **Nincs** raw (`{{{x}}}`, `{{&x}}` → hiba), partial, komment, delimiter-váltás, `@index`.
|
|
166
|
+
- Literál `{{` a html-ben: `{{`.
|
|
167
|
+
|
|
168
|
+
### Értékek
|
|
169
|
+
|
|
170
|
+
- Hiányzó, `null` vagy `null`-on átvezető path → **üres string, csendben**.
|
|
171
|
+
- Primitív → `String(v)` (`false` → `false`, `0` → `0`).
|
|
172
|
+
- Objektum vagy lista `{{p}}`-ként → üres + `console.warn`.
|
|
173
|
+
- Minden behelyettesített érték HTML-escape (`& < > " '`). URL-attribútumban (`href="{{url}}"`)
|
|
174
|
+
nincs séma-szűrés: az érték a szerver saját modelljéből jön, a szerző felel érte.
|
|
175
|
+
|
|
176
|
+
### Szekciók — a jel dönt, nem az érték
|
|
177
|
+
|
|
178
|
+
| Jel | Név | Mit csinál |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `{{#p}}` | **belépő** szekció | lista: elemenként egyszer, a scope az elem; objektum: egyszer, a scope az objektum |
|
|
181
|
+
| `{{?p}}` | **őr** | egyszer renderel, ha van érték; **a scope nem mozdul** |
|
|
182
|
+
| `{{^p}}` | hamis-őr | egyszer renderel, ha nincs érték; a scope nem mozdul |
|
|
183
|
+
|
|
184
|
+
- **„Nincs érték"** = hiányzó, `null`, `false`, `""`, üres lista. **A `0` érték** (szándékos eltérés
|
|
185
|
+
a Mustache-től: `{{?count}}` a nullát ne rejtse el).
|
|
186
|
+
- **Szigorú scope**: belépő szekción belül a path csak az aktuális elemben oldódik, a szülő mezőit
|
|
187
|
+
magától nem éri el (nincs Mustache-féle felfelé keresés — attól a szerver mezőlistája az adattól
|
|
188
|
+
függne). A körülvevő scope **kimondva** érhető el: `{{../path}}`.
|
|
189
|
+
- **`../`**: minden `../` egy **belépő** szekcióval lép kijjebb. A `?` és `^` őr nem szint (a scope-ot
|
|
190
|
+
sem mozdítja), átlát rajta. Érték-tagen, pipe-pal, attribútum-értékben és szekció-tagen is állhat
|
|
191
|
+
(`{{#../members}}…{{/../members}}` — a záró tag ugyanazt a pathot ismétli). A widget `root`-ja
|
|
192
|
+
fölé nem megy: több `../`, mint ahány belépő szekció nyitva van, hiba.
|
|
193
|
+
- Szekció-tagen nincs pipe.
|
|
194
|
+
- `{{#p}}` egyetlen értéken (szám, string): a kliens őrként rendereli és `console.warn`-t ír — a
|
|
195
|
+
Java-builder ezt nem látja előre (nincs adata), ilyenkor `{{?p}}` kell. A `../`-nek ez is egy
|
|
196
|
+
szint (a scope-ja a körülvevő), így a kliens és a szerver mezőlistája egyezik.
|
|
197
|
+
- A szekció-tag üres stringre cserélődik, a körülötte lévő whitespace marad.
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<ul>
|
|
201
|
+
{{#parties}}
|
|
202
|
+
<li>{{displayName}} {{#roles}}<span>{{. | label:'BusinessRole'}}</span>{{/roles}}</li>
|
|
203
|
+
{{/parties}}
|
|
204
|
+
</ul>
|
|
205
|
+
{{^parties}}<p>— nincs érintett —</p>{{/parties}}
|
|
206
|
+
{{?note}}<p class="note">{{note}}</p>{{/note}}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
A kártya saját mezői a lista elemein belül, `root = "card"` mellett:
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<ul>
|
|
213
|
+
{{#parties}}
|
|
214
|
+
<li data-sb4-action="REMOVE_PARTY" data-sb4-action-identifier="{{../id}}_{{id}}">
|
|
215
|
+
{{displayName}} — {{../title}}
|
|
216
|
+
{{#roles}}<span>{{. | label:'BusinessRole'}} ({{../displayName}})</span>{{/roles}}
|
|
217
|
+
</li>
|
|
218
|
+
{{/parties}}
|
|
219
|
+
</ul>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A származtatott mezők: `card.parties`, `card.id`, `card.parties.id`, `card.parties.displayName`,
|
|
223
|
+
`card.title`, `card.parties.roles` — a `{{../displayName}}` a `roles`-on belülről a
|
|
224
|
+
`card.parties.displayName`, a `{{../title}}` a `card.title`.
|
|
225
|
+
|
|
226
|
+
### Formázó pipe-ok
|
|
227
|
+
|
|
228
|
+
`| név` vagy `| név:'arg'`; az argumentum egyetlen, **egyszeres idézőjelű** literál (`\'` és `\\`
|
|
229
|
+
escape-pel), a pipe-ok balról jobbra láncolhatók, a kimenetük is escape-en megy át. A készlet
|
|
230
|
+
**zárt**:
|
|
231
|
+
|
|
232
|
+
| Pipe | Argumentum | Default | Szemantika |
|
|
233
|
+
|---|---|---|---|
|
|
234
|
+
| `date` | date-fns formátum | `'yyyy.MM.dd'` | ISO string → böngésző-időzóna szerinti dátum |
|
|
235
|
+
| `datetime` | date-fns formátum | `'yyyy.MM.dd H:mm'` | ugyanaz |
|
|
236
|
+
| `time` | date-fns formátum | `'H:mm'` | ugyanaz; a csak-idő (`12:34:56`) értéket is olvassa |
|
|
237
|
+
| `number` | Angular `digitsInfo` | `'1.0-3'` | `Intl.NumberFormat` a session locale-jával; szám és numerikus string |
|
|
238
|
+
| `label` | a valueSet neve (**kötelező**) | — | kód → címke a kontextus `valueSets`-éből |
|
|
239
|
+
| `default` | literál | `''` | üres / hiányzó érték helyett a literál |
|
|
240
|
+
| `upper` / `lower` | nincs | — | kis/nagybetű a session locale-jával |
|
|
241
|
+
|
|
242
|
+
- A defaultok a grid cella-defaultjai: a kártya és a tábla ugyanazt mutatja.
|
|
243
|
+
- **Dátum**: a dróton nem minden dátum zulu (`…Z` / offset, offset nélküli `LocalDateTime`,
|
|
244
|
+
`LocalDate`, `LocalTime`) — mindet olvassa. Nem ISO érték → nyers érték + warn.
|
|
245
|
+
**A date-fns `YYYY` / `DD` tokenje hibás**: `yyyy` / `dd` kell.
|
|
246
|
+
- **`label`**: a kulcs-mező `valueSetData.keyProperty`, különben `objectUri`, különben `uri`; a címke
|
|
247
|
+
`displayValue`, különben `name` — ugyanaz a lánc, mint a form select-jeinél. Nincs valueSet vagy
|
|
248
|
+
nincs találat → a nyers kód. A valueSetnek az oldal `ComponentModel.valueSets`-ében kell lennie.
|
|
249
|
+
- Üres értéket a `default` kivételével minden pipe érintetlenül enged tovább.
|
|
250
|
+
|
|
251
|
+
### Hibák
|
|
252
|
+
|
|
253
|
+
| Hol | Mi történik |
|
|
254
|
+
|---|---|
|
|
255
|
+
| 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` |
|
|
256
|
+
| kliens | nem dob: `console.error`, és a html **feloldatlanul** kerül ki — látható hiba, nem üres kártya |
|
|
257
|
+
|
|
258
|
+
Egyetlen szándékos különbség: az ismeretlen pipe-név a kliensen csak warn (az érték formázatlanul
|
|
259
|
+
megy ki), hogy egy régebbi kliens ne törjön el egy újabb szerver pipe-ján.
|
|
260
|
+
|
|
261
|
+
### Szerzői szabályok a szerver-fázis miatt
|
|
262
|
+
|
|
263
|
+
Ha a html átmegy a szerver template-motorján (Jsoup), akkor:
|
|
264
|
+
|
|
265
|
+
- **egy gyökérelem kötelező** — a gyökér-szintű szöveg és szekció-tag eldobódik;
|
|
266
|
+
- táblázatban **explicit `<tbody>`** kell, különben a parser a `{{#rows}}` után szúr egyet;
|
|
267
|
+
- a whitespace-re (`:empty`, `white-space: pre`) ne építs, a motor átírja.
|
|
268
|
+
|
|
269
|
+
## Toolbar-slot
|
|
270
|
+
|
|
271
|
+
```html
|
|
272
|
+
<smart-ui-action-toolbar data-sb4-toolbar="ROW_TOOLBAR"
|
|
273
|
+
data-sb4-toolbar-direction="HORIZONTAL" data-sb4-toolbar-alignment="END"
|
|
274
|
+
data-sb4-toolbar-scrollable></smart-ui-action-toolbar>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A kliens a marker-elembe egy **valódi toolbart hidratál**, amely a `data-sb4-toolbar` alatti
|
|
278
|
+
címre (`uiAction.toolbar == id`) címzett akciókat rajzolja ki — ugyanazokkal a szabályokkal, mint
|
|
279
|
+
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,
|
|
280
|
+
nézeten az oldalét). Java: `Tag.toolbar(id)` / `Tag.toolbar(id, toolbarProperties)`.
|
|
281
|
+
|
|
282
|
+
- A marker **maga a `<smart-ui-action-toolbar>` tag** legyen: a hostok tag-szelektorral stílusozzák
|
|
283
|
+
a toolbart, egy `<div data-sb4-toolbar>` működne, de a host-CSS nem érné el.
|
|
284
|
+
- Üres tartalommal írd (a hidratálás úgyis kitörli); a saját `class` / `style` attribútumaid
|
|
285
|
+
megmaradnak.
|
|
286
|
+
- `direction`: `HORIZONTAL` | `VERTICAL`, `alignment`: `START` | `END`, a `scrollable` logikai
|
|
287
|
+
attribútum. Ismeretlen érték → warn, a tulajdonság kimarad.
|
|
288
|
+
- A helyőrzők feloldása **megelőzi** a hidratálást, tehát az id is lehet sablon:
|
|
289
|
+
`data-sb4-toolbar="addr_{{id}}_toolbar"` — szekcióban elemenként más toolbar.
|
|
290
|
+
- Üres vagy üresre feloldott id → warn, a slot kimarad. Ismétlődő marker → markerenként saját
|
|
291
|
+
toolbar.
|
|
292
|
+
- A kliens csak akkor írja újra a html-t, ha a feloldás eredménye **megváltozott**; változatlan
|
|
293
|
+
eredménynél a hidratált toolbarok élnek tovább.
|
|
294
|
+
|
|
295
|
+
## Akció-trigger
|
|
296
|
+
|
|
297
|
+
```html
|
|
298
|
+
<div class="card" data-sb4-action="OPEN_ENTRY">…</div>
|
|
299
|
+
<li data-sb4-action="REMOVE_MEMBER" data-sb4-action-identifier="{{id}}">…</li>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Bármely elem hordozhat triggert: a kattintás (vagy Enter / Space) azt az akciót futtatja, amelynek
|
|
303
|
+
a `code`-ja — és ha meg van adva, az `identifier`-e — egyezik. Az akciót a kliens **kattintáskor**
|
|
304
|
+
keresi meg ugyanabban a listában, amiből az ott lévő toolbarok húznak, **a toolbar-címtől
|
|
305
|
+
függetlenül**. Hiányzó `data-sb4-action-identifier` = identifier nélküli akció. Java:
|
|
306
|
+
`Tag.action(code)` / `Tag.action(code, identifier)`.
|
|
307
|
+
|
|
308
|
+
- **Lista-szekcióban elemenként egy akció kell a szerveren.** A `{{id}}`-s identifier címez, nem
|
|
309
|
+
paraméterez: a kliens a (kód, identifier) párt keresi a listában, tehát a page minden elemhez
|
|
310
|
+
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
|
|
311
|
+
közt). Egyetlen, identifier nélküli akció mellett az identifieres triggerek nem interaktívak.
|
|
312
|
+
- **A gyökérelemre tett trigger a `DIV` widget `onClick`-jének megfelelője.** A HTML widgetnek
|
|
313
|
+
nincs saját kattintása.
|
|
314
|
+
- **Csak-trigger akció**: ha egy nézet-szintű akciót csak trigger lőhet, a címe legyen
|
|
315
|
+
`UiActions.NO_TOOLBAR` (`"_none"`) — olyan cím, amit egyetlen toolbar sem visel, így az id nélküli
|
|
316
|
+
toolbarok sem rajzolják ki gombként. Kártyán (sorakciónál) nem kell.
|
|
317
|
+
- A trigger nem enged tovább se klikket, se dupla klikket a körülötte lévő elemeknek: kártyán nem
|
|
318
|
+
jelöl ki és nem futtat default akciót. Beágyazott triggereknél a legbelső nyer.
|
|
319
|
+
- `<a href>`-re tett triggernél az akció nyer, a link nem navigál.
|
|
320
|
+
- **a11y**: nem natív elem (`div`, `span`, `li`…) `role="button"`-t és `tabindex="0"`-t kap, ha a
|
|
321
|
+
szerző nem adott mást; natív elemhez (`button`, `a[href]`, `input`…) a kliens nem nyúl.
|
|
322
|
+
- **Letiltott akció**: `aria-disabled="true"` + `sb4-action-disabled` osztály, a kattintás no-op; az
|
|
323
|
+
állapot a lista változását a html újraírása nélkül követi.
|
|
324
|
+
- **Nem kínált akció** (a sor / nézet nem adja): az elem nem interaktív, `sb4-action-inert` osztályt
|
|
325
|
+
kap, a kliens egyszer warn-ol, a kattintás a körülötte lévő triggerre vagy a kártyára jut.
|
|
326
|
+
- Paraméter-attribútum nincs: az identifier és a sor-scope (`params.model` = a sor) fedi az
|
|
327
|
+
eseteket.
|
|
328
|
+
|
|
329
|
+
## Sor-layoutok a kártya-griden
|
|
330
|
+
|
|
331
|
+
A `GridViewDescriptor.rowLayouts` kulcsonként egy-egy layoutot visz **gridenként egyszer**; a
|
|
332
|
+
kliens soronként rendereli a sor `data`-jával. Csak `kind == CARDS` mellett él.
|
|
333
|
+
|
|
334
|
+
```java
|
|
335
|
+
gridBuilder.kind(KindEnum.CARDS)
|
|
336
|
+
.rowLayout(layoutBuilder(view).vForm(f -> f.html(h -> h.html(CARD_HTML))).build()) // default
|
|
337
|
+
.rowLayout("DISTRIBUTION_LIST", listLayout); // kulcsos
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Melyik layouttal renderel egy sor, sorrendben:
|
|
341
|
+
|
|
342
|
+
1. a sor saját `layoutDescriptor.componentLayouts.GRID_ROW_LAYOUT`-ja (soronkénti felülbírálás);
|
|
343
|
+
2. a `rowLayouts[row.rowLayout]`;
|
|
344
|
+
3. a **default sor-layout**: a `"default"` kulcs (`Layouts.DEFAULT_LAYOUT`), vagy ha csak egy
|
|
345
|
+
bejegyzés van, az;
|
|
346
|
+
4. a host `<gridId>Card` néven regisztrált kártya-komponense;
|
|
347
|
+
5. semmi.
|
|
348
|
+
|
|
349
|
+
Ismeretlen kulcs → a default + egy `console.warn` (grid, sor, kulcs): az elgépelt kulcs nem tüntet
|
|
350
|
+
el kártyát, de látszik.
|
|
351
|
+
|
|
352
|
+
**Oszlopok.** A kártyán olvasott mezőket a grid oszlopainak kell adniuk: a `GridBuilder.build()`
|
|
353
|
+
WARN-t ír minden olyan mezőre, amit egy oszlop sem fed (név vagy dot-prefix szerint: a `card`
|
|
354
|
+
oszlop fedi a `card.parties.displayName`-et). A mezőlista ellenőriz, nem választ oszlopot.
|
|
355
|
+
Szerver-származtatott mező (pl. `partyCount`) legyen valódi oszlop.
|
|
356
|
+
|
|
357
|
+
**Kattintás** (csak a layoutból renderelt kártyán; a host-regisztrált kártya maga kezeli a saját
|
|
358
|
+
klikkjeit):
|
|
359
|
+
|
|
360
|
+
- **klikk = kijelölés**, ha a grid `selectionMode`-ja nem `NONE` és a sor `selectable`; a kijelölt
|
|
361
|
+
kártya hostja `selected` osztályt és egy minimális, felülírható outline-t kap;
|
|
362
|
+
- **dupla klikk = a sor default akciója** (`defaultRowActions`);
|
|
363
|
+
- a toolbar-gomb és a trigger megtartja magának a kattintást.
|
|
364
|
+
|
|
365
|
+
Ha a kártyának „klikk = megnyitás" kell, tegyél a html gyökérelemére triggert.
|
|
366
|
+
|
|
367
|
+
## Html a tábla cellájában
|
|
368
|
+
|
|
369
|
+
A tábla egy cellája kétféleképpen kaphat html-t, és a kettő **más bizalmi szinten** áll.
|
|
370
|
+
|
|
371
|
+
| | HTML column | Column template |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| Honnan jön a html | a cella **értéke**, adatként (`GridRow.data`) | az oszlop **sablonja** (`GridColumnMeta.htmlProperties`) |
|
|
374
|
+
| Ki írta | bármi, ami az adatot előállítja | a backend, mint a HTML widgetnél |
|
|
375
|
+
| Kiírás | **sanitizált** innerHTML (Angular default) | változtatás nélkül, a behelyettesített értékek escape-elve |
|
|
376
|
+
| Helyőrzők, toolbar-slot, trigger | nincs (a `data-*` és az ismeretlen tag kiesik) | mind, mint a widgetben |
|
|
377
|
+
|
|
378
|
+
### HTML column és `contentType`
|
|
379
|
+
|
|
380
|
+
A `GridColumnMeta.contentType` mondja meg, hogyan kerül ki az érték:
|
|
381
|
+
|
|
382
|
+
- **hiányzó** vagy `html` — sanitizált innerHTML, ahogy a tábla mindig is írta. Semmi nem változik
|
|
383
|
+
annak a hostnak, amelyik nem állítja.
|
|
384
|
+
- `text` — **escape-elt** szöveg: a `<b>` betűkként látszik. Erre való minden adat, amiben
|
|
385
|
+
felhasználói szöveg van és nem jelölésként kell olvasni.
|
|
386
|
+
|
|
387
|
+
A rendezés a szerveren a **nyers** értéken fut, ezért html-oszlophoz adj
|
|
388
|
+
`sortOrderPropertyName`-et. Java:
|
|
389
|
+
|
|
390
|
+
```java
|
|
391
|
+
gridBuilder.contentType(GridColumnContentType.TEXT, "name", "comment")
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Column template
|
|
395
|
+
|
|
396
|
+
```java
|
|
397
|
+
gridBuilder.htmlColumn("name", h -> h.html(t -> t
|
|
398
|
+
.b(b -> b.text("{{name}}"))
|
|
399
|
+
.span(s -> s.action("OPEN").text("megnyitás"))
|
|
400
|
+
.toolbar("ROW_TOOLBAR")))
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
- **Ugyanaz a bean, mint a widgeté** (`HtmlProperties`): `html`, `root`, `fields`, `css`, és
|
|
404
|
+
ugyanaz a nyelvtan, pipe-készlet, escape, stíluslap- és hibakezelés. A `root` a **sor `data`-jához** relatív; a
|
|
405
|
+
`valueSets` a gridet mutató oldalé.
|
|
406
|
+
- A sablon az oszlop **minden érték-renderelését** kiváltja, a `typeClass` szerinti dátum-,
|
|
407
|
+
checkbox- és ikon-formázást is — a formázás a sablon pipe-jaiba kerül. A sor ikonjai
|
|
408
|
+
(`GridRow.icons`) és az oszlop cella-toolbarja (`columnActions`) mellette megmaradnak. Ha van
|
|
409
|
+
sablon, a `contentType` nem számít.
|
|
410
|
+
- A toolbar-slot és a trigger **a sor akcióit** látja (`row.actions`), pontosan úgy, mint a
|
|
411
|
+
sor-layouttal renderelt kártya: az akció modellje a sor, a scope a grid és a sor. A trigger a
|
|
412
|
+
kattintást és a dupla kattintást megtartja magának, a sor-klikk nem fut.
|
|
413
|
+
- **Sor-menü**: a csak-trigger akció (`UiActions.NO_TOOLBAR`) sosem kerül a sor menüjébe. Ha a
|
|
414
|
+
táblában van oszlop-sablon, akkor a **toolbar-címes** sorakció sem — az egy sablonbeli slot-é.
|
|
415
|
+
Sablon nélküli táblán a címes sorakció a menüben marad, ahogy eddig.
|
|
416
|
+
- A cella a sor minden cseréjekor újra felold; változatlan eredménynél a html és a hidratált
|
|
417
|
+
toolbar él tovább. Teljesítmény-kalap nincs: nagy táblán a sablon soronként egyszer oldódik fel,
|
|
418
|
+
hidratálás csak ott fut, ahol slot van.
|
|
419
|
+
- A `GridBuilder.build()` a sablon mezőit is ellenőrzi az oszlopokkal szemben, ugyanazzal a
|
|
420
|
+
WARN-nal, mint a sor-layoutét (`column '<név>' reads '<mező>' which no column covers`). Az
|
|
421
|
+
ismeretlen oszlopra adott sablon vagy `contentType` szintén WARN.
|
|
422
|
+
- A `GridBuilder`-nek nincs template-feloldója: a `template(ctx, KEY)` itt nem működik. Kulcsból
|
|
423
|
+
épített sablonhoz építsd a widgetet a page layout-builderével, és add át a `properties(...)`-szel.
|
|
424
|
+
|
|
425
|
+
## Példa: egy kártya
|
|
426
|
+
|
|
427
|
+
```html
|
|
428
|
+
<div class="contact-card contact-card-{{status}}" data-sb4-action="OPEN_CONTACT_ENTRY">
|
|
429
|
+
<div class="contact-card-main">
|
|
430
|
+
<div class="contact-card-icon contact-card-icon-{{channelType}}"></div>
|
|
431
|
+
<div class="contact-card-value">{{value}}</div>
|
|
432
|
+
<div class="contact-card-channel-type">{{channelType | label:'ContactChannelType'}}</div>
|
|
433
|
+
<div class="contact-card-modified">{{modifiedAt | datetime}}</div>
|
|
434
|
+
<smart-ui-action-toolbar data-sb4-toolbar="CONTACT_ROW_TOOLBAR"></smart-ui-action-toolbar>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="contact-card-associations">
|
|
437
|
+
<div class="contact-card-associations-title">Érintettek ({{partyCount}})</div>
|
|
438
|
+
{{#parties}}
|
|
439
|
+
<div class="contact-card-related-party">
|
|
440
|
+
<div class="contact-card-related-party-initials">{{monogram}}</div>
|
|
441
|
+
<div class="contact-card-related-party-company">{{displayName}}</div>
|
|
442
|
+
<div>{{#businessRoles}}<span>{{. | label:'BusinessRole'}}</span> {{/businessRoles}}</div>
|
|
443
|
+
</div>
|
|
444
|
+
{{/parties}}
|
|
445
|
+
{{^parties}}<div class="contact-card-associations-title">— nincs érintett —</div>{{/parties}}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`root = "card"`, ha a sor `data`-jában a kártya a `card` oszlop alatt van. Amit a kliens-dialektus
|
|
451
|
+
szándékosan nem tud, annak a helye:
|
|
452
|
+
|
|
453
|
+
- **ikon / értékfüggő megjelenés**: szerver-származtatott mező, vagy érték-alapú CSS-osztály
|
|
454
|
+
(`contact-card-icon-{{channelType}}`);
|
|
455
|
+
- **feltétel egy érték egyenlőségére**: sor-layout kulcs (`row.rowLayout`), vagy szerver-származtatott
|
|
456
|
+
logikai mező + `{{?…}}` őr;
|
|
457
|
+
- **darabszám** (`parties.size()`): valódi oszlop (`partyCount`);
|
|
458
|
+
- **fordítás**: `label` pipe valueSetből, vagy a szerver-fázis `${…}`-e.
|
package/MIGRATION-7.0.md
CHANGED
|
@@ -1252,7 +1252,7 @@ shorts link produced no player. It plays now.
|
|
|
1252
1252
|
|
|
1253
1253
|
| Removed | Replacement |
|
|
1254
1254
|
|---|---|
|
|
1255
|
-
| `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.
|
|
1255
|
+
| `SmartformwidgetComponent` | nothing — no host imported the class; `<smartform>` still renders it. 7.3 splits it into per-type components |
|
|
1256
1256
|
| `SmartWidgetSettings` (its only member was `static useUtc`) | nothing — **delete the assignment** |
|
|
1257
1257
|
|
|
1258
1258
|
3.6 moved the `useUtc` static into `SmartNgClientConfig.useUtcDates`; **3.8 then removed the
|
|
@@ -1844,6 +1844,50 @@ Two traps the audit cannot see:
|
|
|
1844
1844
|
The measured spread, over the 56 host repositories that were audited when this shipped: 1061 read
|
|
1845
1845
|
sites, 301 A, 671 B, 77 C.
|
|
1846
1846
|
|
|
1847
|
+
### New in 7.2: HTML widget, row layouts, hydrated toolbars (#30388)
|
|
1848
|
+
|
|
1849
|
+
Additive, with **one behaviour change** and one removed field. The reference is
|
|
1850
|
+
`HTML-WIDGET.md`, shipped in the package next to this file.
|
|
1851
|
+
|
|
1852
|
+
- **`SmartFormWidgetType.HTML`**: server-sent html whose client placeholders (`{{…}}`) the client
|
|
1853
|
+
resolves against the model, with toolbars hydrated into its toolbar slots
|
|
1854
|
+
(`<smart-ui-action-toolbar data-sb4-toolbar="…">`) and action triggers (`data-sb4-action`) on
|
|
1855
|
+
any element. The `DIV` widget is untouched. A host that never sends the widget sees no change.
|
|
1856
|
+
The widget can bring its own style sheet (`HtmlProperties.css`), which the client adds to the
|
|
1857
|
+
document head once per distinct text; a host with a nonce-based `style-src` provides
|
|
1858
|
+
Angular's `CSP_NONCE`. A style sheet can animate new html coming in and, through the
|
|
1859
|
+
`sb4-html-leaving` class the client puts on the widget or cell before it replaces the html,
|
|
1860
|
+
going out.
|
|
1861
|
+
Inside a `{{#section}}` a placeholder reads the surrounding scope only when it says so,
|
|
1862
|
+
`{{../title}}`: one step per entering section, never above the widget's root.
|
|
1863
|
+
- **Row layouts**: a cards grid can carry its card layouts once per grid
|
|
1864
|
+
(`GridViewDescriptor.rowLayouts`, picked per row by `GridRow.rowLayout`) instead of once per
|
|
1865
|
+
row. The per-row `layoutDescriptor.GRID_ROW_LAYOUT` still works and wins over them; a
|
|
1866
|
+
host-registered `'<GRID_ID>Card'` component still renders the rows that get no layout.
|
|
1867
|
+
- **Behaviour change — clicking a card rendered from a layout.** Since #29717 a click on such a
|
|
1868
|
+
card fired the row's default action. From 7.2 **a click selects the row** (when the grid's
|
|
1869
|
+
`selectionMode` is not `NONE` and the row is selectable) and **a double click fires the default
|
|
1870
|
+
row action**, exactly as a `ROW_SELECT` table does. A card that must open on a single click gets
|
|
1871
|
+
it back on the server side: put an action trigger on the root element of the card's html, or
|
|
1872
|
+
offer the action on a toolbar. Cards rendered by a host-registered component are not affected —
|
|
1873
|
+
the grid never handled their clicks.
|
|
1874
|
+
- **`SmartGrid.layoutComponent` is removed.** The card gets the layout class through the
|
|
1875
|
+
`SMART_ROW_LAYOUT_COMPONENT` token that `provideSmartNgClient()` provides, so a cards grid a
|
|
1876
|
+
host builds with `addGrid()` renders backend layouts too. Delete the field if a host grid model
|
|
1877
|
+
sets it; nothing else changes.
|
|
1878
|
+
- A toolbar no longer lets a `dblclick` on its buttons through to the element around it (it
|
|
1879
|
+
already kept the `click`).
|
|
1880
|
+
- The toolbar's re-entry guard is per action now: while one action runs, another button of the
|
|
1881
|
+
same toolbar can be fired. It is also released when the action fails.
|
|
1882
|
+
- **Trusted Types**: the HTML widget writes backend-authored html as it is, as the `DIV` widget
|
|
1883
|
+
always has. A host that enforces Trusted Types must allow that write.
|
|
1884
|
+
- **Table cells**: `GridColumnMeta.contentType` now reaches the table. A column that names none
|
|
1885
|
+
(or `html`) renders exactly as before, sanitized; `text` escapes the value. A column with
|
|
1886
|
+
`htmlProperties` renders a column template — the HTML widget's dialect, resolved per row, with
|
|
1887
|
+
toolbar slots and triggers offering the row's actions. A row action addressed to
|
|
1888
|
+
`UiActions.NO_TOOLBAR` no longer shows in the row menu; in a table with column templates no row
|
|
1889
|
+
action addressed to a toolbar does.
|
|
1890
|
+
|
|
1847
1891
|
### Bugfixes shipped with 7.0
|
|
1848
1892
|
|
|
1849
1893
|
- `SmartformwidgetComponent.ngAfterViewInit` no longer crashes with
|
package/WIDGETS.md
CHANGED
|
@@ -150,6 +150,13 @@ export class MyRowComponent {
|
|
|
150
150
|
Leaving it `undefined` means "not my business", and the toolbars below fall back to the
|
|
151
151
|
client — which is what makes providing it unconditionally safe.
|
|
152
152
|
|
|
153
|
+
A toolbar does not have to be in a template at all. The HTML widget **hydrates** one into every
|
|
154
|
+
toolbar slot of the html the backend sent (`<smart-ui-action-toolbar data-sb4-toolbar="…">`), and
|
|
155
|
+
such a toolbar resolves by the very same pull: a `SmartActionHost` above it if one has a list, the
|
|
156
|
+
screen component otherwise. That is why a row layout's html gets the row's actions on a grid card
|
|
157
|
+
with no code of its own, and a table cell rendered from a column template gets them the same way.
|
|
158
|
+
An action trigger (`data-sb4-action`) looks its action up in the same list. See `HTML-WIDGET.md`.
|
|
159
|
+
|
|
153
160
|
**A `UiActionModel` is frozen.** Every field is `readonly`, and `[uiActionModels]` takes
|
|
154
161
|
`readonly UiActionModel[]`. Build a new entry; never edit one. Writing into an entry a
|
|
155
162
|
toolbar is already rendering never reached the screen under zone.js either — it only appeared
|
|
@@ -199,7 +206,9 @@ A `SmartComponentLayoutDefinition` node of type `WIDGET` places one of the libra
|
|
|
199
206
|
`widget.identifier`: `grid`, `tree`, `filter` (`smart-filter-widget`, a filter builder whose
|
|
200
207
|
identifier is its `filterId`), `toolbar`, `map`, `diagram` and `embedded_slot`. Each of them
|
|
201
208
|
follows the contract above, so a widget placed by the layout and one written into a page
|
|
202
|
-
template by hand register with the same client the same way.
|
|
209
|
+
template by hand register with the same client the same way. Inside a `FORM` node the form
|
|
210
|
+
widget type `HTML` places server-sent html with client placeholders, toolbar slots and action
|
|
211
|
+
triggers — `HTML-WIDGET.md` is its reference. The table in
|
|
203
212
|
`src/lib/smart-component-layout/README.md` lists what each identifier means.
|
|
204
213
|
|
|
205
214
|
## The executable version
|