@dooboostore/simple-web-component 1.0.50 → 1.0.52
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/README.md +735 -354
- package/dist/cjs/SwcAppEngine.js.map +2 -2
- package/dist/cjs/decorators/addEventListener.js +64 -146
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/applyAttribute.js +36 -48
- package/dist/cjs/decorators/applyAttribute.js.map +2 -2
- package/dist/cjs/decorators/applyClass.js +9 -12
- package/dist/cjs/decorators/applyClass.js.map +2 -2
- package/dist/cjs/decorators/applyNode.js +14 -22
- package/dist/cjs/decorators/applyNode.js.map +2 -2
- package/dist/cjs/decorators/applyProperty.js +12 -42
- package/dist/cjs/decorators/applyProperty.js.map +2 -2
- package/dist/cjs/decorators/applySlot.js +3 -3
- package/dist/cjs/decorators/applySlot.js.map +2 -2
- package/dist/cjs/decorators/applyStyle.js +9 -12
- package/dist/cjs/decorators/applyStyle.js.map +2 -2
- package/dist/cjs/decorators/around.js +124 -0
- package/dist/cjs/decorators/around.js.map +7 -0
- package/dist/cjs/decorators/changedAttribute.js +40 -12
- package/dist/cjs/decorators/changedAttribute.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +224 -195
- package/dist/cjs/decorators/elementDefine.js.map +3 -3
- package/dist/cjs/decorators/emitCustomEvent.js +14 -12
- package/dist/cjs/decorators/emitCustomEvent.js.map +2 -2
- package/dist/cjs/decorators/eventMedia.js +36 -8
- package/dist/cjs/decorators/eventMedia.js.map +2 -2
- package/dist/cjs/decorators/fetch.js +278 -0
- package/dist/cjs/decorators/fetch.js.map +7 -0
- package/dist/cjs/decorators/index.js +5 -2
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/intersectionObserver.js +33 -13
- package/dist/cjs/decorators/intersectionObserver.js.map +2 -2
- package/dist/cjs/decorators/lifecycles.js +11 -11
- package/dist/cjs/decorators/lifecycles.js.map +2 -2
- package/dist/cjs/decorators/mutationObserver.js +55 -39
- package/dist/cjs/decorators/mutationObserver.js.map +3 -3
- package/dist/cjs/decorators/parameter.js +92 -9
- package/dist/cjs/decorators/parameter.js.map +2 -2
- package/dist/cjs/decorators/publishSwcAppMessage.js +2 -2
- package/dist/cjs/decorators/publishSwcAppMessage.js.map +2 -2
- package/dist/cjs/decorators/query.js +11 -5
- package/dist/cjs/decorators/query.js.map +2 -2
- package/dist/cjs/decorators/requestAnimationFrame.js +7 -16
- package/dist/cjs/decorators/requestAnimationFrame.js.map +2 -2
- package/dist/cjs/decorators/resizeObserver.js +41 -17
- package/dist/cjs/decorators/resizeObserver.js.map +3 -3
- package/dist/cjs/decorators/setInterval.js +36 -25
- package/dist/cjs/decorators/setInterval.js.map +2 -2
- package/dist/cjs/decorators/setTimeout.js +36 -25
- package/dist/cjs/decorators/setTimeout.js.map +2 -2
- package/dist/cjs/decorators/state.js +13 -10
- package/dist/cjs/decorators/state.js.map +2 -2
- package/dist/cjs/decorators/{subscribeSwcAppMessageWhileConnected.js → subscribeSwcAppMessage.js} +20 -21
- package/dist/cjs/decorators/subscribeSwcAppMessage.js.map +7 -0
- package/dist/cjs/decorators/{subscribeSwcAppRouteChangeWhileConnected.js → subscribeSwcAppRouteChange.js} +23 -19
- package/dist/cjs/decorators/subscribeSwcAppRouteChange.js.map +7 -0
- package/dist/cjs/decorators/swcAppRoute.js +149 -0
- package/dist/cjs/decorators/swcAppRoute.js.map +7 -0
- package/dist/cjs/elements/SwcApp.js +70 -0
- package/dist/cjs/elements/SwcApp.js.map +2 -2
- package/dist/cjs/elements/SwcAppMixin.js +262 -83
- package/dist/cjs/elements/SwcAppMixin.js.map +3 -3
- package/dist/cjs/types/index.js +5 -1
- package/dist/cjs/types/index.js.map +2 -2
- package/dist/cjs/utils/Utils.js +36 -15
- package/dist/cjs/utils/Utils.js.map +2 -2
- package/dist/esm/SwcAppEngine.js.map +2 -2
- package/dist/esm/decorators/addEventListener.js +64 -146
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/applyAttribute.js +36 -48
- package/dist/esm/decorators/applyAttribute.js.map +2 -2
- package/dist/esm/decorators/applyClass.js +9 -12
- package/dist/esm/decorators/applyClass.js.map +2 -2
- package/dist/esm/decorators/applyNode.js +15 -23
- package/dist/esm/decorators/applyNode.js.map +2 -2
- package/dist/esm/decorators/applyProperty.js +12 -42
- package/dist/esm/decorators/applyProperty.js.map +2 -2
- package/dist/esm/decorators/applySlot.js +3 -3
- package/dist/esm/decorators/applySlot.js.map +2 -2
- package/dist/esm/decorators/applyStyle.js +9 -12
- package/dist/esm/decorators/applyStyle.js.map +2 -2
- package/dist/esm/decorators/around.js +105 -0
- package/dist/esm/decorators/around.js.map +7 -0
- package/dist/esm/decorators/changedAttribute.js +40 -12
- package/dist/esm/decorators/changedAttribute.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +222 -193
- package/dist/esm/decorators/elementDefine.js.map +3 -3
- package/dist/esm/decorators/emitCustomEvent.js +15 -13
- package/dist/esm/decorators/emitCustomEvent.js.map +2 -2
- package/dist/esm/decorators/eventMedia.js +36 -8
- package/dist/esm/decorators/eventMedia.js.map +2 -2
- package/dist/esm/decorators/fetch.js +259 -0
- package/dist/esm/decorators/fetch.js.map +7 -0
- package/dist/esm/decorators/index.js +5 -2
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/intersectionObserver.js +33 -13
- package/dist/esm/decorators/intersectionObserver.js.map +2 -2
- package/dist/esm/decorators/lifecycles.js +11 -11
- package/dist/esm/decorators/lifecycles.js.map +2 -2
- package/dist/esm/decorators/mutationObserver.js +55 -39
- package/dist/esm/decorators/mutationObserver.js.map +3 -3
- package/dist/esm/decorators/parameter.js +92 -9
- package/dist/esm/decorators/parameter.js.map +2 -2
- package/dist/esm/decorators/publishSwcAppMessage.js +2 -2
- package/dist/esm/decorators/publishSwcAppMessage.js.map +2 -2
- package/dist/esm/decorators/query.js +11 -5
- package/dist/esm/decorators/query.js.map +2 -2
- package/dist/esm/decorators/requestAnimationFrame.js +8 -17
- package/dist/esm/decorators/requestAnimationFrame.js.map +2 -2
- package/dist/esm/decorators/resizeObserver.js +41 -17
- package/dist/esm/decorators/resizeObserver.js.map +2 -2
- package/dist/esm/decorators/setInterval.js +37 -26
- package/dist/esm/decorators/setInterval.js.map +2 -2
- package/dist/esm/decorators/setTimeout.js +37 -26
- package/dist/esm/decorators/setTimeout.js.map +2 -2
- package/dist/esm/decorators/state.js +13 -10
- package/dist/esm/decorators/state.js.map +2 -2
- package/dist/esm/decorators/subscribeSwcAppMessage.js +53 -0
- package/dist/esm/decorators/subscribeSwcAppMessage.js.map +7 -0
- package/dist/esm/decorators/{subscribeSwcAppRouteChangeWhileConnected.js → subscribeSwcAppRouteChange.js} +20 -16
- package/dist/esm/decorators/subscribeSwcAppRouteChange.js.map +7 -0
- package/dist/esm/decorators/swcAppRoute.js +130 -0
- package/dist/esm/decorators/swcAppRoute.js.map +7 -0
- package/dist/esm/elements/SwcApp.js +70 -0
- package/dist/esm/elements/SwcApp.js.map +2 -2
- package/dist/esm/elements/SwcAppMixin.js +264 -85
- package/dist/esm/elements/SwcAppMixin.js.map +3 -3
- package/dist/esm/types/index.js +5 -1
- package/dist/esm/types/index.js.map +2 -2
- package/dist/esm/utils/Utils.js +36 -15
- package/dist/esm/utils/Utils.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +1893 -1000
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/SwcAppEngine.d.ts +1 -0
- package/dist/types/SwcAppEngine.d.ts.map +1 -1
- package/dist/types/decorators/addEventListener.d.ts +923 -960
- package/dist/types/decorators/addEventListener.d.ts.map +1 -1
- package/dist/types/decorators/applyAttribute.d.ts +26 -38
- package/dist/types/decorators/applyAttribute.d.ts.map +1 -1
- package/dist/types/decorators/applyClass.d.ts +1 -3
- package/dist/types/decorators/applyClass.d.ts.map +1 -1
- package/dist/types/decorators/applyNode.d.ts +15 -17
- package/dist/types/decorators/applyNode.d.ts.map +1 -1
- package/dist/types/decorators/applyProperty.d.ts +25 -45
- package/dist/types/decorators/applyProperty.d.ts.map +1 -1
- package/dist/types/decorators/applyStyle.d.ts +1 -3
- package/dist/types/decorators/applyStyle.d.ts.map +1 -1
- package/dist/types/decorators/around.d.ts +87 -0
- package/dist/types/decorators/around.d.ts.map +1 -0
- package/dist/types/decorators/changedAttribute.d.ts +21 -1
- package/dist/types/decorators/changedAttribute.d.ts.map +1 -1
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/decorators/emitCustomEvent.d.ts +0 -1
- package/dist/types/decorators/emitCustomEvent.d.ts.map +1 -1
- package/dist/types/decorators/eventMedia.d.ts +24 -1
- package/dist/types/decorators/eventMedia.d.ts.map +1 -1
- package/dist/types/decorators/fetch.d.ts +139 -0
- package/dist/types/decorators/fetch.d.ts.map +1 -0
- package/dist/types/decorators/index.d.ts +5 -2
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/intersectionObserver.d.ts +24 -11
- package/dist/types/decorators/intersectionObserver.d.ts.map +1 -1
- package/dist/types/decorators/lifecycles.d.ts +8 -7
- package/dist/types/decorators/lifecycles.d.ts.map +1 -1
- package/dist/types/decorators/mutationObserver.d.ts +26 -13
- package/dist/types/decorators/mutationObserver.d.ts.map +1 -1
- package/dist/types/decorators/parameter.d.ts +84 -6
- package/dist/types/decorators/parameter.d.ts.map +1 -1
- package/dist/types/decorators/publishSwcAppMessage.d.ts +0 -1
- package/dist/types/decorators/publishSwcAppMessage.d.ts.map +1 -1
- package/dist/types/decorators/query.d.ts +7 -7
- package/dist/types/decorators/query.d.ts.map +1 -1
- package/dist/types/decorators/requestAnimationFrame.d.ts +1 -1
- package/dist/types/decorators/requestAnimationFrame.d.ts.map +1 -1
- package/dist/types/decorators/resizeObserver.d.ts +26 -13
- package/dist/types/decorators/resizeObserver.d.ts.map +1 -1
- package/dist/types/decorators/setInterval.d.ts +10 -0
- package/dist/types/decorators/setInterval.d.ts.map +1 -1
- package/dist/types/decorators/setTimeout.d.ts +10 -0
- package/dist/types/decorators/setTimeout.d.ts.map +1 -1
- package/dist/types/decorators/state.d.ts.map +1 -1
- package/dist/types/decorators/subscribeSwcAppMessage.d.ts +44 -0
- package/dist/types/decorators/subscribeSwcAppMessage.d.ts.map +1 -0
- package/dist/types/decorators/subscribeSwcAppRouteChange.d.ts +60 -0
- package/dist/types/decorators/subscribeSwcAppRouteChange.d.ts.map +1 -0
- package/dist/types/decorators/swcAppRoute.d.ts +94 -0
- package/dist/types/decorators/swcAppRoute.d.ts.map +1 -0
- package/dist/types/elements/SwcApp.d.ts.map +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts +423 -388
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +43 -4
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/utils/Utils.d.ts +15 -5
- package/dist/types/utils/Utils.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +1893 -1000
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/package.json +8 -6
- package/src/SwcAppEngine.ts +1 -0
- package/src/decorators/addEventListener.ts +102 -96
- package/src/decorators/applyAttribute.ts +76 -138
- package/src/decorators/applyClass.ts +8 -14
- package/src/decorators/applyNode.ts +33 -41
- package/src/decorators/applyProperty.ts +51 -99
- package/src/decorators/applySlot.ts +3 -3
- package/src/decorators/applyStyle.ts +8 -14
- package/src/decorators/around.ts +196 -0
- package/src/decorators/changedAttribute.ts +39 -15
- package/src/decorators/elementDefine.ts +270 -219
- package/src/decorators/emitCustomEvent.ts +17 -13
- package/src/decorators/eventMedia.ts +50 -9
- package/src/decorators/fetch.ts +454 -0
- package/src/decorators/index.ts +5 -2
- package/src/decorators/intersectionObserver.ts +38 -24
- package/src/decorators/lifecycles.ts +24 -22
- package/src/decorators/mutationObserver.ts +71 -52
- package/src/decorators/parameter.ts +145 -14
- package/src/decorators/publishSwcAppMessage.ts +3 -4
- package/src/decorators/query.ts +22 -14
- package/src/decorators/requestAnimationFrame.ts +10 -19
- package/src/decorators/resizeObserver.ts +50 -29
- package/src/decorators/setInterval.ts +37 -26
- package/src/decorators/setTimeout.ts +37 -26
- package/src/decorators/state.ts +19 -13
- package/src/decorators/subscribeSwcAppMessage.ts +106 -0
- package/src/decorators/subscribeSwcAppRouteChange.ts +131 -0
- package/src/decorators/swcAppRoute.ts +231 -0
- package/src/elements/SwcApp.ts +40 -10
- package/src/elements/SwcAppMixin.ts +340 -101
- package/src/types/index.ts +38 -5
- package/src/utils/Utils.ts +50 -18
- package/dist/cjs/decorators/subscribeSwcAppMessageWhileConnected.js.map +0 -7
- package/dist/cjs/decorators/subscribeSwcAppRouteChangeWhileConnected.js.map +0 -7
- package/dist/esm/decorators/subscribeSwcAppMessageWhileConnected.js +0 -54
- package/dist/esm/decorators/subscribeSwcAppMessageWhileConnected.js.map +0 -7
- package/dist/esm/decorators/subscribeSwcAppRouteChangeWhileConnected.js.map +0 -7
- package/dist/types/decorators/subscribeSwcAppMessageWhileConnected.d.ts +0 -23
- package/dist/types/decorators/subscribeSwcAppMessageWhileConnected.d.ts.map +0 -1
- package/dist/types/decorators/subscribeSwcAppRouteChangeWhileConnected.d.ts +0 -31
- package/dist/types/decorators/subscribeSwcAppRouteChangeWhileConnected.d.ts.map +0 -1
- package/src/decorators/subscribeSwcAppMessageWhileConnected.ts +0 -91
- package/src/decorators/subscribeSwcAppRouteChangeWhileConnected.ts +0 -107
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
## 🎯 Core Features
|
|
12
12
|
|
|
13
|
+
> **No arguments → no parentheses.** Every decorator that can run without arguments is used bare (`@query`, `@innerHtml`, `@eventClick`, `@swcAppRouteGo`, ...). The empty-call form `@x()` is a type error; pass arguments only when you have them (`@innerHtml('.wrap')`, `@eventClick({ once: true })`).
|
|
14
|
+
|
|
13
15
|
### 1. **@elementDefine** - Component Registration
|
|
14
16
|
Register Web Components with automatic lifecycle management and DI support.
|
|
15
17
|
|
|
@@ -22,6 +24,22 @@ class MyComponent extends HTMLElement {
|
|
|
22
24
|
}
|
|
23
25
|
```
|
|
24
26
|
|
|
27
|
+
**`@elementDefine(name, options?)` options:** `window`, `extends` (customized built-in, e.g. `'body'`), `useShadow` (`true | 'open' | 'closed'`), `observedAttributes`, `customElementRegistry`.
|
|
28
|
+
`window` may be another window than the one running the code (an iframe's, or a server-side DOM window) — the class then extends that window's `HTMLElement` (`class X extends w.HTMLElement`).
|
|
29
|
+
|
|
30
|
+
**Inheritance:** decorators on a base class apply to its subclasses — lifecycle hooks, events, observers, timers, message/route subscriptions, `@changedAttribute`, `@query`, `@state` …
|
|
31
|
+
- hooks on both run parent first, then child
|
|
32
|
+
- a subclass's own decorators never leak into the base class or into sibling subclasses
|
|
33
|
+
- a subclass of a class that is itself `@elementDefine`d runs each lifecycle once (not once per level)
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
class BasePage extends HTMLElement {
|
|
37
|
+
@onInitialize init(@inject(AuthService.SYMBOL) auth: AuthService) { this.auth = auth; }
|
|
38
|
+
@onDisconnected cleanup() { /* … */ }
|
|
39
|
+
}
|
|
40
|
+
@elementDefine('my-page') class MyPage extends BasePage {} // init / cleanup run for <my-page>
|
|
41
|
+
```
|
|
42
|
+
|
|
25
43
|
### 2. **Dependency Injection (@onInitialize)**
|
|
26
44
|
Inject services into Web Components using the `@onInitialize` decorator.
|
|
27
45
|
|
|
@@ -64,8 +82,8 @@ Replace child nodes with new content.
|
|
|
64
82
|
|
|
65
83
|
```typescript
|
|
66
84
|
@replaceChildrenLight
|
|
67
|
-
|
|
68
|
-
return
|
|
85
|
+
renderNode() {
|
|
86
|
+
return this.buildNode(); // Node, Node[] or HTML string
|
|
69
87
|
}
|
|
70
88
|
```
|
|
71
89
|
### 3.5 **Slot Management with @applySlot**
|
|
@@ -76,7 +94,7 @@ Manage dynamic content insertion into named slots using slot decorators. Slots a
|
|
|
76
94
|
@elementDefine('content-manager')
|
|
77
95
|
class ContentManager extends HTMLElement {
|
|
78
96
|
// Method: Replace children in slot with HTML
|
|
79
|
-
@
|
|
97
|
+
@event('.update-btn', 'click')
|
|
80
98
|
@replaceChildrenHtmlSlot('main-content')
|
|
81
99
|
updateContent() {
|
|
82
100
|
return '<div>Updated content here</div>';
|
|
@@ -95,7 +113,7 @@ class ContentManager extends HTMLElement {
|
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
// Method: Clear slot content
|
|
98
|
-
@
|
|
116
|
+
@event('.clear-btn', 'click')
|
|
99
117
|
@clearSlot('main-content')
|
|
100
118
|
clearContent() {
|
|
101
119
|
return true;
|
|
@@ -166,7 +184,7 @@ class CounterApp extends HTMLElement {
|
|
|
166
184
|
@state('isActive')
|
|
167
185
|
isActive: boolean = false;
|
|
168
186
|
|
|
169
|
-
@
|
|
187
|
+
@event('button', 'click')
|
|
170
188
|
increment() {
|
|
171
189
|
this.count++; // Triggers automatic DOM update
|
|
172
190
|
}
|
|
@@ -179,7 +197,7 @@ class CounterApp extends HTMLElement {
|
|
|
179
197
|
<!--[html @message@ ]-->
|
|
180
198
|
|
|
181
199
|
<!-- Text directive: render as text -->
|
|
182
|
-
<!--[text Count: @count@ ]-->
|
|
200
|
+
<!--[text 'Count: ' + @count@ ]-->
|
|
183
201
|
|
|
184
202
|
<!-- Attribute binding with a:: prefix -->
|
|
185
203
|
<div a::title="'Count is'+@count@"></div>
|
|
@@ -227,7 +245,7 @@ State values accessed in templates via `@stateName@` syntax are read-only. You c
|
|
|
227
245
|
|
|
228
246
|
```typescript
|
|
229
247
|
// ✅ CORRECT - Update state from method
|
|
230
|
-
@
|
|
248
|
+
@event('button', 'click')
|
|
231
249
|
increment() {
|
|
232
250
|
this.count++; // Direct property assignment in code
|
|
233
251
|
}
|
|
@@ -249,7 +267,7 @@ class UserProfile extends HTMLElement {
|
|
|
249
267
|
@state('isEditing')
|
|
250
268
|
isEditing = false;
|
|
251
269
|
|
|
252
|
-
@
|
|
270
|
+
@event('.edit-btn', 'click')
|
|
253
271
|
toggleEdit() {
|
|
254
272
|
this.isEditing = !this.isEditing;
|
|
255
273
|
}
|
|
@@ -276,19 +294,19 @@ class UserProfile extends HTMLElement {
|
|
|
276
294
|
|
|
277
295
|
### 4. **Event Handling**
|
|
278
296
|
|
|
279
|
-
#### @
|
|
297
|
+
#### @event
|
|
280
298
|
Attach event listeners to elements with optional filter support.
|
|
281
299
|
|
|
282
300
|
```typescript
|
|
283
|
-
@
|
|
301
|
+
@event('#submit-btn', 'click')
|
|
284
302
|
onSubmit(event: Event) {
|
|
285
303
|
console.log('Submitted');
|
|
286
304
|
}
|
|
287
305
|
|
|
288
306
|
// Filter events - only process matching events
|
|
289
|
-
@
|
|
290
|
-
filter: (event,
|
|
291
|
-
return event.target?.id === 'critical-button';
|
|
307
|
+
@event('button', 'click', {
|
|
308
|
+
filter: (event, meta) => {
|
|
309
|
+
return (event.target as HTMLElement)?.id === 'critical-button';
|
|
292
310
|
}
|
|
293
311
|
})
|
|
294
312
|
onCriticalClick(event: Event) {
|
|
@@ -296,27 +314,31 @@ onCriticalClick(event: Event) {
|
|
|
296
314
|
}
|
|
297
315
|
```
|
|
298
316
|
|
|
299
|
-
**@
|
|
300
|
-
- `
|
|
301
|
-
- `event(
|
|
302
|
-
- `
|
|
303
|
-
- `
|
|
304
|
-
- `
|
|
305
|
-
- `
|
|
306
|
-
- `
|
|
307
|
-
- `
|
|
308
|
-
- `
|
|
309
|
-
- `
|
|
310
|
-
- `
|
|
311
|
-
- `
|
|
312
|
-
- `
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
- `
|
|
316
|
-
- `
|
|
317
|
-
- `
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
**@event Decorator Variants:**
|
|
318
|
+
- `event(selector, type, options)` - Full form
|
|
319
|
+
- `event(type, options)` (no selector) - Listen on $this element. With the default `root: 'auto'` it binds to the shadowRoot when the element has one (clicks inside the shadow bubble to it, but events dispatched on the element itself — `el.click()`, a parent's `el.dispatchEvent(...)` — never go down into it); use `{ root: 'light' }` to bind the element itself, `{ root: 'all' }` for both
|
|
320
|
+
- `eventAppHost(type, options)` - Listen on $appHost
|
|
321
|
+
- `eventWindow(type, options)` - Listen on window
|
|
322
|
+
- `eventDocument(type, options)` - Listen on document
|
|
323
|
+
- `eventDelegate(selector, type, options)` - Event delegation
|
|
324
|
+
- `eventDelegateLight(selector, type, options)` - Delegate in light DOM
|
|
325
|
+
- `eventDelegateShadow(selector, type, options)` - Delegate in shadow DOM
|
|
326
|
+
- `eventDelegateAll(selector, type, options)` - Delegate in all DOM
|
|
327
|
+
- `eventLight(selector, type, options)` - Bind (non-delegated) in light DOM
|
|
328
|
+
- `eventShadow(selector, type, options)` - Bind (non-delegated) in shadow DOM
|
|
329
|
+
- `eventAll(selector, type, options)` - Bind (non-delegated) in both light & shadow DOM
|
|
330
|
+
- `eventMutation(selector, type, options)` - Delegate via `MutationObserver` instead of event bubbling (useful for non-bubbling events like `focus`/`blur`); `*Light`/`*Shadow`/`*All` variants also available
|
|
331
|
+
|
|
332
|
+
**@event Options:**
|
|
333
|
+
- `capture` / `once` / `passive` - standard listener options
|
|
334
|
+
- `preventDefault` / `stopPropagation` / `stopImmediatePropagation` - applied before the handler runs
|
|
335
|
+
- `filter: (event, { currentThis, helper }) => boolean | Promise<boolean>` - skip the handler when it returns false
|
|
336
|
+
- `before: (event, meta, args) => any` - runs after `filter`; its return value is injectable with `@eventBeforeReturn`
|
|
337
|
+
- `finally: (event, meta, { args, result, error }) => any` - always runs after the handler
|
|
338
|
+
- `debounceTime` / `throttleTime` (ms) / `distinctUntilChanged` - rate-limit the event stream
|
|
339
|
+
- `removeListener: (target, options) => void` - cleanup callback on disconnect
|
|
340
|
+
|
|
341
|
+
**Event-Type-Specific Aliases (eventClick, eventInputDelegateShadow, ...):**
|
|
320
342
|
|
|
321
343
|
Beyond the generic `event(selector, type, options)` form, every scope/delegate variant above is also pre-bound to ~59 common DOM event types, so you don't have to repeat the type string:
|
|
322
344
|
|
|
@@ -324,16 +346,22 @@ Beyond the generic `event(selector, type, options)` form, every scope/delegate v
|
|
|
324
346
|
@eventClick('.logo')
|
|
325
347
|
onLogoClick() { ... }
|
|
326
348
|
|
|
349
|
+
// No selector → the element itself (same as @event('click')); bare form, no parens
|
|
350
|
+
@eventClick
|
|
351
|
+
onClick(event: MouseEvent) { ... }
|
|
352
|
+
|
|
353
|
+
// Options without a selector → still the element itself
|
|
354
|
+
@eventKeydown({ filter: (e, meta) => !meta.currentThis.isLocked })
|
|
355
|
+
onKeydown(event: KeyboardEvent) { ... }
|
|
356
|
+
|
|
327
357
|
// Same as: @eventDelegateLight('.item', 'click')
|
|
328
358
|
@eventClickDelegateLight('.item')
|
|
329
359
|
onItemClick(event: Event) { ... }
|
|
330
|
-
|
|
331
|
-
// Same as: @addEventListenerThis('keydown')
|
|
332
|
-
@eventKeydownThis()
|
|
333
|
-
onKeydown(event: KeyboardEvent) { ... }
|
|
334
360
|
```
|
|
335
361
|
|
|
336
|
-
Naming pattern: `event` + `PascalCase(type)` + scope suffix (`''`(base) / `DelegateLight` / `DelegateShadow` / `DelegateAll` / `Delegate` / `MutationLight` / `MutationShadow` / `MutationAll` / `Mutation` / `Light` / `Shadow` / `All` / `
|
|
362
|
+
Naming pattern: `event` + `PascalCase(type)` + scope suffix (`''`(base) / `DelegateLight` / `DelegateShadow` / `DelegateAll` / `Delegate` / `MutationLight` / `MutationShadow` / `MutationAll` / `Mutation` / `Light` / `Shadow` / `All` / `AppHost` / `Window` / `Document`) — e.g. `eventClick`, `eventClickDelegateShadow`, `eventDblclick`, `eventPointerdownDelegateAll`, `eventKeydownWindow`.
|
|
363
|
+
|
|
364
|
+
The base alias (`eventClick`) takes an optional selector: bare `@eventClick` or `@eventClick({...})` listens on the element itself, `@eventClick('.btn', {...})` on the selector (there is no separate `…This` variant). The `AppHost`/`Window`/`Document` variants take no selector — only options — so they are used bare (`@eventClickWindow`) or with options. Never use empty parens. The `Delegate*`/`Light`/`Shadow`/`All` variants require a `selector`.
|
|
337
365
|
|
|
338
366
|
Covered event types include mouse (`click`, `dblclick`, `mousedown`, `mouseup`, `mousemove`, `mouseover`, `mouseout`, `mouseenter`, `mouseleave`, `contextmenu`, `wheel`), keyboard (`keydown`, `keyup`, `keypress`), form (`input`, `change`, `submit`, `reset`, `invalid`, `select`), focus (`focus`, `blur`, `focusin`, `focusout`), drag & drop (`dragstart`, `drag`, `dragend`, `dragenter`, `dragleave`, `dragover`, `drop`), touch (`touchstart`, `touchmove`, `touchend`, `touchcancel`), pointer (`pointerdown`, `pointerup`, `pointermove`, `pointerover`, `pointerout`, `pointerenter`, `pointerleave`, `pointercancel`), clipboard (`copy`, `cut`, `paste`), animation/transition (`animationstart/end/iteration/cancel`, `transitionstart/end/cancel/run`), and misc (`scroll`, `resize`, `load`, `error`, `toggle`).
|
|
339
367
|
|
|
@@ -341,27 +369,27 @@ These aliases are generated in `addEventListener.ts` from a small internal `make
|
|
|
341
369
|
|
|
342
370
|
#### Order-Independent Parameter Decorators (@eventObject, @matchedElement, @hostSet, @helperHostSet, @helperSet)
|
|
343
371
|
|
|
344
|
-
`@
|
|
372
|
+
`@event`-bound handlers normally receive fixed positional arguments (`(event, legacyHelper1, legacyHelper2)`). If you'd rather not memorize that order, decorate individual parameters instead — order and position no longer matter, and any parameter left undecorated in a method that uses none of these decorators still gets the legacy positional arguments (fully backward-compatible):
|
|
345
373
|
|
|
346
374
|
```typescript
|
|
347
|
-
import {
|
|
375
|
+
import { event, eventObject, matchedElement, hostSet, helperHostSet, helperSet } from '@dooboostore/simple-web-component';
|
|
348
376
|
|
|
349
377
|
@elementDefine('product-list')
|
|
350
378
|
class ProductList extends HTMLElement {
|
|
351
379
|
// Order can be anything you like
|
|
352
|
-
@
|
|
380
|
+
@eventDelegateLight('.item', 'click')
|
|
353
381
|
onItemClick(@matchedElement $item: Element, @eventObject event: Event) {
|
|
354
382
|
console.log('clicked item:', $item, event);
|
|
355
383
|
}
|
|
356
384
|
|
|
357
385
|
// Only need the host tree info? Just ask for @hostSet.
|
|
358
|
-
@
|
|
386
|
+
@eventClick
|
|
359
387
|
onHostClick(@hostSet hs: HostSet) {
|
|
360
388
|
console.log(hs.$appHost, hs.$hosts);
|
|
361
389
|
}
|
|
362
390
|
|
|
363
391
|
// Need DOM helpers ($q, $qa, ...) too? Use @helperHostSet (superset of @hostSet) or @helperSet (helpers only).
|
|
364
|
-
@
|
|
392
|
+
@eventClick
|
|
365
393
|
onHostClickFull(@helperHostSet full: HelperHostSet, @helperSet helpers: HelperSet) { ... }
|
|
366
394
|
}
|
|
367
395
|
```
|
|
@@ -372,19 +400,24 @@ class ProductList extends HTMLElement {
|
|
|
372
400
|
- `@helperHostSet` - `@hostSet` + DOM/window helpers (`$d`, `$w`, `$q`, `$qa`, `$qi`) + `$this`
|
|
373
401
|
- `@helperSet` - DOM/window helpers only (`$d`, `$w`, `$q`, `$qa`, `$qi`), no host-tree info
|
|
374
402
|
|
|
403
|
+
`$w`/`$d` (and the `helper` passed to every decorator's `filter`/`before`/`finally`) come from the element's window, resolved the same way everywhere: its own `@elementDefine({ window })` → the nearest ancestor SWC host's → the global `window`. Route subscribers (`@subscribeSwcAppRouteChange`) and app messages use the SwcApp's runtime `window` instead (the per-request window under SSR).
|
|
404
|
+
|
|
375
405
|
The same mechanism (and the same 5 decorators, plus two more) also applies to:
|
|
376
406
|
- **`@onInitialize`/`@onConnectedBefore`/`@onConnectedAfter`/... lifecycle methods** — can freely mix `@hostSet`/`@helperHostSet`/`@helperSet` with `@inject(...)` on the same method.
|
|
377
|
-
- **`@
|
|
378
|
-
- **`@
|
|
407
|
+
- **`@subscribeSwcAppRouteChange`** — add `@swcAppRouterEvent` to receive the `RouterEventType` regardless of position.
|
|
408
|
+
- **`@subscribeSwcAppMessage`** — add `@appMessage` to receive the `SwcAppMessage` regardless of position.
|
|
409
|
+
- **`before` hook results** — `@eventBeforeReturn`, `@routeChangeBeforeReturn`, `@appMessageBeforeReturn`, `@changedAttributeBeforeReturn`, `@mutationObserverBeforeReturn`, `@resizeObserverBeforeReturn`, `@intersectionObserverBeforeReturn`, `@eventMediaBeforeReturn`, `@setIntervalBeforeReturn`, `@setTimeoutBeforeReturn` inject the value returned by that decorator's `before` option.
|
|
410
|
+
|
|
411
|
+
Note: once any parameter of a method is decorated, only decorated parameters receive values (the legacy positional arguments are no longer passed).
|
|
379
412
|
|
|
380
413
|
```typescript
|
|
381
414
|
@onConnectedAfter
|
|
382
415
|
onconstructor(@inject(UserService.SYMBOL) userService: UserService, @hostSet hs: HostSet) { ... }
|
|
383
416
|
|
|
384
|
-
@
|
|
385
|
-
onRouteChanged(@helperSet helpers: HelperSet, @
|
|
417
|
+
@subscribeSwcAppRouteChange
|
|
418
|
+
onRouteChanged(@helperSet helpers: HelperSet, @swcAppRouterEvent re: RouterEventType) { ... }
|
|
386
419
|
|
|
387
|
-
@
|
|
420
|
+
@subscribeSwcAppMessage
|
|
388
421
|
onMessage(@hostSet hs: HostSet, @appMessage msg: SwcAppMessage) { ... }
|
|
389
422
|
```
|
|
390
423
|
|
|
@@ -402,13 +435,16 @@ async onLogin() {
|
|
|
402
435
|
**@emitCustomEvent Decorator Variants:**
|
|
403
436
|
- `emitCustomEvent(target, type, options)` - Full form
|
|
404
437
|
- `emit(target, type, options)` - Short alias
|
|
405
|
-
- `
|
|
438
|
+
- `emitCustomEvent(type, options)` - Without a selector it emits from the element itself (`emitAppHost` / `emitWindow` / `emitDocument` take `(type, options)`; `emitLight` / `emitShadow` / `emitAll` take `(selector, type, options)`)
|
|
439
|
+
- Options: `bubbles`, `composed`, `cancelable`, `filter`, `valueKey`, `attributeName`, `root`
|
|
440
|
+
- `valueKey`: if the returned object has that key, only its value becomes `detail` (otherwise the whole return value); default key `EMIT_CUSTOM_EVENT_METADATA_KEY`. The method still returns the full value, so several emit/publish decorators can share one return object
|
|
406
441
|
|
|
407
442
|
#### @publishSwcAppMessage
|
|
408
443
|
Publish messages through the message bus when method completes.
|
|
409
444
|
|
|
410
445
|
```typescript
|
|
411
|
-
|
|
446
|
+
// No arguments needed → bare form
|
|
447
|
+
@publishSwcAppMessage
|
|
412
448
|
async onLogin() {
|
|
413
449
|
const user = await this.authService.login();
|
|
414
450
|
return user; // Published as message with data: user
|
|
@@ -422,13 +458,13 @@ updateProfile() {
|
|
|
422
458
|
```
|
|
423
459
|
|
|
424
460
|
**@publishSwcAppMessage Decorator Variants:**
|
|
425
|
-
- `publishSwcAppMessage
|
|
461
|
+
- `publishSwcAppMessage` - Bare decorator, publish without message type
|
|
426
462
|
- `publishSwcAppMessage(messageType)` - Publish with specific message type
|
|
427
463
|
- `publishSwcAppMessage(messageType, { valueKey: 'customKey' })` - Publish with custom value extraction
|
|
428
464
|
- `publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' })` - Publish with options object
|
|
429
|
-
- `
|
|
430
|
-
- `
|
|
431
|
-
- `
|
|
465
|
+
- `publishMessage` - Short alias
|
|
466
|
+
- `publishMessage(messageType)` - Short alias with message type
|
|
467
|
+
- `publishMessage(messageType, options)` - Short alias with options
|
|
432
468
|
|
|
433
469
|
**Using valueKey for Multiple Decorators:**
|
|
434
470
|
|
|
@@ -443,38 +479,40 @@ handleMultipleEvents() {
|
|
|
443
479
|
}
|
|
444
480
|
```
|
|
445
481
|
|
|
446
|
-
####
|
|
447
|
-
|
|
482
|
+
#### Emitting from the element itself
|
|
483
|
+
`@emitCustomEvent(type, options)` without a selector emits from $this. Pass `attributeName` so a parent can attach a handler through that attribute
|
|
484
|
+
(there is no default attribute name; set it explicitly). The attribute script gets `event` and `$data` (= `event.detail`).
|
|
448
485
|
|
|
449
486
|
```typescript
|
|
450
|
-
@
|
|
487
|
+
@emitCustomEvent('navigate', { attributeName: 'on-emit-navigate' })
|
|
451
488
|
onNavClick(e: any) {
|
|
452
489
|
return { path: e.target.dataset.path };
|
|
453
490
|
}
|
|
491
|
+
// Parent: <app-header on-emit-navigate="$host.onHeaderNavigate(event, $data)"></app-header>
|
|
454
492
|
```
|
|
455
493
|
|
|
456
|
-
####
|
|
457
|
-
|
|
494
|
+
#### Listening on the element itself
|
|
495
|
+
`@event(type, options)` without a selector (or a typed alias without one, e.g. bare `@eventClick`) listens on the component element itself (`$this`). If the element has a shadow root, the default `root: 'auto'` binds to the shadowRoot — clicks inside the shadow are received, events dispatched on the element itself are not. Use `@event('click', { root: 'light' })` (or `'all'`) to also catch those.
|
|
458
496
|
|
|
459
497
|
```typescript
|
|
460
|
-
@
|
|
498
|
+
@event('click')
|
|
461
499
|
onHostClick(event: Event) {
|
|
462
500
|
console.log('Host element clicked');
|
|
463
501
|
}
|
|
464
502
|
```
|
|
465
503
|
|
|
466
|
-
#### @
|
|
504
|
+
#### @eventAppHost
|
|
467
505
|
Listen to events on the app root host element (`$appHost` selector). Enables selective event handling with filters.
|
|
468
506
|
|
|
469
507
|
```typescript
|
|
470
508
|
// Listen to all user-action events from $appHost
|
|
471
|
-
@
|
|
509
|
+
@eventAppHost('user-action')
|
|
472
510
|
onUserAction(e: CustomEvent) {
|
|
473
511
|
console.log('User action:', e.detail);
|
|
474
512
|
}
|
|
475
513
|
|
|
476
514
|
// Filter specific events - loose coupling pattern
|
|
477
|
-
@
|
|
515
|
+
@eventAppHost('user-action', {
|
|
478
516
|
filter: (event, helper) => event.detail?.type === 'login'
|
|
479
517
|
})
|
|
480
518
|
onUserLogin(e: CustomEvent) {
|
|
@@ -482,7 +520,7 @@ onUserLogin(e: CustomEvent) {
|
|
|
482
520
|
}
|
|
483
521
|
|
|
484
522
|
// Different component filtering same event differently
|
|
485
|
-
@
|
|
523
|
+
@eventAppHost('user-action', {
|
|
486
524
|
filter: (event, helper) => event.detail?.type === 'logout'
|
|
487
525
|
})
|
|
488
526
|
onUserLogout(e: CustomEvent) {
|
|
@@ -493,7 +531,7 @@ onUserLogout(e: CustomEvent) {
|
|
|
493
531
|
### 5. **DOM Querying**
|
|
494
532
|
|
|
495
533
|
#### @query
|
|
496
|
-
Query single element
|
|
534
|
+
Query a single element. The default `root: 'auto'` searches the shadow root if one exists, otherwise the light DOM.
|
|
497
535
|
|
|
498
536
|
```typescript
|
|
499
537
|
@query('#form-input')
|
|
@@ -514,91 +552,86 @@ textInputs?: HTMLInputElement[];
|
|
|
514
552
|
listItems?: HTMLLIElement[];
|
|
515
553
|
```
|
|
516
554
|
|
|
517
|
-
####
|
|
518
|
-
|
|
555
|
+
#### Special selectors, `pick`, and shorthands
|
|
556
|
+
Special selectors: `$this`, `$host`, `$parentHost`, `$appHost`, `$firstHost`, `$lastHost`, `$firstAppHost`, `$lastAppHost`, `$hosts`, `$appHosts`, `$window`, `$document`. A function selector `(currentThis, helper) => Element | Element[] | NodeList | null` is also accepted (no `root` option).
|
|
519
557
|
|
|
520
558
|
```typescript
|
|
521
|
-
@
|
|
522
|
-
self?: HTMLElement;
|
|
559
|
+
@query // bare = the component itself (same as @query('$this')); @queryAll → [itself]
|
|
560
|
+
self?: HTMLElement; // no arguments → no parentheses: @query() / @queryAll() are type errors
|
|
523
561
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
562
|
+
@query('.item', { pick: 'last' }) // 'first' (default) | 'last' | number → single; 'all' | 'even' | 'odd' → array
|
|
563
|
+
lastItem?: HTMLElement;
|
|
564
|
+
|
|
565
|
+
@(queryIn('shadow', 'even')('.row')) // root + pick factory — the chained call needs the outer parentheses
|
|
566
|
+
evenRows?: HTMLElement[];
|
|
527
567
|
```
|
|
528
568
|
|
|
569
|
+
- Shorthands: `queryLight` / `queryShadow` / `queryAllRoots` (single), `queryAllLight` / `queryAllShadow` / `queryAllAll` (arrays)
|
|
570
|
+
- `filter: (el, { currentThis, helper }) => boolean` narrows the matches
|
|
571
|
+
- Assigning `null` / `undefined` / `[]` to a `@query` field removes the matched elements from the DOM
|
|
572
|
+
|
|
529
573
|
### 6. **Attribute Binding**
|
|
530
574
|
|
|
531
|
-
#### @attribute (Field Decorator)
|
|
532
|
-
|
|
575
|
+
#### @attribute (Field / Method Decorator)
|
|
576
|
+
Works on both fields and methods; behavior depends on where it is applied.
|
|
577
|
+
- **Field**: getter/setter that reads/writes the element attribute (`type` converts to Number/Boolean)
|
|
578
|
+
- **Method**: applies the return value to the attribute. `null` **removes** it, `undefined` does nothing
|
|
533
579
|
|
|
534
|
-
|
|
580
|
+
**One string = attribute name on `$this`**, **two strings = (selector, attribute name)**.
|
|
535
581
|
|
|
536
582
|
```typescript
|
|
537
583
|
@elementDefine('product-card')
|
|
538
584
|
class ProductCard extends HTMLElement {
|
|
539
|
-
//
|
|
585
|
+
// Field — product-id on $this
|
|
540
586
|
@attribute('product-id')
|
|
541
587
|
productId: string;
|
|
542
588
|
|
|
543
|
-
//
|
|
589
|
+
// Field — data-id on the selector target
|
|
544
590
|
@attribute('#user', 'data-id')
|
|
545
591
|
userId: string;
|
|
546
592
|
|
|
547
|
-
//
|
|
593
|
+
// Field — bare (field name = attribute name)
|
|
548
594
|
@attribute
|
|
549
595
|
title: string;
|
|
550
596
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
#### @setAttribute (Method Decorator)
|
|
561
|
-
Set element attributes from method return values.
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
@elementDefine('status-updater')
|
|
565
|
-
class StatusUpdater extends HTMLElement {
|
|
566
|
-
// Set attribute on $this
|
|
567
|
-
@setAttribute('data-status')
|
|
568
|
-
updateStatus() {
|
|
569
|
-
return this.isActive ? 'active' : 'inactive';
|
|
597
|
+
// Method — return value goes to nav[data-active] (null → removed)
|
|
598
|
+
@attribute('nav', 'data-active')
|
|
599
|
+
updateNav() {
|
|
600
|
+
return this.section; // 'releases' | null
|
|
570
601
|
}
|
|
571
602
|
|
|
572
|
-
//
|
|
573
|
-
@
|
|
603
|
+
// Method — data-name on $this
|
|
604
|
+
@attribute('data-name')
|
|
574
605
|
setUserName() {
|
|
575
606
|
return this.userName;
|
|
576
607
|
}
|
|
577
|
-
|
|
578
|
-
// Bare decorator (uses method name as attribute)
|
|
579
|
-
@setAttribute
|
|
580
|
-
setValue() {
|
|
581
|
-
return this.computedValue;
|
|
582
|
-
}
|
|
583
608
|
}
|
|
584
609
|
```
|
|
585
610
|
|
|
586
|
-
**
|
|
587
|
-
- `@attribute
|
|
588
|
-
- `@attribute('
|
|
589
|
-
- `@attribute('attr-name', options)` -
|
|
590
|
-
- `@attribute(
|
|
591
|
-
- `@attribute
|
|
611
|
+
**Patterns:**
|
|
612
|
+
- `@attribute` - `$this`, name = field/method name
|
|
613
|
+
- `@attribute('attr-name', options?)` - attr-name on `$this`
|
|
614
|
+
- `@attribute('selector', 'attr-name', options?)` - selector target (`nav`, `#user`, `.card`, `$appHost`, ...)
|
|
615
|
+
- `@attribute((this, helper) => el, 'attr-name')` - function selector
|
|
616
|
+
- `@attribute('selector', 'attr-name', { valueKey: 'k' })` - (method) set only `k` from the returned object
|
|
617
|
+
- Options: `type`, `filter`, `valueKey`, `root`. Shorthands: `attrAppHost`, `attrLight`, `attrShadow`, `attrAll` (for $this just give the name: `@attribute('name')`)
|
|
618
|
+
- A field getter evaluates a `{{= expr }}` attribute value (see "Attribute Expressions")
|
|
619
|
+
|
|
620
|
+
#### @removeAttribute (Method Decorator)
|
|
621
|
+
Always removes the attribute when the method runs. The return value passes through unchanged,
|
|
622
|
+
so it is safe on subscriber handlers that must not return a value (e.g. route changes).
|
|
592
623
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
624
|
+
```typescript
|
|
625
|
+
// Runs before the other route handlers — reset the nav highlight
|
|
626
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
627
|
+
@removeAttribute('nav', 'href')
|
|
628
|
+
handleNavReset() {
|
|
629
|
+
return undefined; // keep the chain going
|
|
630
|
+
}
|
|
631
|
+
```
|
|
599
632
|
|
|
600
633
|
#### @changedAttribute (Method Decorator)
|
|
601
|
-
Listen for attribute changes on the component
|
|
634
|
+
Listen for attribute changes on the component itself (`attributeChangedCallback`). Handler arguments: `(newValue, oldValue, name, helperHostSet)`; `newValue` is converted by `type` (and a `{{= expr }}` value is evaluated first).
|
|
602
635
|
|
|
603
636
|
```typescript
|
|
604
637
|
@elementDefine('reactive-component')
|
|
@@ -614,80 +647,59 @@ class ReactiveComponent extends HTMLElement {
|
|
|
614
647
|
console.log(`Status is now: ${newValue}`);
|
|
615
648
|
}
|
|
616
649
|
|
|
617
|
-
//
|
|
618
|
-
@changedAttribute
|
|
619
|
-
|
|
620
|
-
console.log('An attribute changed');
|
|
621
|
-
}
|
|
650
|
+
// Bare (or no name) → the method name is used as the attribute name
|
|
651
|
+
@changedAttribute
|
|
652
|
+
status(newValue: string) { }
|
|
622
653
|
}
|
|
623
654
|
```
|
|
624
655
|
|
|
625
656
|
**@changedAttribute Options:**
|
|
626
657
|
- `attributeName` - Attribute name to listen for (optional, defaults to method name)
|
|
627
658
|
- `type` - Type converter: `Number`, `Boolean`, or `String`
|
|
628
|
-
- `while` -
|
|
659
|
+
- `while` - `'connected'`: skip changes while disconnected, and run once on connect with the current value (if set)
|
|
660
|
+
- `filter` / `before` / `finally` - same hook pattern as `@event` (`before` result → `@changedAttributeBeforeReturn`)
|
|
629
661
|
|
|
630
662
|
### 6.5 **Property Binding**
|
|
631
663
|
|
|
632
|
-
#### @property (Field Decorator)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
class InputWrapper extends HTMLElement {
|
|
638
|
-
// Property on $this (auto-detected)
|
|
639
|
-
@property('value')
|
|
640
|
-
inputValue: string;
|
|
664
|
+
#### @property (Field / Method Decorator)
|
|
665
|
+
Works on both fields and methods.
|
|
666
|
+
- **Field**: getter/setter that reads/writes a property of the target element
|
|
667
|
+
- Bare `@property` (= `$this` + same name) stays a plain field with no getter/setter and is only registered as an **SSR hydration target**
|
|
668
|
+
- **Method**: assigns the return value to the target element's property (`undefined` is ignored)
|
|
641
669
|
|
|
642
|
-
|
|
643
|
-
@property('#submit-btn', 'disabled')
|
|
644
|
-
isSubmitDisabled: boolean;
|
|
645
|
-
|
|
646
|
-
// Bare decorator (uses field name as property)
|
|
647
|
-
@property
|
|
648
|
-
checked: boolean;
|
|
649
|
-
}
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
#### @setProperty (Method Decorator)
|
|
653
|
-
Set element properties from method return values.
|
|
670
|
+
Unlike `@attribute`, **the first string is always a selector**. A property on the element itself is just the field, so a name-only form would be meaningless.
|
|
654
671
|
|
|
655
672
|
```typescript
|
|
656
673
|
@elementDefine('form-handler')
|
|
657
674
|
class FormHandler extends HTMLElement {
|
|
658
|
-
//
|
|
659
|
-
@
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
675
|
+
// Field — bare: own field (SSR hydration target)
|
|
676
|
+
@property
|
|
677
|
+
declare rows: Row[];
|
|
678
|
+
|
|
679
|
+
// Field — property of the selector target
|
|
680
|
+
@property('#chart', 'data')
|
|
681
|
+
chartData: number[];
|
|
663
682
|
|
|
664
|
-
//
|
|
665
|
-
@
|
|
683
|
+
// Method — return value goes to #submit-btn.disabled
|
|
684
|
+
@property('#submit-btn', 'disabled')
|
|
666
685
|
updateSubmitState() {
|
|
667
686
|
return this.hasErrors;
|
|
668
687
|
}
|
|
669
688
|
|
|
670
|
-
//
|
|
671
|
-
@
|
|
672
|
-
|
|
673
|
-
return this.
|
|
689
|
+
// Method — same-named property on the selector target (input.value)
|
|
690
|
+
@property('input')
|
|
691
|
+
value() {
|
|
692
|
+
return this.initialValue;
|
|
674
693
|
}
|
|
675
694
|
}
|
|
676
695
|
```
|
|
677
696
|
|
|
678
|
-
**
|
|
679
|
-
- `@property
|
|
680
|
-
- `@property('
|
|
681
|
-
- `@property('propertyName', options)` -
|
|
682
|
-
- `@property(
|
|
683
|
-
- `@
|
|
684
|
-
|
|
685
|
-
**@setProperty Patterns:**
|
|
686
|
-
- `@setProperty('propertyName')` - Set property on $this
|
|
687
|
-
- `@setProperty('#selector', 'propertyName')` - Set property on selector
|
|
688
|
-
- `@setProperty('propertyName', options)` - Set property on $this with options
|
|
689
|
-
- `@setProperty('#selector', 'propertyName', options)` - Set property on selector with options
|
|
690
|
-
- `@setProperty` - Bare decorator (uses method name as property on $this)
|
|
697
|
+
**Patterns:**
|
|
698
|
+
- `@property` - `$this`, name = field/method name
|
|
699
|
+
- `@property('selector', options?)` - same-named property on the target
|
|
700
|
+
- `@property('selector', 'propertyName', options?)` - propertyName on the target
|
|
701
|
+
- `@property((this, helper) => el, 'propertyName')` - function selector
|
|
702
|
+
- To call a method on the target, use `@callProperty('selector', 'methodName')` (an array return value is spread as arguments; `callPropertyLight` / `callPropertyShadow` / `callPropertyAll` also exist)
|
|
691
703
|
|
|
692
704
|
### 6.6 **DOM Observers (@mutationObserver, @resizeObserver, @intersectionObserver)**
|
|
693
705
|
|
|
@@ -714,8 +726,10 @@ class ListObserver extends HTMLElement {
|
|
|
714
726
|
```
|
|
715
727
|
|
|
716
728
|
**Callback signature:**
|
|
717
|
-
- `matchedEls: HTMLElement[]` - Elements matching the selector (1st arg)
|
|
718
|
-
- `mutations: MutationRecord[]` -
|
|
729
|
+
- `matchedEls: HTMLElement[]` - Elements matching the selector (1st arg). For the element itself (bare `@mutationObserver` / `$this`) it is just `[element]` — the added/removed nodes are in `mutations`
|
|
730
|
+
- `mutations: MutationRecord[]` - Mutation records for this method's own targets (2nd arg)
|
|
731
|
+
|
|
732
|
+
Bare `@mutationObserver` (no arguments) watches the element's whole subtree (`childList` + `subtree`) and is attached **before** the first render, so it sees the render itself. Pass options to narrow it (e.g. `@mutationObserver({ childList: true })` = direct children only).
|
|
719
733
|
- `observer: MutationObserver` - The observer instance (3rd arg)
|
|
720
734
|
|
|
721
735
|
**How delegate works:**
|
|
@@ -746,7 +760,7 @@ Detect element size changes and react automatically.
|
|
|
746
760
|
@elementDefine('chart-widget')
|
|
747
761
|
class ChartWidget extends HTMLElement {
|
|
748
762
|
// Re-render when component size changes (selector defaults to $this)
|
|
749
|
-
@resizeObserverLight
|
|
763
|
+
@resizeObserverLight
|
|
750
764
|
onResize(matchedEls: HTMLElement[], entries: ResizeObserverEntry[], observer: ResizeObserver) {
|
|
751
765
|
this.redraw(entries[0]?.contentRect);
|
|
752
766
|
}
|
|
@@ -838,20 +852,20 @@ Surgically add, replace, or remove nodes in the DOM with fine-grained control.
|
|
|
838
852
|
```typescript
|
|
839
853
|
@elementDefine('content-updater')
|
|
840
854
|
class ContentUpdater extends HTMLElement {
|
|
841
|
-
@
|
|
842
|
-
@replaceChildren
|
|
855
|
+
@event('button', 'click')
|
|
856
|
+
@replaceChildren
|
|
843
857
|
updateContent() {
|
|
844
858
|
return `<div>New content</div>`;
|
|
845
859
|
}
|
|
846
860
|
|
|
847
|
-
@
|
|
848
|
-
@
|
|
861
|
+
@event('.append-btn', 'click')
|
|
862
|
+
@insertBeforeEnd
|
|
849
863
|
appendContent() {
|
|
850
864
|
return `<p>Appended content</p>`;
|
|
851
865
|
}
|
|
852
866
|
|
|
853
|
-
@
|
|
854
|
-
@
|
|
867
|
+
@event('.prepend-btn', 'click')
|
|
868
|
+
@insertAfterBegin
|
|
855
869
|
prependContent() {
|
|
856
870
|
return `<p>Prepended content</p>`;
|
|
857
871
|
}
|
|
@@ -870,15 +884,14 @@ class ContentUpdater extends HTMLElement {
|
|
|
870
884
|
```
|
|
871
885
|
|
|
872
886
|
**@applyNode Decorator Variants:**
|
|
873
|
-
- `
|
|
874
|
-
- `replaceChildrenLight
|
|
875
|
-
- `
|
|
876
|
-
- `
|
|
877
|
-
- `
|
|
878
|
-
- `
|
|
879
|
-
- `
|
|
880
|
-
- `
|
|
881
|
-
- `innerText()` - Set innerText
|
|
887
|
+
- `applyNode(selector?, options?)` / `apply` / `node` - Full form (selector defaults to `$this`). Position: with no options at all it is `replaceChildren`; once you pass an options object without `position` it is `beforeEnd` (append) — bare `@applyAppHost` appends too. Pass `position` explicitly when it matters; `applyAppHost` / `applyLight` / `applyShadow` / `applyAll`
|
|
888
|
+
- `@replaceChildren` / `@replaceChildrenLight` - Replace all children
|
|
889
|
+
- `@insertBeforeEnd` / `@insertBeforeEndLight` / `@insertBeforeEndShadow` - Append to end
|
|
890
|
+
- `@insertAfterBegin` / `@insertAfterBeginLight` - Prepend to beginning
|
|
891
|
+
- `@innerHtml` / `@innerHtmlLight` / `@innerHtmlShadow` - Set innerHTML
|
|
892
|
+
- `@innerText` / `@innerTextLight` / `@innerTextShadow` - Set innerText
|
|
893
|
+
- `@clearChildrenNode` / `@clearChildrenLight` - Remove all children
|
|
894
|
+
- Options: `position`, `root`, `filter`, `fallback`, `valueKey`. Ready-made `filter` helpers: `skipIfSameTagPresent`, `skipIfExists(selector)`, `skipIfEmpty`, `applyIfChanged` (e.g. `@innerHtmlLight({ filter: skipIfExists('my-page') })`)
|
|
882
895
|
|
|
883
896
|
**Position Options:**
|
|
884
897
|
```typescript
|
|
@@ -891,7 +904,8 @@ export type ApplyNodePosition =
|
|
|
891
904
|
| 'replaceChildren' // Replace children
|
|
892
905
|
| 'innerHtml' // Set innerHTML
|
|
893
906
|
| 'innerText' // Set innerText
|
|
894
|
-
| 'remove'
|
|
907
|
+
| 'remove' // Remove element
|
|
908
|
+
| 'clearChildren'; // Remove all children
|
|
895
909
|
```
|
|
896
910
|
|
|
897
911
|
### 8. **Style & Class Management**
|
|
@@ -904,8 +918,8 @@ class StyledComponent extends HTMLElement {
|
|
|
904
918
|
@state('isActive')
|
|
905
919
|
isActive = false;
|
|
906
920
|
|
|
907
|
-
@
|
|
908
|
-
@updateStyle
|
|
921
|
+
@event('button', 'click')
|
|
922
|
+
@updateStyle
|
|
909
923
|
toggleStyle() {
|
|
910
924
|
return {
|
|
911
925
|
color: this.isActive ? 'green' : 'red',
|
|
@@ -913,9 +927,9 @@ class StyledComponent extends HTMLElement {
|
|
|
913
927
|
};
|
|
914
928
|
}
|
|
915
929
|
|
|
916
|
-
@
|
|
917
|
-
@updateClass
|
|
918
|
-
|
|
930
|
+
@event('.toggle-btn', 'click')
|
|
931
|
+
@updateClass
|
|
932
|
+
toggleState() {
|
|
919
933
|
return {
|
|
920
934
|
'active': this.isActive,
|
|
921
935
|
'disabled': !this.isActive
|
|
@@ -934,17 +948,18 @@ class StyledComponent extends HTMLElement {
|
|
|
934
948
|
}
|
|
935
949
|
```
|
|
936
950
|
|
|
937
|
-
**@applyStyle Variants:**
|
|
938
|
-
- `
|
|
939
|
-
- `
|
|
940
|
-
- `
|
|
951
|
+
**@applyStyle Variants:** (each takes `(selector?, options?)`; bare form targets `$this`)
|
|
952
|
+
- `setStyle` - Clear and set styles
|
|
953
|
+
- `updateStyle` - Update/merge styles
|
|
954
|
+
- `removeStyle` - Remove specific styles
|
|
955
|
+
- `applyStyle(selector, action?)` / `style` - action `'set' | 'update' | 'remove'` (default `'update'`); `styleAppHost` / `styleLight` / `styleShadow` / `styleAll` (for $this use bare `@setStyle` / `@updateStyle` / `@removeStyle`)
|
|
941
956
|
|
|
942
|
-
**@applyClass Variants:**
|
|
943
|
-
- `
|
|
944
|
-
- `
|
|
945
|
-
- `
|
|
946
|
-
- `
|
|
947
|
-
- `
|
|
957
|
+
**@applyClass Variants:** (each takes `(selector?, classMapOrOptions?, options?)`; bare form targets `$this`)
|
|
958
|
+
- `setClass` - Replace all classes
|
|
959
|
+
- `updateClass` - Toggle classes by a `{ className: boolean }` map
|
|
960
|
+
- `addClass` / `removeClass` / `toggleClass`
|
|
961
|
+
- `applyClass(selector, action?)` / `cls` - action `'set' | 'update' | 'add' | 'remove' | 'toggle'` (default `'update'`); `clsAppHost` / `clsLight` / `clsShadow` / `clsAll` (for $this use bare `@setClass` / `@updateClass` / `@addClass` / `@removeClass` / `@toggleClass`)
|
|
962
|
+
- Note: for the class decorators, an options object in the 2nd position must contain `root` (e.g. `{ root: 'auto', valueKey: 'k' }`); an object without `root` is read as a class map
|
|
948
963
|
|
|
949
964
|
### 8.5 **Lifecycle Hooks**
|
|
950
965
|
|
|
@@ -961,17 +976,18 @@ class MyComponent extends HTMLElement {
|
|
|
961
976
|
|
|
962
977
|
@onConnectedBefore({ order: 0 })
|
|
963
978
|
beforeConnected() {
|
|
964
|
-
// Called
|
|
979
|
+
// Called on connect, before rendering
|
|
965
980
|
}
|
|
966
981
|
|
|
967
982
|
@onConnectedAfter({ order: 1 })
|
|
968
983
|
afterConnected() {
|
|
969
|
-
// Called after
|
|
984
|
+
// Called on connect, after rendering and observer setup
|
|
970
985
|
}
|
|
971
986
|
|
|
972
|
-
@
|
|
973
|
-
|
|
974
|
-
// Called when element enters DOM
|
|
987
|
+
@onConnectedBody
|
|
988
|
+
render() {
|
|
989
|
+
// Called when element enters DOM — renders the returned HTML (variants: @onConnectedBodyShadow / @onConnectedBodyLight)
|
|
990
|
+
return `<div>...</div>`;
|
|
975
991
|
}
|
|
976
992
|
|
|
977
993
|
@onDisconnectedBefore
|
|
@@ -979,7 +995,7 @@ class MyComponent extends HTMLElement {
|
|
|
979
995
|
// Called before element disconnects from DOM
|
|
980
996
|
}
|
|
981
997
|
|
|
982
|
-
@onDisconnected
|
|
998
|
+
@onDisconnected // alias of @onDisconnectedAfter
|
|
983
999
|
onDisconnected() {
|
|
984
1000
|
// Called when element leaves DOM
|
|
985
1001
|
}
|
|
@@ -989,15 +1005,15 @@ class MyComponent extends HTMLElement {
|
|
|
989
1005
|
// Called before element is adopted into new document
|
|
990
1006
|
}
|
|
991
1007
|
|
|
992
|
-
@onAdopted
|
|
1008
|
+
@onAdopted // alias of @onAdoptedAfter
|
|
993
1009
|
onAdopted() {
|
|
994
1010
|
// Called when element is adopted into new document
|
|
995
1011
|
}
|
|
996
1012
|
|
|
997
1013
|
@onConnectedSwcApp({ order: 0 })
|
|
998
1014
|
onSwcAppConnected() {
|
|
999
|
-
// Called
|
|
1000
|
-
//
|
|
1015
|
+
// Called when the element registers with a connected SwcApp host
|
|
1016
|
+
// (deferred until SwcApp.connect() if the app is not connected yet)
|
|
1001
1017
|
}
|
|
1002
1018
|
|
|
1003
1019
|
@onConnectedCompleted({ order: 0 })
|
|
@@ -1007,17 +1023,16 @@ class MyComponent extends HTMLElement {
|
|
|
1007
1023
|
}
|
|
1008
1024
|
```
|
|
1009
1025
|
|
|
1010
|
-
**Lifecycle Execution Order:**
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
- `@onAdopted` - Document adoption
|
|
1026
|
+
**Lifecycle Execution Order (connectedCallback):**
|
|
1027
|
+
1. `@onInitialize` - once, on first init (DI via `@inject` parameters)
|
|
1028
|
+
2. `@onConnectedSwcApp` - the element registers with its app host (deferred until `connect()` if needed)
|
|
1029
|
+
3. `@onConnectedBefore` - before rendering
|
|
1030
|
+
4. `@onConnectedBody` (`Shadow` / `Light`) - render the returned HTML/Node (skipped for SSR-rendered elements)
|
|
1031
|
+
5. Observers / timers / listeners are wired up
|
|
1032
|
+
6. `@onConnectedAfter` - after rendering
|
|
1033
|
+
7. `@onConnectedCompleted` - always runs last (in a `finally` block), then `trigger: 'connectedDone'` replays fire
|
|
1034
|
+
|
|
1035
|
+
Disconnect: `@onDisconnectedBefore` → cleanup → `@onDisconnected` (`@onDisconnectedAfter`). Adopt: `@onAdoptedBefore` → `@onAdopted` (`@onAdoptedAfter`).
|
|
1021
1036
|
|
|
1022
1037
|
**Order Parameter:**
|
|
1023
1038
|
All lifecycle decorators support `order?: number` for controlling execution sequence:
|
|
@@ -1029,17 +1044,19 @@ onFirst() { }
|
|
|
1029
1044
|
onSecond() { }
|
|
1030
1045
|
```
|
|
1031
1046
|
|
|
1032
|
-
### 8.6 **Timers (@setInterval, @setTimeout)**
|
|
1047
|
+
### 8.6 **Timers (@setInterval, @setTimeout, @requestAnimationFrame)**
|
|
1033
1048
|
|
|
1034
|
-
Declaratively
|
|
1049
|
+
Declaratively schedule work tied to the component. Every timer the decorator starts is cleared automatically on disconnect. There are two modes (`type` option):
|
|
1035
1050
|
|
|
1036
|
-
-
|
|
1037
|
-
-
|
|
1051
|
+
- `type: 'returnValue'` (**default**) - nothing starts automatically. When you call the method, its return value (a function, or the function under `valueKey`) becomes the repeating/delayed callback, called with `(id: number)`. The original return value passes through.
|
|
1052
|
+
- `type: 'onConnected'` - starts on connect and calls the decorated method itself on every tick/fire.
|
|
1038
1053
|
|
|
1039
1054
|
```typescript
|
|
1040
1055
|
@elementDefine('live-clock')
|
|
1041
1056
|
class LiveClock extends HTMLElement {
|
|
1057
|
+
// Auto-start on connect
|
|
1042
1058
|
@setInterval(1000, {
|
|
1059
|
+
type: 'onConnected',
|
|
1043
1060
|
parameter: (set) => [Date.now()],
|
|
1044
1061
|
created: (set, id) => console.log('interval started, timer id:', id)
|
|
1045
1062
|
})
|
|
@@ -1047,94 +1064,94 @@ class LiveClock extends HTMLElement {
|
|
|
1047
1064
|
this.textContent = new Date(now).toLocaleTimeString();
|
|
1048
1065
|
}
|
|
1049
1066
|
|
|
1050
|
-
// No options - called with no arguments, once per second
|
|
1051
|
-
@setInterval(1000)
|
|
1052
|
-
onTick() {
|
|
1053
|
-
console.log('tick');
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
1067
|
// Fires once, 3 seconds after connect
|
|
1057
|
-
@setTimeout(3000)
|
|
1068
|
+
@setTimeout(3000, { type: 'onConnected' })
|
|
1058
1069
|
onceAfter3s() {
|
|
1059
1070
|
console.log('3 seconds have passed');
|
|
1060
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
// Default 'returnValue': starts when you call startPolling()
|
|
1074
|
+
@setInterval(900)
|
|
1075
|
+
startPolling() {
|
|
1076
|
+
return (id: number) => console.log('interval', id, 'ticked');
|
|
1077
|
+
}
|
|
1061
1078
|
}
|
|
1062
1079
|
```
|
|
1063
1080
|
|
|
1064
1081
|
**`@setInterval(interval, options?)` / `@setTimeout(delay, options?)`:**
|
|
1065
1082
|
- `interval` / `delay` - milliseconds. `setInterval` repeats; `setTimeout` fires once.
|
|
1066
|
-
- `
|
|
1067
|
-
- `
|
|
1068
|
-
- `
|
|
1083
|
+
- `type` - `'returnValue'` (default) | `'onConnected'`
|
|
1084
|
+
- `parameter?: (set: HelperHostSet) => any[]` - (`onConnected`) arguments for each tick/fire; omitted → no arguments.
|
|
1085
|
+
- `created?: (set: HelperHostSet, id: number) => void` - called once right after the timer is armed. For logging only; cleanup is automatic.
|
|
1086
|
+
- `filter` / `before` / `finally` - (`onConnected`) per-tick hooks; `before` result → `@setIntervalBeforeReturn` / `@setTimeoutBeforeReturn`.
|
|
1087
|
+
- `valueKey?: symbol | string` - which key of the returned object holds the callback function (defaults to `SET_INTERVAL_METADATA_KEY` / `SET_TIMEOUT_METADATA_KEY`).
|
|
1088
|
+
|
|
1089
|
+
**`@requestAnimationFrame(options?)`** (bare form also works) - same two modes. The frame callback is `(timestamp, prevValue) => any`: return `undefined`/`null` to stop the loop, anything else is passed as `prevValue` to the next frame. Options: `type`, `created`, `valueKey`.
|
|
1069
1090
|
|
|
1070
1091
|
```typescript
|
|
1071
|
-
@
|
|
1072
|
-
|
|
1073
|
-
return
|
|
1074
|
-
onTick: (id: number) => console.log('interval', id, 'ticked'),
|
|
1075
|
-
};
|
|
1092
|
+
@requestAnimationFrame
|
|
1093
|
+
animate() {
|
|
1094
|
+
return (ts: number, prev = 0) => (prev < 100 ? prev + 1 : null); // stops at 100
|
|
1076
1095
|
}
|
|
1077
1096
|
```
|
|
1078
1097
|
|
|
1079
|
-
|
|
1098
|
+
### 8.7 **@around (AOP) and @eventMedia**
|
|
1080
1099
|
|
|
1081
|
-
|
|
1100
|
+
`@around` wraps a method (`before(helper, args)` → new args, `after(helper, result)` → new result, `finally(helper, { args, result, error })`) or a field (`get(helper, stored)`, `set(helper, incoming)`). Async hooks are supported when the method is async.
|
|
1082
1101
|
|
|
1083
|
-
|
|
1102
|
+
```typescript
|
|
1103
|
+
@around({ before: (h, args: [string]) => [args[0].trim()], after: (h, r: string) => r + '!' })
|
|
1104
|
+
greet(name: string) { return `hi ${name}`; }
|
|
1084
1105
|
|
|
1085
|
-
|
|
1106
|
+
@around({ set: (h, v: string) => v?.trim() })
|
|
1107
|
+
label: string = '';
|
|
1108
|
+
```
|
|
1086
1109
|
|
|
1087
|
-
|
|
1110
|
+
`@eventMedia(query, eventType, options?)` / `@eventMediaChange(query, options?)` subscribe to `window.matchMedia(query)` on connect and unsubscribe on disconnect. Handler arguments: `(event: MediaQueryListEvent, helperHostSet)`. Options: `filter`, `before`, `finally`.
|
|
1088
1111
|
|
|
1089
|
-
|
|
1112
|
+
```typescript
|
|
1113
|
+
@eventMediaChange('(max-width: 600px)')
|
|
1114
|
+
onMobile(e: MediaQueryListEvent) { this.compact = e.matches; }
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### 9. **Attribute Expressions (`{{= }}`)**
|
|
1090
1118
|
|
|
1091
|
-
|
|
1092
|
-
- Used for boolean conditions and value comparisons
|
|
1093
|
-
- Syntax: `{{ expression }}`
|
|
1094
|
-
- Example: `{{ $value === 'pending' }}`, `{{ $host.isLoggedIn }}`
|
|
1095
|
-
- Context variables: `$value`, `$item`, `$index`, `$host`, `$parentHost`, `$appHost`, etc.
|
|
1119
|
+
An attribute value of the form `{{= expression }}` is evaluated as JavaScript instead of being used as a string:
|
|
1096
1120
|
|
|
1097
|
-
|
|
1098
|
-
-
|
|
1099
|
-
|
|
1100
|
-
- Automatically executes functions and captures return values
|
|
1101
|
-
- Example: `{{= $item.name }}`, `{{= $parentHost.getData() }}`, `{{= formatDate($item.date) }}`
|
|
1102
|
-
- Used in attributes for dynamic substitution
|
|
1103
|
-
- Result is converted to string for DOM attributes
|
|
1121
|
+
```html
|
|
1122
|
+
<product-detail product-id="{{= $host.selectedId }}"></product-detail>
|
|
1123
|
+
```
|
|
1104
1124
|
|
|
1105
|
-
|
|
1106
|
-
-
|
|
1107
|
-
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
- `$item` - current item (in SwcLoop)
|
|
1111
|
-
- `$index` - current index (in SwcLoop)
|
|
1112
|
-
- Helper functions - utility methods from `SwcUtils.getHelperAndHostSet()`
|
|
1125
|
+
- Evaluated when the value is read by an `@attribute` field getter and before it is passed to `@changedAttribute` handlers
|
|
1126
|
+
- `this` is the element that owns the attribute; the helper/host variables are in scope: `$this`, `$host`, `$parentHost`, `$hosts`, `$firstHost`, `$lastHost`, `$appHost`, `$appHosts`, `$d`, `$w`, `$q`, `$qa`, `$qi`
|
|
1127
|
+
- The result keeps its JavaScript type (then `type` conversion applies); on error the raw string is used
|
|
1128
|
+
|
|
1129
|
+
Template bindings inside `@onConnectedBody*` HTML (`@state@`, `<!--[html ]-->`, `<!--[text ]-->`, `a::`, `e::`, and `p::` for DOM properties) are described in the `@state` section.
|
|
1113
1130
|
|
|
1114
1131
|
---
|
|
1115
1132
|
|
|
1116
|
-
###
|
|
1133
|
+
### 10. **Component Communication - Message Bus**
|
|
1117
1134
|
|
|
1118
1135
|
SWC provides a powerful message bus system for inter-component communication through SwcApp. Components can publish and subscribe to typed messages while connected.
|
|
1119
1136
|
|
|
1120
|
-
#### @
|
|
1137
|
+
#### @subscribeSwcAppMessage
|
|
1121
1138
|
Subscribe to messages while component is connected to DOM.
|
|
1122
1139
|
|
|
1123
1140
|
```typescript
|
|
1124
1141
|
@elementDefine('notification-panel')
|
|
1125
1142
|
class NotificationPanel extends HTMLElement {
|
|
1126
|
-
@
|
|
1143
|
+
@subscribeSwcAppMessage
|
|
1127
1144
|
onAnyMessage(message: SwcAppMessage) {
|
|
1128
1145
|
console.log('Received message:', message);
|
|
1129
1146
|
}
|
|
1130
1147
|
|
|
1131
|
-
@
|
|
1148
|
+
@subscribeSwcAppMessage('user-login')
|
|
1132
1149
|
onUserLogin(message: SwcAppMessage<{ username: string }>) {
|
|
1133
1150
|
console.log(`Welcome ${message.data?.username}`);
|
|
1134
1151
|
}
|
|
1135
1152
|
|
|
1136
|
-
@
|
|
1137
|
-
filter: (msg) => msg.data?.username === 'admin'
|
|
1153
|
+
@subscribeSwcAppMessage('user-login', {
|
|
1154
|
+
filter: (msg, currentThis) => msg.data?.username === 'admin'
|
|
1138
1155
|
})
|
|
1139
1156
|
onAdminLogin(message: SwcAppMessage) {
|
|
1140
1157
|
console.log('Admin logged in!');
|
|
@@ -1142,13 +1159,16 @@ class NotificationPanel extends HTMLElement {
|
|
|
1142
1159
|
}
|
|
1143
1160
|
```
|
|
1144
1161
|
|
|
1162
|
+
**@subscribeSwcAppMessage Options:** `filter(message, currentThis)`, `before(message, currentThis)` (result → `@appMessageBeforeReturn`), `finally(message, currentThis, { args, result, error })`, `subject`, `trigger` (see "Message Replay"). Every matching subscriber runs (fire-and-forget); return values do not stop other subscribers.
|
|
1163
|
+
|
|
1145
1164
|
#### @publishSwcAppMessage
|
|
1146
1165
|
Publish a message from a method's return value.
|
|
1147
1166
|
|
|
1148
1167
|
```typescript
|
|
1149
1168
|
@elementDefine('login-form')
|
|
1150
1169
|
class LoginForm extends HTMLElement {
|
|
1151
|
-
|
|
1170
|
+
// No arguments needed → bare form
|
|
1171
|
+
@publishSwcAppMessage
|
|
1152
1172
|
async onSubmit() {
|
|
1153
1173
|
const user = await this.authService.login(
|
|
1154
1174
|
this.username,
|
|
@@ -1166,11 +1186,11 @@ class LoginForm extends HTMLElement {
|
|
|
1166
1186
|
```
|
|
1167
1187
|
|
|
1168
1188
|
**@publishSwcAppMessage Decorator Variants:**
|
|
1169
|
-
- `publishSwcAppMessage
|
|
1189
|
+
- `publishSwcAppMessage` - Publish without message type (bare)
|
|
1170
1190
|
- `publishSwcAppMessage(messageType)` - Publish with specific message type
|
|
1171
1191
|
- `publishSwcAppMessage(messageType, { valueKey: 'customKey' })` - Publish with custom value extraction
|
|
1172
1192
|
- `publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' })` - Publish with options object
|
|
1173
|
-
- `publishMessage
|
|
1193
|
+
- `publishMessage` - Short alias
|
|
1174
1194
|
- `publishMessage(messageType)` - Short alias with message type
|
|
1175
1195
|
- `publishMessage(messageType, options)` - Short alias with options
|
|
1176
1196
|
|
|
@@ -1202,7 +1222,7 @@ type SwcAppMessage<T = any> = {
|
|
|
1202
1222
|
- ✅ **Filter support** - use custom filter functions to handle specific conditions
|
|
1203
1223
|
- ✅ **Auto-publishing** - return value automatically becomes message payload
|
|
1204
1224
|
- ✅ **Async support** - works with async methods (promises)
|
|
1205
|
-
- ✅ **Centralized** - all messages routed through SwcApp host
|
|
1225
|
+
- ✅ **Centralized** - all messages routed through the nearest SwcApp host (`$appHost`)
|
|
1206
1226
|
- ✅ **Decoupled** - components don't need to know about each other
|
|
1207
1227
|
|
|
1208
1228
|
**Usage Example:**
|
|
@@ -1220,7 +1240,7 @@ class ProductList extends HTMLElement {
|
|
|
1220
1240
|
// Subscriber component
|
|
1221
1241
|
@elementDefine('product-detail')
|
|
1222
1242
|
class ProductDetail extends HTMLElement {
|
|
1223
|
-
@
|
|
1243
|
+
@subscribeSwcAppMessage('product-selected')
|
|
1224
1244
|
onProductSelected(message: SwcAppMessage<Product>) {
|
|
1225
1245
|
this.displayProduct(message.data);
|
|
1226
1246
|
}
|
|
@@ -1236,6 +1256,8 @@ SWC provides a flexible Mixin-based architecture for creating SwcApp elements th
|
|
|
1236
1256
|
#### Available SwcApp Variants
|
|
1237
1257
|
|
|
1238
1258
|
```typescript
|
|
1259
|
+
// SwcApp, SwcAppBody, ... are createElement helpers: SwcAppBody(w, data?) creates the element.
|
|
1260
|
+
// defineSwcApp, defineSwcAppBody, ... register the custom element.
|
|
1239
1261
|
import {
|
|
1240
1262
|
SwcApp, // swc-app (HTMLElement)
|
|
1241
1263
|
SwcAppBody, // swc-app-body (HTMLBodyElement, is="body")
|
|
@@ -1292,7 +1314,7 @@ w.document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1292
1314
|
path: '/',
|
|
1293
1315
|
routeType: 'path',
|
|
1294
1316
|
container: container,
|
|
1295
|
-
onStartedLazyDefineComponent: [yourComponentFactory1,
|
|
1317
|
+
onStartedLazyDefineComponent: [yourComponentFactory1, yourComponentFactory2],
|
|
1296
1318
|
window: w,
|
|
1297
1319
|
onEngineStarted: () => {
|
|
1298
1320
|
console.log('🚀 Application started successfully');
|
|
@@ -1302,6 +1324,10 @@ w.document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1302
1324
|
});
|
|
1303
1325
|
```
|
|
1304
1326
|
|
|
1327
|
+
**Main `connect()` options (`SwcAttributeConfigType`):** `routeType` (`'path' | 'hash' | 'element'`), `path` (first URL), `container` (DI container symbol), `window`, `onStartedLazyDefineComponent` (component factories), `onEngineStarted(sp, app)`, `onConnected` / `onDisconnected`, `onConnectedChildBefore` / `onConnectedChildAfter`, `onDisconnectedChildBefore` / `onDisconnectedChildAfter`, `onChildrenConnectedDone`, `onChildrenRouteChanged`, `messageReplayBufferSize`, `ssr`, `otherInstanceSim`.
|
|
1328
|
+
|
|
1329
|
+
App host methods: `routing(path)`, `back()`, `forward()`, `reload()`, `publishMessage(message)`, `observeMessage(...)`, `connectedElements()`.
|
|
1330
|
+
|
|
1305
1331
|
#### Factory Pattern - Component Registration
|
|
1306
1332
|
|
|
1307
1333
|
```typescript
|
|
@@ -1341,7 +1367,7 @@ appElement.connect({
|
|
|
1341
1367
|
});
|
|
1342
1368
|
```
|
|
1343
1369
|
|
|
1344
|
-
#### 12. **Routing with @
|
|
1370
|
+
#### 12. **Routing with @subscribeSwcAppRouteChange**
|
|
1345
1371
|
|
|
1346
1372
|
SWC provides declarative routing with automatic path matching, order-based execution, and optional propagation control.
|
|
1347
1373
|
|
|
@@ -1350,26 +1376,26 @@ SWC provides declarative routing with automatic path matching, order-based execu
|
|
|
1350
1376
|
```typescript
|
|
1351
1377
|
@elementDefine('root-router')
|
|
1352
1378
|
class RootRouter extends HTMLElement {
|
|
1353
|
-
@
|
|
1379
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 })
|
|
1354
1380
|
@innerHtmlLight
|
|
1355
1381
|
handleHome(routerPathSet: RouterEventType) {
|
|
1356
1382
|
return `<landing-page/>`;
|
|
1357
1383
|
}
|
|
1358
1384
|
|
|
1359
|
-
@
|
|
1385
|
+
@subscribeSwcAppRouteChange(['/products'], { order: 1 })
|
|
1360
1386
|
@innerHtmlLight
|
|
1361
1387
|
handleProducts(routerPathSet: RouterEventType) {
|
|
1362
1388
|
return `<products-list/>`;
|
|
1363
1389
|
}
|
|
1364
1390
|
|
|
1365
|
-
@
|
|
1391
|
+
@subscribeSwcAppRouteChange(['/product/{id}'], { order: 2 })
|
|
1366
1392
|
@innerHtmlLight
|
|
1367
1393
|
handleProductDetail(routerPathSet: RouterEventType) {
|
|
1368
1394
|
const { id } = routerPathSet.pathData;
|
|
1369
1395
|
return `<product-detail product-id="${id}"/>`;
|
|
1370
1396
|
}
|
|
1371
1397
|
|
|
1372
|
-
@
|
|
1398
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 })
|
|
1373
1399
|
@innerHtmlLight
|
|
1374
1400
|
handle404(routerPathSet: RouterEventType) {
|
|
1375
1401
|
return `<not-found-page/>`;
|
|
@@ -1380,48 +1406,138 @@ class RootRouter extends HTMLElement {
|
|
|
1380
1406
|
#### Route Handler Features
|
|
1381
1407
|
|
|
1382
1408
|
**1. Path Matching**
|
|
1383
|
-
- **No path pattern** (omit path): `@
|
|
1384
|
-
- **
|
|
1385
|
-
- **
|
|
1386
|
-
- **
|
|
1387
|
-
- **
|
|
1409
|
+
- **No path pattern** (omit path): `@subscribeSwcAppRouteChange({ order: -1 })` - matches all routes
|
|
1410
|
+
- Patterns are matched against the **whole** path (anchored regex); a string, an array (first match wins), or a function `(currentThis) => string | string[]`
|
|
1411
|
+
- **Exact match**: `['', '/']` - matches home route only; `['/products']` matches `/products` only
|
|
1412
|
+
- **Sub paths**: `['/products/{tail:.*}']` - use a regex segment to match `/products/...`
|
|
1413
|
+
- **Dynamic segments**: `['/product/{id}']` - captures `id` (`[^/]+`); custom regex: `{slug:[a-z0-9-]+}`
|
|
1414
|
+
- **Wildcard**: `['/{tail:.*}']` - matches any path (use for 404)
|
|
1388
1415
|
|
|
1389
1416
|
**2. Order-Based Execution**
|
|
1390
|
-
|
|
1417
|
+
Handlers of one element run in `order` (lowest first, default 0). The first matching handler that returns a value stops the rest of **that element's** chain:
|
|
1391
1418
|
|
|
1392
1419
|
```typescript
|
|
1393
|
-
@
|
|
1394
|
-
@
|
|
1395
|
-
@
|
|
1420
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 }) // Checked first
|
|
1421
|
+
@subscribeSwcAppRouteChange(['/admin'], { order: 1 }) // Checked second
|
|
1422
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 }) // Checked last (404)
|
|
1396
1423
|
```
|
|
1397
1424
|
|
|
1398
1425
|
**3. Propagation Control**
|
|
1399
|
-
- **Return a value** (
|
|
1400
|
-
- **Return undefined
|
|
1426
|
+
- **Return a value** (anything other than `undefined`/`null`, after `valueKey` extraction) → stops this element's remaining handlers
|
|
1427
|
+
- **Return `undefined`/`null`** → continues to the next handler
|
|
1401
1428
|
|
|
1402
1429
|
```typescript
|
|
1403
1430
|
// This handler stops propagation (returns HTML)
|
|
1404
|
-
@
|
|
1431
|
+
@subscribeSwcAppRouteChange(['/admin'], { order: 1 })
|
|
1405
1432
|
@innerHtmlLight
|
|
1406
1433
|
handleAdmin(routerPathSet: RouterEventType) {
|
|
1407
1434
|
return `<admin-panel/>`; // ✅ Stops here
|
|
1408
1435
|
}
|
|
1409
1436
|
|
|
1410
1437
|
// This handler continues propagation (no return value)
|
|
1411
|
-
@
|
|
1438
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
1412
1439
|
onRouteChange(routerPathSet: RouterEventType) {
|
|
1413
1440
|
console.log('Route changed:', routerPathSet.path);
|
|
1414
1441
|
// No return value → continues to next handler
|
|
1415
1442
|
}
|
|
1416
1443
|
```
|
|
1417
1444
|
|
|
1445
|
+
**4. When to run: `on`** (default `'match'` — the behavior above)
|
|
1446
|
+
|
|
1447
|
+
| `on` | Runs when |
|
|
1448
|
+
|---|---|
|
|
1449
|
+
| `'match'` | the path matches (every route change) |
|
|
1450
|
+
| `'enter'` | it didn't match before and matches now (also the replay when the element connects) |
|
|
1451
|
+
| `'update'` | it keeps matching but the path or query changed |
|
|
1452
|
+
| `'leave'` | it matched before and doesn't now — only for elements that stay connected (a page that gets replaced should clean up in `onDisconnected`) |
|
|
1453
|
+
| `'beforeLeave'` | before leaving this route; return `false` (or `Promise<false>`) to cancel the navigation |
|
|
1454
|
+
|
|
1455
|
+
A bare `@subscribeSwcAppRouteChange` (no path) matches every route: with `'match'` it runs on every change, `'update'` on every URL change, `'enter'` once.
|
|
1456
|
+
|
|
1457
|
+
```typescript
|
|
1458
|
+
@subscribeSwcAppRouteChange('/releases/{releaseSeq}/tasks/{taskSeq}', { on: 'update' })
|
|
1459
|
+
@innerHtml('.card')
|
|
1460
|
+
swapCard(@swcAppRoutePathVariable('taskSeq') taskSeq: string) { ... } // same page, only the card changes
|
|
1461
|
+
|
|
1462
|
+
@subscribeSwcAppRouteChange('/edit/{id}', { on: 'beforeLeave' })
|
|
1463
|
+
confirmLeave() { return this.dirty ? confirm('Discard your changes?') : true; }
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
`beforeLeave` covers `router.go(...)` and the browser back/forward buttons. Back/forward have already changed the history when the guard runs, so a cancel restores the previous URL with `pushState`. Calling `pushState`/`replaceState` directly and closing the tab (`beforeunload`) are not covered. The handler receives the route being left with `to` (the target `RouteData`).
|
|
1467
|
+
|
|
1468
|
+
**5. Route parameter decorators**
|
|
1469
|
+
|
|
1470
|
+
No prefix means *first*; `First`/`Last` pick the first/last value when a key repeats; plural forms return every value.
|
|
1471
|
+
|
|
1472
|
+
| Query (`?tag=a&tag=b`) | Value | Path (`/a/{id}/b/{id}` ← `/a/1/b/2`) | Value |
|
|
1473
|
+
|---|---|---|---|
|
|
1474
|
+
| `@swcAppRouteQueryParam('tag')` | `'a'` (`null` if absent) | `@swcAppRoutePathVariable('id')` | `'1'` |
|
|
1475
|
+
| `@swcAppRouteFirstQueryParam('tag')` | `'a'` | `@swcAppRouteFirstPathVariable('id')` | `'1'` |
|
|
1476
|
+
| `@swcAppRouteLastQueryParam('tag')` | `'b'` | `@swcAppRouteLastPathVariable('id')` | `'2'` |
|
|
1477
|
+
| `@swcAppRouteQueryParams('tag')` | `['a','b']` | `@swcAppRoutePathVariables('id')` | `['1','2']` |
|
|
1478
|
+
| `@swcAppRouteQueryParamObject` | `{ tag: 'a' }` | `@swcAppRoutePathVariableObject` | `{ id: '1' }` |
|
|
1479
|
+
| `@swcAppRouteFirstQueryParamObject` | `{ tag: 'a' }` | `@swcAppRouteFirstPathVariableObject` | `{ id: '1' }` |
|
|
1480
|
+
| `@swcAppRouteLastQueryParamObject` | `{ tag: 'b' }` | `@swcAppRouteLastPathVariableObject` | `{ id: '2' }` |
|
|
1481
|
+
| `@swcAppRouteQueryParamsObject` | `{ tag: ['a','b'] }` | `@swcAppRoutePathVariablesObject` | `{ id: ['1','2'] }` |
|
|
1482
|
+
| `@swcAppRouteURLSearchParams` | `URLSearchParams` | | |
|
|
1483
|
+
|
|
1484
|
+
```typescript
|
|
1485
|
+
@subscribeSwcAppRouteChange('/releases/{releaseSeq}')
|
|
1486
|
+
load(@swcAppRoutePathVariable('releaseSeq') seq: string, @swcAppRouteQueryParam('tab') tab: string | null) { ... }
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
- Once any parameter is decorated, the positional route argument is no longer passed — add `@swcAppRouterEvent` for the whole event.
|
|
1490
|
+
- `route.pathData` keeps its old behavior: a repeated variable name holds the **last** value. Use the decorators above to pick first/last explicitly.
|
|
1491
|
+
|
|
1492
|
+
**6. Navigating: `@swcAppRoute` / `@swcAppRouteGo`**
|
|
1493
|
+
|
|
1494
|
+
The method returns where to go; the decorator calls the element's SwcApp router — no `Router` injection needed. `@swcAppRoute({ type })` picks the Router method (default `'go'`); `@swcAppRouteGo`, `@swcAppRoutePush`, ... are aliases with the type fixed.
|
|
1495
|
+
|
|
1496
|
+
```typescript
|
|
1497
|
+
@eventDelegateAll('[data-href]', 'click')
|
|
1498
|
+
@swcAppRouteGo
|
|
1499
|
+
onNavigate(@matchedElement el: Element) {
|
|
1500
|
+
return el.getAttribute('data-href');
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
@eventShadow('form', 'submit', { preventDefault: true })
|
|
1504
|
+
@swcAppRouteGo
|
|
1505
|
+
async onSubmit() {
|
|
1506
|
+
await this.authService.login(...);
|
|
1507
|
+
return '/releases'; // after an async body too
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
| Return value (`go`) | Result |
|
|
1512
|
+
|---|---|
|
|
1513
|
+
| `'/path'` / `{ path, searchParams }` | `router.go(...)` |
|
|
1514
|
+
| `{ path, replace?, state?, scrollToTop? }` | `router.go` with those options |
|
|
1515
|
+
| a number | history move (`-1` = back) |
|
|
1516
|
+
| `undefined` / `null` / `false` | no navigation (conditional) |
|
|
1517
|
+
|
|
1518
|
+
- Other Router methods: `@swcAppRoute({ type: 'push' })` or the alias with the type fixed (`@swcAppRoutePush` / `@swcAppRoutePush({ state, valueKey })`):
|
|
1519
|
+
|
|
1520
|
+
| Alias | `type` | Return value |
|
|
1521
|
+
|---|---|---|
|
|
1522
|
+
| `@swcAppRouteGo` | `go` (default) | see the table above |
|
|
1523
|
+
| `@swcAppRoutePush` / `@swcAppRouteReplace` | `push` / `replace` | `RouteAction` |
|
|
1524
|
+
| `@swcAppRoutePushUpsertSearchParam` / `@swcAppRouteReplaceUpsertSearchParam` | `pushUpsertSearchParam` / `replaceUpsertSearchParam` | `{ key: value \| value[] }` |
|
|
1525
|
+
| `@swcAppRoutePushAddSearchParam` / `@swcAppRouteReplaceAddSearchParam` | `pushAddSearchParam` / `replaceAddSearchParam` | `[[key, value], ...]` |
|
|
1526
|
+
| `@swcAppRoutePushDeleteSearchParam` / `@swcAppRouteReplaceDeleteSearchParam` | `pushDeleteSearchParam` / `replaceDeleteSearchParam` | `string \| string[]` |
|
|
1527
|
+
| `@swcAppRoutePushDeleteHashSearchParam` / `@swcAppRouteReplaceDeleteHashSearchParam` | `pushDeleteHashSearchParam` / `replaceDeleteHashSearchParam` | `string \| string[]` |
|
|
1528
|
+
|
|
1529
|
+
`push`/`replace` go through `beforeLeave` guards like `go`. The search-param methods keep the path, so they don't count as leaving and skip the guards.
|
|
1530
|
+
- Options: `@swcAppRoute({ type, state, valueKey, filter })`, plus `replace` / `scrollToTop` for `go` (`@swcAppRouteGo({ replace: true })`). `valueKey` picks the target out of a return object when stacked with other output decorators (default key `SWC_APP_ROUTE_METADATA_KEY`).
|
|
1531
|
+
- `filter(router, value, { currentThis, helper })` → `false` skips the navigation (same shape as the other output decorators' `filter`; `value` is after `valueKey` extraction). E.g. `@swcAppRouteGo({ filter: (router, to) => to !== router.value.path })`.
|
|
1532
|
+
- The original return value still passes through, and navigation goes through `router.go`, so `beforeLeave` guards apply.
|
|
1533
|
+
|
|
1418
1534
|
#### Advanced Example with Logging
|
|
1419
1535
|
|
|
1420
1536
|
```typescript
|
|
1421
1537
|
@elementDefine('accommodation-router')
|
|
1422
1538
|
class AccommodationRouter extends HTMLElement {
|
|
1423
1539
|
// Global route logger (order: -1 runs first, no path pattern = matches all routes)
|
|
1424
|
-
@
|
|
1540
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
1425
1541
|
onRouteChange(routerPathSet: RouterEventType) {
|
|
1426
1542
|
console.log('[Route Change]', {
|
|
1427
1543
|
path: routerPathSet.path,
|
|
@@ -1432,7 +1548,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1432
1548
|
}
|
|
1433
1549
|
|
|
1434
1550
|
// Home route
|
|
1435
|
-
@
|
|
1551
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 })
|
|
1436
1552
|
@innerHtmlLight
|
|
1437
1553
|
handleHome(routerPathSet: RouterEventType) {
|
|
1438
1554
|
console.log('[Route Handler] Home');
|
|
@@ -1440,7 +1556,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1440
1556
|
}
|
|
1441
1557
|
|
|
1442
1558
|
// List route
|
|
1443
|
-
@
|
|
1559
|
+
@subscribeSwcAppRouteChange(['/list'], { order: 1 })
|
|
1444
1560
|
@innerHtmlLight
|
|
1445
1561
|
handleList(routerPathSet: RouterEventType) {
|
|
1446
1562
|
console.log('[Route Handler] List');
|
|
@@ -1448,7 +1564,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1448
1564
|
}
|
|
1449
1565
|
|
|
1450
1566
|
// Detail route with dynamic parameter
|
|
1451
|
-
@
|
|
1567
|
+
@subscribeSwcAppRouteChange(['/detail/{productId}'], { order: 2 })
|
|
1452
1568
|
@innerHtmlLight
|
|
1453
1569
|
handleDetail(routerPathSet: RouterEventType) {
|
|
1454
1570
|
const { productId } = routerPathSet.pathData;
|
|
@@ -1457,7 +1573,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1457
1573
|
}
|
|
1458
1574
|
|
|
1459
1575
|
// 404 fallback (order: 999 runs last)
|
|
1460
|
-
@
|
|
1576
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 })
|
|
1461
1577
|
@innerHtmlLight
|
|
1462
1578
|
handle404(routerPathSet: RouterEventType) {
|
|
1463
1579
|
console.log('[Route Handler] 404 Not Found', routerPathSet.path);
|
|
@@ -1470,15 +1586,21 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1470
1586
|
|
|
1471
1587
|
```typescript
|
|
1472
1588
|
interface SwcAppRouteChangeOptions {
|
|
1473
|
-
path?: RoutePathType; //
|
|
1589
|
+
path?: RoutePathType; // string | string[] | (currentThis) => string | string[]
|
|
1474
1590
|
order?: number; // Execution order (default: 0)
|
|
1475
|
-
filter?: (router: Router, meta: {currentThis: any
|
|
1591
|
+
filter?: (router: Router, meta: { currentThis: any; helper: HelperHostSet }) => boolean | Promise<boolean>;
|
|
1592
|
+
before?: (router: Router, meta) => any; // result → @routeChangeBeforeReturn
|
|
1593
|
+
finally?: (router: Router, meta, ctx: { args; result?; error? }) => any;
|
|
1594
|
+
valueKey?: symbol | string; // value used for the stop check
|
|
1595
|
+
trigger?: 'connected' | 'connectedDone'; // replay timing (see "Message Replay")
|
|
1476
1596
|
}
|
|
1477
1597
|
```
|
|
1478
1598
|
|
|
1599
|
+
Forms: `@subscribeSwcAppRouteChange` (bare, all routes), `('/path')`, `(['/a', '/b'])`, `(path, options)`, `(options)`. Alias: `@changedRoute`.
|
|
1600
|
+
|
|
1479
1601
|
**Usage:**
|
|
1480
1602
|
```typescript
|
|
1481
|
-
@
|
|
1603
|
+
@subscribeSwcAppRouteChange(
|
|
1482
1604
|
['/admin/{section}'],
|
|
1483
1605
|
{
|
|
1484
1606
|
order: 5,
|
|
@@ -1492,13 +1614,43 @@ handleAdminSection(routerPathSet: RouterEventType) {
|
|
|
1492
1614
|
}
|
|
1493
1615
|
```
|
|
1494
1616
|
|
|
1617
|
+
#### Combined Example: Route + Attribute (nav highlight)
|
|
1618
|
+
|
|
1619
|
+
Stack several decorators on one method and split the returned object with `valueKey`.
|
|
1620
|
+
**Note:** returning a value stops the later handlers, so an observer-style handler uses `order: -1` and returns `undefined` to keep the chain going.
|
|
1621
|
+
|
|
1622
|
+
```typescript
|
|
1623
|
+
@replaceChildrenLight({ valueKey: 'element' })
|
|
1624
|
+
@subscribeSwcAppRouteChange('/releases')
|
|
1625
|
+
@attribute('nav', 'href', { valueKey: 'href' })
|
|
1626
|
+
handleExplore() {
|
|
1627
|
+
return { element: ExplorePage(w), href: 'releases' }; // highlight nav + render the page
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
1631
|
+
@removeAttribute('nav', 'href')
|
|
1632
|
+
handleNavReset() {
|
|
1633
|
+
return undefined; // reset the highlight, keep the chain going
|
|
1634
|
+
}
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
```css
|
|
1638
|
+
/* Highlight the current section button */
|
|
1639
|
+
nav[href="releases"] [data-href="/releases"] { color: var(--color-text); background: var(--color-bg-alt); }
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1495
1642
|
#### RouterEventType Structure
|
|
1496
1643
|
|
|
1497
1644
|
```typescript
|
|
1498
1645
|
type RouterEventType = {
|
|
1499
|
-
path: string;
|
|
1500
|
-
|
|
1501
|
-
|
|
1646
|
+
path: string; // Current route path
|
|
1647
|
+
url: string;
|
|
1648
|
+
search: string;
|
|
1649
|
+
searchParams: URLSearchParams;
|
|
1650
|
+
data?: any;
|
|
1651
|
+
pathData?: Record<string, any>; // Extracted path parameters (filled per matched pattern)
|
|
1652
|
+
router: Router;
|
|
1653
|
+
triggerPoint: 'start' | 'end' | 'first-end'; // Route change phase
|
|
1502
1654
|
};
|
|
1503
1655
|
```
|
|
1504
1656
|
|
|
@@ -1531,10 +1683,10 @@ import {
|
|
|
1531
1683
|
|
|
1532
1684
|
@elementDefine('multi-decorator-example')
|
|
1533
1685
|
class MultiDecoratorExample extends HTMLElement {
|
|
1534
|
-
@
|
|
1535
|
-
@
|
|
1536
|
-
@updateStyle
|
|
1537
|
-
@updateClass
|
|
1686
|
+
@attribute('selector', 'data-state')
|
|
1687
|
+
@property('selector', 'value')
|
|
1688
|
+
@updateStyle
|
|
1689
|
+
@updateClass
|
|
1538
1690
|
handleUpdate() {
|
|
1539
1691
|
return {
|
|
1540
1692
|
[ATTRIBUTE_METADATA_KEY]: 'attribute-value',
|
|
@@ -1553,10 +1705,10 @@ For more readable code, use the `valueKey` option to specify custom keys:
|
|
|
1553
1705
|
```typescript
|
|
1554
1706
|
@elementDefine('custom-key-example')
|
|
1555
1707
|
class CustomKeyExample extends HTMLElement {
|
|
1556
|
-
@
|
|
1557
|
-
@
|
|
1558
|
-
@updateStyle({ valueKey: 'styleValue' })
|
|
1559
|
-
@updateClass({ valueKey: 'classValue' })
|
|
1708
|
+
@attribute('selector', 'data-state', { valueKey: 'attrValue' })
|
|
1709
|
+
@property('selector', 'value', { valueKey: 'propValue' })
|
|
1710
|
+
@updateStyle('$this', { valueKey: 'styleValue' })
|
|
1711
|
+
@updateClass('$this', { root: 'auto', valueKey: 'classValue' }) // class options need `root`
|
|
1560
1712
|
handleUpdate() {
|
|
1561
1713
|
return {
|
|
1562
1714
|
attrValue: 'attribute-value',
|
|
@@ -1575,9 +1727,9 @@ You can mix both approaches in the same method:
|
|
|
1575
1727
|
```typescript
|
|
1576
1728
|
@elementDefine('mixed-keys-example')
|
|
1577
1729
|
class MixedKeysExample extends HTMLElement {
|
|
1578
|
-
@
|
|
1579
|
-
@
|
|
1580
|
-
@updateStyle({ valueKey: 'styles' }) // Uses custom key
|
|
1730
|
+
@attribute('selector', 'data-state') // Uses default ATTRIBUTE_METADATA_KEY
|
|
1731
|
+
@property('selector', 'value', { valueKey: 'customProp' }) // Uses custom key
|
|
1732
|
+
@updateStyle('$this', { valueKey: 'styles' }) // Uses custom key
|
|
1581
1733
|
handleUpdate() {
|
|
1582
1734
|
return {
|
|
1583
1735
|
[ATTRIBUTE_METADATA_KEY]: 'attr-value',
|
|
@@ -1589,15 +1741,16 @@ class MixedKeysExample extends HTMLElement {
|
|
|
1589
1741
|
```
|
|
1590
1742
|
|
|
1591
1743
|
**Supported Decorators with valueKey:**
|
|
1592
|
-
- `@applyAttribute` / `@
|
|
1593
|
-
- `@applyProperty` / `@
|
|
1594
|
-
- `@applySlot`
|
|
1595
|
-
- `@applyNode` / `@
|
|
1744
|
+
- `@applyAttribute` / `@attribute`
|
|
1745
|
+
- `@applyProperty` / `@property`
|
|
1746
|
+
- `@applySlot(id, { position, valueKey })` (the `clearSlot` / `appendHtmlSlot` / ... shorthands take no options)
|
|
1747
|
+
- `@applyNode` / `@replaceChildren` / `@innerHtml` / etc.
|
|
1596
1748
|
- `@applyStyle` / `@updateStyle` / etc.
|
|
1597
1749
|
- `@applyClass` / `@updateClass` / etc.
|
|
1598
1750
|
- `@emitCustomEvent` / `@emit`
|
|
1599
|
-
- `@publishSwcAppMessage` / `@
|
|
1600
|
-
- `@
|
|
1751
|
+
- `@publishSwcAppMessage` / `@publishMessage`
|
|
1752
|
+
- `@subscribeSwcAppRouteChange` - the extracted value decides whether the chain stops
|
|
1753
|
+
- `@setInterval` / `@setTimeout` / `@requestAnimationFrame` - variant: the extracted value must itself be a function, which becomes the timer/frame callback (see "Timers" above) rather than being applied directly.
|
|
1601
1754
|
|
|
1602
1755
|
Each decorator will use only its corresponding value from the return object, preventing conflicts and allowing clean separation of concerns.
|
|
1603
1756
|
|
|
@@ -1648,7 +1801,7 @@ appElement.connect({
|
|
|
1648
1801
|
|
|
1649
1802
|
### Simplified Decorator API
|
|
1650
1803
|
|
|
1651
|
-
|
|
1804
|
+
Most DOM decorators take an **optional selector** that defaults to `$this` when omitted (or when used bare). Scope shorthands (`*This`, `*AppHost`, `*Light`, `*Shadow`, `*All`) are still provided for brevity.
|
|
1652
1805
|
|
|
1653
1806
|
#### Current Usage Pattern
|
|
1654
1807
|
```typescript
|
|
@@ -1656,7 +1809,7 @@ The decorator API uses a consolidated pattern where all decorators support **opt
|
|
|
1656
1809
|
@applyClass('selector', 'update')
|
|
1657
1810
|
method() { ... }
|
|
1658
1811
|
|
|
1659
|
-
@applyClass('update') //
|
|
1812
|
+
@applyClass('$this', 'update') // a single string is treated as the selector, so name $this explicitly (or use bare @updateClass)
|
|
1660
1813
|
method() { ... }
|
|
1661
1814
|
|
|
1662
1815
|
@applyClass // Bare decorator - selector defaults to $this
|
|
@@ -1666,7 +1819,7 @@ method() { ... }
|
|
|
1666
1819
|
#### Decorator Functions (Current API)
|
|
1667
1820
|
|
|
1668
1821
|
**applyClass.ts:**
|
|
1669
|
-
- `applyClass(selector?, options?)` - Set/update/add/remove/toggle classes
|
|
1822
|
+
- `applyClass(selector?, action?, options?)` - Set/update/add/remove/toggle classes
|
|
1670
1823
|
- `setClass(selector?, options?)` - Replace all classes
|
|
1671
1824
|
- `updateClass(selector?, options?)` - Toggle classes
|
|
1672
1825
|
- `addClass(selector?, options?)` - Add classes
|
|
@@ -1674,52 +1827,51 @@ method() { ... }
|
|
|
1674
1827
|
- `toggleClass(selector?, options?)` - Toggle classes
|
|
1675
1828
|
|
|
1676
1829
|
**applyStyle.ts:**
|
|
1677
|
-
- `applyStyle(selector?, options?)` - Set/update/remove styles
|
|
1830
|
+
- `applyStyle(selector?, action?, options?)` - Set/update/remove styles
|
|
1678
1831
|
- `setStyle(selector?, options?)` - Clear and set styles
|
|
1679
1832
|
- `updateStyle(selector?, options?)` - Update/merge styles
|
|
1680
1833
|
- `removeStyle(selector?, options?)` - Remove specific styles
|
|
1681
1834
|
|
|
1682
1835
|
**applyProperty.ts:**
|
|
1683
|
-
- `property(selector?, options?)` -
|
|
1684
|
-
- `setProperty(selector?, options?)` - Set element properties
|
|
1836
|
+
- `property(selector?, propertyKey?, options?)` - field: property get/set; method: return value → property
|
|
1685
1837
|
|
|
1686
1838
|
**applyAttribute.ts:**
|
|
1687
|
-
- `attribute(selector?, options?)` -
|
|
1688
|
-
- `setAttribute(selector?, options?)` - Set element attributes
|
|
1839
|
+
- `attribute(attrName | selector?, attrName?, options?)` - field: attribute get/set; method: return value → attribute (one string = $this)
|
|
1689
1840
|
|
|
1690
1841
|
**query.ts:**
|
|
1691
|
-
- `query(selector
|
|
1692
|
-
- `queryAll(selector
|
|
1842
|
+
- `query(selector, options?)` - Query single element (supports $this, $host, $appHost, etc.; `pick` option)
|
|
1843
|
+
- `queryAll(selector, options?)` - Query multiple elements (`query` with `pick: 'all'`)
|
|
1693
1844
|
|
|
1694
1845
|
**emitCustomEvent.ts:**
|
|
1695
1846
|
- `emitCustomEvent(target, type, options?)` - Emit custom events
|
|
1696
1847
|
- `emit(target, type, options?)` - Short alias
|
|
1848
|
+
- `emitCustomEvent(type, options?)` - Emit from `$this`
|
|
1697
1849
|
|
|
1698
1850
|
#### Usage Examples
|
|
1699
1851
|
|
|
1700
1852
|
```typescript
|
|
1701
1853
|
@elementDefine('my-component')
|
|
1702
1854
|
class MyComponent extends HTMLElement {
|
|
1703
|
-
// Class management -
|
|
1704
|
-
@updateClass
|
|
1855
|
+
// Class management - bare form targets $this
|
|
1856
|
+
@updateClass
|
|
1705
1857
|
toggleActive() {
|
|
1706
1858
|
return { 'active': this.isActive };
|
|
1707
1859
|
}
|
|
1708
1860
|
|
|
1709
|
-
// Style management -
|
|
1710
|
-
@updateStyle
|
|
1861
|
+
// Style management - bare form targets $this
|
|
1862
|
+
@updateStyle
|
|
1711
1863
|
applyTheme() {
|
|
1712
1864
|
return { color: this.theme.color, fontSize: '16px' };
|
|
1713
1865
|
}
|
|
1714
1866
|
|
|
1715
|
-
// Property binding -
|
|
1716
|
-
@
|
|
1867
|
+
// Property binding - first string is the selector
|
|
1868
|
+
@property('input', 'value')
|
|
1717
1869
|
updateValue() {
|
|
1718
1870
|
return this.computedValue;
|
|
1719
1871
|
}
|
|
1720
1872
|
|
|
1721
|
-
// Attribute binding -
|
|
1722
|
-
@
|
|
1873
|
+
// Attribute binding - one string = attribute on $this
|
|
1874
|
+
@attribute('data-id')
|
|
1723
1875
|
updateId() {
|
|
1724
1876
|
return this.elementId;
|
|
1725
1877
|
}
|
|
@@ -1732,14 +1884,14 @@ class MyComponent extends HTMLElement {
|
|
|
1732
1884
|
listItems?: HTMLLIElement[];
|
|
1733
1885
|
|
|
1734
1886
|
// Emit custom events
|
|
1735
|
-
@
|
|
1887
|
+
@event('button', 'click')
|
|
1736
1888
|
@emitCustomEvent('$this', 'item-selected')
|
|
1737
1889
|
onItemSelect() {
|
|
1738
1890
|
return { itemId: this.selectedId };
|
|
1739
1891
|
}
|
|
1740
1892
|
|
|
1741
|
-
// With selector
|
|
1742
|
-
@
|
|
1893
|
+
// With selector and action
|
|
1894
|
+
@applyClass('.card', 'update')
|
|
1743
1895
|
updateCardClass() {
|
|
1744
1896
|
return { 'highlighted': true };
|
|
1745
1897
|
}
|
|
@@ -1748,7 +1900,7 @@ class MyComponent extends HTMLElement {
|
|
|
1748
1900
|
|
|
1749
1901
|
#### Key Features
|
|
1750
1902
|
|
|
1751
|
-
1. **
|
|
1903
|
+
1. **Optional Selector** - Omit the selector (or use the bare form) to target `$this`
|
|
1752
1904
|
2. **Cleaner Code** - Single function name with multiple overloads
|
|
1753
1905
|
3. **Better Discoverability** - Intuitive parameter patterns
|
|
1754
1906
|
4. **Flexible Usage** - Supports bare decorators, with options, and with selectors
|
|
@@ -1775,17 +1927,16 @@ class UserWidget extends w.HTMLElement { }
|
|
|
1775
1927
|
class UserWidget extends w.HTMLElement { }
|
|
1776
1928
|
```
|
|
1777
1929
|
|
|
1778
|
-
### Factory Always Returns
|
|
1930
|
+
### Factory Always Returns (the tag name)
|
|
1779
1931
|
```typescript
|
|
1780
1932
|
export default (w: Window) => {
|
|
1781
1933
|
const tagName = 'my-element';
|
|
1782
|
-
|
|
1783
|
-
if (existing) return existing;
|
|
1934
|
+
if (w.customElements.get(tagName)) return tagName;
|
|
1784
1935
|
|
|
1785
1936
|
@elementDefine(tagName, { window: w })
|
|
1786
1937
|
class MyElement extends w.HTMLElement { }
|
|
1787
|
-
|
|
1788
|
-
return
|
|
1938
|
+
|
|
1939
|
+
return tagName;
|
|
1789
1940
|
};
|
|
1790
1941
|
```
|
|
1791
1942
|
|
|
@@ -1803,6 +1954,20 @@ constructor(private service: Service) { super(); } // Web Components can't have
|
|
|
1803
1954
|
|
|
1804
1955
|
---
|
|
1805
1956
|
|
|
1957
|
+
## 🧪 Testing
|
|
1958
|
+
|
|
1959
|
+
`pnpm test` type-checks the tests and runs them in a real browser (Chromium via Playwright, falling back to the installed Chrome) — no jsdom. Each test gets its own same-origin iframe window, so custom elements, `history` and `location` never leak between tests.
|
|
1960
|
+
|
|
1961
|
+
```bash
|
|
1962
|
+
pnpm test # type checks + all tests
|
|
1963
|
+
node test/unit/run.mjs query event # only files whose name contains "query" / "event"
|
|
1964
|
+
SWC_TEST_GREP='bare @query' node test/unit/run.mjs # only tests whose name matches
|
|
1965
|
+
SWC_TEST_DEBUG=1 node test/unit/run.mjs route # print the page console
|
|
1966
|
+
SWC_TEST_SERVE=1 node test/unit/run.mjs route # just serve the pages and open them yourself
|
|
1967
|
+
```
|
|
1968
|
+
|
|
1969
|
+
Test files are plain `node:test` / `node:assert` code (`test/unit/*.test.ts`); the runner maps those imports to small in-browser versions. `test/unit/empty-parens.types.ts` is checked by `tsc` only and guards the "no arguments → no parentheses" rule.
|
|
1970
|
+
|
|
1806
1971
|
## 📚 Examples
|
|
1807
1972
|
|
|
1808
1973
|
- **[Commerce (E-Commerce SPA)](https://github.com/dooboostore-develop/packages/tree/main/%40dooboostore/simple-web-component/examples/commerce)** - Full shopping cart example
|
|
@@ -1822,3 +1987,219 @@ constructor(private service: Service) { super(); } // Web Components can't have
|
|
|
1822
1987
|
## 📄 License
|
|
1823
1988
|
[MIT License](https://github.com/dooboostore-develop/packages/tree/main/%40dooboostore/simple-web-component/LICENSE.md)
|
|
1824
1989
|
|
|
1990
|
+
|
|
1991
|
+
---
|
|
1992
|
+
|
|
1993
|
+
---
|
|
1994
|
+
|
|
1995
|
+
## 📨 Message Replay (subject / trigger)
|
|
1996
|
+
|
|
1997
|
+
Late subscribers can receive past messages. The buffer keeps every typed message
|
|
1998
|
+
(`SwcConfigType.messageReplayBufferSize`, default 10, minimum 1 guaranteed).
|
|
1999
|
+
|
|
2000
|
+
```typescript
|
|
2001
|
+
// 'behavior' = last message only, 'replay' = whole buffer in order.
|
|
2002
|
+
// Unset (or 'subject') = live only.
|
|
2003
|
+
@subscribeSwcAppMessage('auth-changed', { subject: 'behavior' })
|
|
2004
|
+
onAuth(@appMessage msg: SwcAppMessage<User | null>) { ... }
|
|
2005
|
+
```
|
|
2006
|
+
|
|
2007
|
+
`trigger` controls when replay happens (default `'connected'`).
|
|
2008
|
+
|
|
2009
|
+
- `'connected'`: replay when the element registers with the app host, i.e. before its own
|
|
2010
|
+
`@onConnectedBody` render. For handlers that don't need DOM targets.
|
|
2011
|
+
- `'connectedDone'`: replay after the element finished rendering (after `@onConnectedCompleted`).
|
|
2012
|
+
Use it when the handler targets DOM rendered by the element itself, like `@innerHtml('.wrap')`
|
|
2013
|
+
(avoids the race where replay arrives before render and gets silently dropped).
|
|
2014
|
+
|
|
2015
|
+
```typescript
|
|
2016
|
+
@subscribeSwcAppMessage('auth-changed', { subject: 'behavior', trigger: 'connectedDone' })
|
|
2017
|
+
@innerHtml('.wrap')
|
|
2018
|
+
async load(@appMessage msg: SwcAppMessage<User | null>) { ... }
|
|
2019
|
+
```
|
|
2020
|
+
|
|
2021
|
+
Route subscribers (`@subscribeSwcAppRouteChange`) accept the same `trigger`.
|
|
2022
|
+
Live broadcasts always reach everyone regardless of trigger.
|
|
2023
|
+
|
|
2024
|
+
## 🔌 App Host Hooks (onConnected / onSwcAppConnected / onDisconnected)
|
|
2025
|
+
|
|
2026
|
+
The three host hooks of `SwcAppMixin` are **abstract**: a subclass must implement them (use an empty method if there is nothing to do).
|
|
2027
|
+
`@inject` parameters are resolved via DI (host element itself included).
|
|
2028
|
+
|
|
2029
|
+
- `onConnected()` - at connectedCallback (every time it is attached). It runs one async step after attaching, so its order relative to `connect()` depends on when you call `connect()`:
|
|
2030
|
+
- `connect()` right after attaching → `connect()` → `onSwcAppConnected()` → `onConnected()` (DI already exists)
|
|
2031
|
+
- `connect()` later → `onConnected()` (no DI yet) → `connect()` → `onSwcAppConnected()`
|
|
2032
|
+
|
|
2033
|
+
So don't rely on DI or on ordering here — keep it to DOM/attribute work and subscriptions; call services in `onSwcAppConnected()`
|
|
2034
|
+
- `onSwcAppConnected()` - after `connect()` completes (DI container and router are ready). **Call services here**
|
|
2035
|
+
- `onDisconnected()` - at disconnectedCallback. Release subscriptions created in `onConnected`
|
|
2036
|
+
|
|
2037
|
+
```typescript
|
|
2038
|
+
class MyAppBody extends SwcAppMixin(w.HTMLBodyElement) {
|
|
2039
|
+
onConnected() {}
|
|
2040
|
+
async onSwcAppConnected(@inject(AuthService.SYMBOL) auth: AuthService) {
|
|
2041
|
+
this.publishMessage({ type: 'auth-changed', data: await auth.me().catch(() => null) });
|
|
2042
|
+
}
|
|
2043
|
+
onDisconnected() {}
|
|
2044
|
+
}
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
### observeMessage — the message bus as an Observable
|
|
2048
|
+
|
|
2049
|
+
```typescript
|
|
2050
|
+
host.observeMessage() // all types, live only
|
|
2051
|
+
host.observeMessage('auth-changed') // one type
|
|
2052
|
+
host.observeMessage('auth-changed', { subject: 'behavior' }) // type + replay of the last value
|
|
2053
|
+
host.observeMessage({ type: 'auth-changed', subject: 'replay' }) // options-object form
|
|
2054
|
+
```
|
|
2055
|
+
|
|
2056
|
+
- `subject`: `'behavior'` (last one) / `'replay'` (whole buffer) is delivered first at subscribe time, then live messages.
|
|
2057
|
+
- Replay only works when `type` is given; options without a type receive live messages only.
|
|
2058
|
+
- Returns an `Observable`; `unsubscribe()` the `Subscription` yourself (usually in `onDisconnected`).
|
|
2059
|
+
|
|
2060
|
+
When publishing from the host itself, call `this.publishMessage` directly.
|
|
2061
|
+
The `@publishSwcAppMessage` decorator publishes through the parent host, and the
|
|
2062
|
+
host itself has no parent, so the message would be dropped.
|
|
2063
|
+
|
|
2064
|
+
Use `connectedElements()` to get the currently connected child elements.
|
|
2065
|
+
|
|
2066
|
+
## 🌐 @fetch — Declarative HTTP
|
|
2067
|
+
|
|
2068
|
+
```typescript
|
|
2069
|
+
// bare form: GET by default
|
|
2070
|
+
@fetch('https://api.example.com/post/1')
|
|
2071
|
+
async load(@fetchSettled settled: PromiseSettledResult<any>) {
|
|
2072
|
+
return settled;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// before (default): build request → fetch → inject settled → run method.
|
|
2076
|
+
// Returns the method result.
|
|
2077
|
+
@fetch({ url: '/api/post/1', request: 'GET' })
|
|
2078
|
+
async loadBefore(@fetchSettled settled: PromiseSettledResult<any>) {
|
|
2079
|
+
return settled.status === 'fulfilled' ? settled.value : null;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// after: run method → use its return value as the body → fetch. Returns the fetch result.
|
|
2083
|
+
@fetch({ url: '/api/posts', trigger: 'after', process: 'json' })
|
|
2084
|
+
async save() {
|
|
2085
|
+
return { title: 'hello' };
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// after + valueKey: use returnValue[valueKey] as the body.
|
|
2089
|
+
@fetch({ url: '/api/echo', trigger: 'after', valueKey: 'form', process: 'form' })
|
|
2090
|
+
async saveForm() {
|
|
2091
|
+
return { form: { title: 'hi' } };
|
|
2092
|
+
}
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
**Modes**
|
|
2096
|
+
- **before** (default, or `trigger: 'before'`): build request → fetch → inject the settled result via
|
|
2097
|
+
`@fetchSettled` → run the method. Returns the method result. A failed fetch does not
|
|
2098
|
+
throw — it arrives as `{ status: 'rejected', reason }`.
|
|
2099
|
+
- **after** (`trigger: 'after'`, or just `valueKey`): run the method → its return value
|
|
2100
|
+
(or `returnValue[valueKey]`) becomes the body → fetch. Returns the fetch result; a failed fetch throws.
|
|
2101
|
+
`valueKey` is after-only — combining it with `trigger: 'before'` is a type error.
|
|
2102
|
+
|
|
2103
|
+
**Declarative vs manual** — pick one:
|
|
2104
|
+
- **Declarative**: describe the request with `url` / `request` / `process` and the framework fetches.
|
|
2105
|
+
- **Manual**: give `manual` and make the request yourself (a service call, your own `fetch`, ...).
|
|
2106
|
+
`url` / `request` / `process` are unused then, so combining them with `manual` is a type error.
|
|
2107
|
+
|
|
2108
|
+
```typescript
|
|
2109
|
+
// service call — only the result matters
|
|
2110
|
+
@fetch({ manual: (self) => self.myService.getProfile({}) })
|
|
2111
|
+
async load(@fetchSettled settled?: PromiseSettledResult<MyService.ProfileResponse>) { ... }
|
|
2112
|
+
|
|
2113
|
+
// your own fetch — pass the signal so an abort cancels the network request too
|
|
2114
|
+
@fetch({ trigger: 'after', manual: (_self, _helper, _params, signal, returnValue) =>
|
|
2115
|
+
window.fetch('/api/save', { method: 'POST', body: JSON.stringify(returnValue), signal }).then(r => r.json()) })
|
|
2116
|
+
async save() { return { title: 'hi' }; }
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
`manual(this, helper, params, signal, returnValue?)` — `signal` aborts on disconnect / `abortPrevious`;
|
|
2120
|
+
`returnValue` is the method's return value (or `returnValue[valueKey]`) in after mode, `undefined` in before mode.
|
|
2121
|
+
Its return value is the fetch result.
|
|
2122
|
+
|
|
2123
|
+
**Options (declarative)**
|
|
2124
|
+
- `url`: a string/`URL`/`Request`, or a factory `(this, helper, params) => url`.
|
|
2125
|
+
- `request`: an HTTP method string (`'GET'`/`'POST'`/...) or a factory returning `RequestInit` —
|
|
2126
|
+
`(this, helper, params) => RequestInit` in before mode, `(this, helper, params, returnValue) => RequestInit`
|
|
2127
|
+
in after mode. Absent = GET, or POST when there is a body.
|
|
2128
|
+
- `process`: body encoding for plain values — `'json'` / `'form'` (multipart) / `'urlencoded'` / `'text'`.
|
|
2129
|
+
Values that already are a `BodyInit` (string, `Blob`, `ArrayBuffer`, `FormData`, `URLSearchParams`,
|
|
2130
|
+
`ReadableStream`) are sent as-is. Unset = pass through untouched.
|
|
2131
|
+
- Headers: `request` may return `headers` as an object, array or `Headers` instance. A `Content-Type`
|
|
2132
|
+
you set wins over the one `process` would add.
|
|
2133
|
+
- The request uses the element's window `fetch` (the per-request window under SSR), falling back to `globalThis.fetch`.
|
|
2134
|
+
- Default response handling: empty body (e.g. 204) → `undefined`, JSON content types (`application/json`,
|
|
2135
|
+
`*+json`) → parsed, otherwise text. A non-OK status throws `HttpResponseError` from `@dooboostore/core`
|
|
2136
|
+
with `response` (status, headers) and the parsed `body`:
|
|
2137
|
+
|
|
2138
|
+
```typescript
|
|
2139
|
+
@fetch('/api/post/1')
|
|
2140
|
+
async load(@fetchSettled settled?: PromiseSettledResult<any>) {
|
|
2141
|
+
if (settled?.status === 'rejected' && isHttpResponseError(settled.reason)) {
|
|
2142
|
+
return settled.reason.response?.status; // e.g. 404
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
```
|
|
2146
|
+
|
|
2147
|
+
**Hooks** (same order as every other SWC decorator: `filter → before → handler → finally`)
|
|
2148
|
+
- `filter(this, helper, params)` → `false` skips the whole call (no method, no fetch; returns `undefined`).
|
|
2149
|
+
- `before(this, helper, params)` runs first after `filter` and is awaited; its return value is ignored.
|
|
2150
|
+
- `after(this, helper, params, settled)` runs right after the fetch settles (fulfilled or rejected).
|
|
2151
|
+
- `finally(this, helper, params, { args, result, error })` always runs around the whole call.
|
|
2152
|
+
`result` is what the call returns (the method result in before mode, the fetch result in after mode).
|
|
2153
|
+
|
|
2154
|
+
**Cancellation**
|
|
2155
|
+
- Every call gets its own `AbortController`. Declarative requests get its signal as `init.signal`
|
|
2156
|
+
(merged with a `signal` you return from `request`, via `AbortSignal.any` or a fallback); `manual` gets it as
|
|
2157
|
+
the `signal` argument.
|
|
2158
|
+
- When the element disconnects, all its in-flight `@fetch` requests are aborted. Such a call ends
|
|
2159
|
+
quietly with `undefined`: the method (before mode) and the `after` hook are skipped so nothing touches
|
|
2160
|
+
a detached DOM; `finally` still runs with `ctx.error` set to the `AbortError`.
|
|
2161
|
+
- A call that *starts* while the element is detached (e.g. a `setTimeout` scheduled just before the user
|
|
2162
|
+
navigated away) is skipped the same quiet way. Elements that were never connected are not affected.
|
|
2163
|
+
- `manual` should pass `signal` on to its own client (`fetch`, axios, ...) so the network
|
|
2164
|
+
request is actually cancelled. If it ignores the signal, the call still ends as soon as it's aborted
|
|
2165
|
+
(the result is raced against the abort) — the request just keeps running in the background and its
|
|
2166
|
+
late result is discarded.
|
|
2167
|
+
- `abortPrevious: true` aborts the previous in-flight call of the same method when it's called again
|
|
2168
|
+
(search-as-you-type) — the superseded call ends quietly the same way, so a late old response can't
|
|
2169
|
+
overwrite the newest result.
|
|
2170
|
+
- Aborting through your own signal is not quiet: it surfaces as a rejected settled result (before mode)
|
|
2171
|
+
or a thrown error (after mode), like any other failure.
|
|
2172
|
+
|
|
2173
|
+
**Aliases**
|
|
2174
|
+
|
|
2175
|
+
| Alias | Same as |
|
|
2176
|
+
|---|---|
|
|
2177
|
+
| `@fetchManual(fn, options?)` | `@fetch({ ...options, manual: fn })` |
|
|
2178
|
+
| `@fetchGet(url, options?)` / `@fetchDelete(url, options?)` | before mode, `method` fixed to GET / DELETE |
|
|
2179
|
+
| `@fetchPost(url, options?)` / `@fetchPut(...)` / `@fetchPatch(...)` | after mode (the method's return value is the body), `method` fixed |
|
|
2180
|
+
| `@fetchLatest(url \| options)` | `@fetch({ ...options, abortPrevious: true })` |
|
|
2181
|
+
| `@fetchBefore(options)` / `@fetchAfter(options)` | `@fetch({ ...options, trigger })` |
|
|
2182
|
+
|
|
2183
|
+
For the method aliases, `request` may only be a factory returning `RequestInit` (e.g. headers) — its `method` is overwritten.
|
|
2184
|
+
|
|
2185
|
+
```typescript
|
|
2186
|
+
@fetchManual((self) => self.myService.getProfile({}))
|
|
2187
|
+
async load(@fetchSettled settled?: PromiseSettledResult<Profile>) { ... }
|
|
2188
|
+
|
|
2189
|
+
@fetchPost('/api/posts', { process: 'json' })
|
|
2190
|
+
async save() { return { title: 'hi' }; }
|
|
2191
|
+
|
|
2192
|
+
@fetchDelete((_self, _helper, params) => `/api/posts/${params[0]}`)
|
|
2193
|
+
async remove(id: number, @fetchSettled settled?: PromiseSettledResult<unknown>) { ... }
|
|
2194
|
+
```
|
|
2195
|
+
|
|
2196
|
+
**Composition** — e.g. form submit:
|
|
2197
|
+
`@event('form', 'submit', { before: (e) => new FormData(e.target), preventDefault: true })`
|
|
2198
|
+
stacked on `@fetch({ url: '/api/echo', trigger: 'after', process: 'form' })`, with the method taking
|
|
2199
|
+
`@eventBeforeReturn formData: FormData` and returning it.
|
|
2200
|
+
|
|
2201
|
+
## 💧 @property Hydration Rules
|
|
2202
|
+
|
|
2203
|
+
- Attach bare `@property` (no args) to a field to mark it as an SSR hydration target.
|
|
2204
|
+
- Self fields (`$this` + matching key) stay plain fields with no getter/setter.
|
|
2205
|
+
- Declare the field with `declare` (both `= null` and `;` initializers get overwritten on upgrade).
|