@dooboostore/simple-web-component 1.0.49 → 1.0.51
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 +790 -337
- package/dist/cjs/SwcAppEngine.js.map +2 -2
- package/dist/cjs/decorators/addEventListener.js +1925 -22
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/applyAttribute.js +41 -47
- package/dist/cjs/decorators/applyAttribute.js.map +2 -2
- package/dist/cjs/decorators/applyClass.js +14 -11
- package/dist/cjs/decorators/applyClass.js.map +2 -2
- package/dist/cjs/decorators/applyNode.js +19 -21
- package/dist/cjs/decorators/applyNode.js.map +2 -2
- package/dist/cjs/decorators/applyProperty.js +24 -48
- package/dist/cjs/decorators/applyProperty.js.map +2 -2
- package/dist/cjs/decorators/applySlot.js +9 -8
- package/dist/cjs/decorators/applySlot.js.map +2 -2
- package/dist/cjs/decorators/applyStyle.js +14 -11
- 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 +61 -24
- package/dist/cjs/decorators/changedAttribute.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +240 -196
- 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 +112 -0
- package/dist/cjs/decorators/eventMedia.js.map +7 -0
- package/dist/cjs/decorators/fetch.js +278 -0
- package/dist/cjs/decorators/fetch.js.map +7 -0
- package/dist/cjs/decorators/index.js +8 -2
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/intersectionObserver.js +76 -65
- 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 +97 -90
- package/dist/cjs/decorators/mutationObserver.js.map +3 -3
- package/dist/cjs/decorators/parameter.js +156 -0
- package/dist/cjs/decorators/parameter.js.map +7 -0
- 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 +115 -0
- package/dist/cjs/decorators/requestAnimationFrame.js.map +7 -0
- package/dist/cjs/decorators/resizeObserver.js +84 -69
- package/dist/cjs/decorators/resizeObserver.js.map +3 -3
- package/dist/cjs/decorators/setInterval.js +65 -22
- package/dist/cjs/decorators/setInterval.js.map +2 -2
- package/dist/cjs/decorators/setTimeout.js +68 -22
- 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 +256 -67
- 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/types/window.d.js.map +1 -1
- 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 +1925 -22
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/applyAttribute.js +41 -47
- package/dist/esm/decorators/applyAttribute.js.map +2 -2
- package/dist/esm/decorators/applyClass.js +14 -11
- package/dist/esm/decorators/applyClass.js.map +2 -2
- package/dist/esm/decorators/applyNode.js +20 -22
- package/dist/esm/decorators/applyNode.js.map +2 -2
- package/dist/esm/decorators/applyProperty.js +24 -48
- package/dist/esm/decorators/applyProperty.js.map +2 -2
- package/dist/esm/decorators/applySlot.js +9 -8
- package/dist/esm/decorators/applySlot.js.map +2 -2
- package/dist/esm/decorators/applyStyle.js +14 -11
- 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 +61 -24
- package/dist/esm/decorators/changedAttribute.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +238 -194
- 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 +94 -0
- package/dist/esm/decorators/eventMedia.js.map +7 -0
- package/dist/esm/decorators/fetch.js +259 -0
- package/dist/esm/decorators/fetch.js.map +7 -0
- package/dist/esm/decorators/index.js +8 -2
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/intersectionObserver.js +76 -65
- 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 +97 -90
- package/dist/esm/decorators/mutationObserver.js.map +3 -3
- package/dist/esm/decorators/parameter.js +137 -0
- package/dist/esm/decorators/parameter.js.map +7 -0
- 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 +96 -0
- package/dist/esm/decorators/requestAnimationFrame.js.map +7 -0
- package/dist/esm/decorators/resizeObserver.js +84 -69
- package/dist/esm/decorators/resizeObserver.js.map +2 -2
- package/dist/esm/decorators/setInterval.js +65 -23
- package/dist/esm/decorators/setInterval.js.map +2 -2
- package/dist/esm/decorators/setTimeout.js +68 -23
- 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 +258 -69
- 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 +4395 -1266
- 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 +936 -11
- package/dist/types/decorators/addEventListener.d.ts.map +1 -1
- package/dist/types/decorators/applyAttribute.d.ts +27 -37
- package/dist/types/decorators/applyAttribute.d.ts.map +1 -1
- package/dist/types/decorators/applyClass.d.ts +2 -2
- package/dist/types/decorators/applyClass.d.ts.map +1 -1
- package/dist/types/decorators/applyNode.d.ts +16 -16
- package/dist/types/decorators/applyNode.d.ts.map +1 -1
- package/dist/types/decorators/applyProperty.d.ts +28 -44
- package/dist/types/decorators/applyProperty.d.ts.map +1 -1
- package/dist/types/decorators/applySlot.d.ts.map +1 -1
- package/dist/types/decorators/applyStyle.d.ts +2 -2
- 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 +22 -4
- 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 +48 -0
- package/dist/types/decorators/eventMedia.d.ts.map +1 -0
- 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 +8 -2
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/intersectionObserver.d.ts +32 -10
- 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 +34 -12
- package/dist/types/decorators/mutationObserver.d.ts.map +1 -1
- package/dist/types/decorators/parameter.d.ts +113 -0
- package/dist/types/decorators/parameter.d.ts.map +1 -0
- 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 +47 -0
- package/dist/types/decorators/requestAnimationFrame.d.ts.map +1 -0
- package/dist/types/decorators/resizeObserver.d.ts +34 -12
- package/dist/types/decorators/resizeObserver.d.ts.map +1 -1
- package/dist/types/decorators/setInterval.d.ts +27 -5
- package/dist/types/decorators/setInterval.d.ts.map +1 -1
- package/dist/types/decorators/setTimeout.d.ts +27 -5
- 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 +4395 -1266
- 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 +1086 -28
- package/src/decorators/applyAttribute.ts +82 -134
- package/src/decorators/applyClass.ts +14 -10
- package/src/decorators/applyNode.ts +39 -37
- package/src/decorators/applyProperty.ts +72 -100
- package/src/decorators/applySlot.ts +9 -8
- package/src/decorators/applyStyle.ts +14 -10
- package/src/decorators/around.ts +196 -0
- package/src/decorators/changedAttribute.ts +65 -26
- package/src/decorators/elementDefine.ts +298 -220
- package/src/decorators/emitCustomEvent.ts +17 -13
- package/src/decorators/eventMedia.ts +119 -0
- package/src/decorators/fetch.ts +454 -0
- package/src/decorators/index.ts +8 -2
- package/src/decorators/intersectionObserver.ts +108 -78
- package/src/decorators/lifecycles.ts +24 -22
- package/src/decorators/mutationObserver.ts +140 -104
- package/src/decorators/parameter.ts +212 -0
- package/src/decorators/publishSwcAppMessage.ts +3 -4
- package/src/decorators/query.ts +22 -14
- package/src/decorators/requestAnimationFrame.ts +149 -0
- package/src/decorators/resizeObserver.ts +119 -82
- package/src/decorators/setInterval.ts +90 -23
- package/src/decorators/setTimeout.ts +93 -23
- 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 +329 -85
- package/src/types/index.ts +38 -5
- package/src/types/window.d.ts +2 -18
- 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/cjs/examples/RxJSOperatorsExample.js +0 -285
- package/dist/cjs/examples/RxJSOperatorsExample.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/esm/examples/RxJSOperatorsExample.js +0 -266
- package/dist/esm/examples/RxJSOperatorsExample.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/dist/types/examples/RxJSOperatorsExample.d.ts +0 -94
- package/dist/types/examples/RxJSOperatorsExample.d.ts.map +0 -1
- package/src/decorators/subscribeSwcAppMessageWhileConnected.ts +0 -91
- package/src/decorators/subscribeSwcAppRouteChangeWhileConnected.ts +0 -107
- package/src/examples/RxJSOperatorsExample.ts +0 -279
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,25 +314,112 @@ 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
|
-
- `
|
|
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, ...):**
|
|
342
|
+
|
|
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:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
@eventClick('.logo')
|
|
347
|
+
onLogoClick() { ... }
|
|
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
|
+
|
|
357
|
+
// Same as: @eventDelegateLight('.item', 'click')
|
|
358
|
+
@eventClickDelegateLight('.item')
|
|
359
|
+
onItemClick(event: Event) { ... }
|
|
360
|
+
```
|
|
361
|
+
|
|
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`.
|
|
365
|
+
|
|
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`).
|
|
367
|
+
|
|
368
|
+
These aliases are generated in `addEventListener.ts` from a small internal `makeTypedEventAliases<TEvent>(type)` factory — adding a new event type is a one-line addition, not hand-written boilerplate. Any event type not covered still works via the generic `@event(selector, 'your-type', options)` / `@eventDelegateLight(selector, 'your-type', options)` etc.
|
|
369
|
+
|
|
370
|
+
#### Order-Independent Parameter Decorators (@eventObject, @matchedElement, @hostSet, @helperHostSet, @helperSet)
|
|
371
|
+
|
|
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):
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { event, eventObject, matchedElement, hostSet, helperHostSet, helperSet } from '@dooboostore/simple-web-component';
|
|
376
|
+
|
|
377
|
+
@elementDefine('product-list')
|
|
378
|
+
class ProductList extends HTMLElement {
|
|
379
|
+
// Order can be anything you like
|
|
380
|
+
@eventDelegateLight('.item', 'click')
|
|
381
|
+
onItemClick(@matchedElement $item: Element, @eventObject event: Event) {
|
|
382
|
+
console.log('clicked item:', $item, event);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Only need the host tree info? Just ask for @hostSet.
|
|
386
|
+
@eventClick
|
|
387
|
+
onHostClick(@hostSet hs: HostSet) {
|
|
388
|
+
console.log(hs.$appHost, hs.$hosts);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Need DOM helpers ($q, $qa, ...) too? Use @helperHostSet (superset of @hostSet) or @helperSet (helpers only).
|
|
392
|
+
@eventClick
|
|
393
|
+
onHostClickFull(@helperHostSet full: HelperHostSet, @helperSet helpers: HelperSet) { ... }
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- `@eventObject` - the original DOM `Event`
|
|
398
|
+
- `@matchedElement` - the delegate-matched element (or `event.currentTarget` for direct bindings)
|
|
399
|
+
- `@hostSet` - host-ancestor-tree info only (`$host`, `$parentHost`, `$hosts`, `$appHost`, `$appHosts`, `$firstHost`, `$lastHost`, `$firstAppHost`, `$lastAppHost`)
|
|
400
|
+
- `@helperHostSet` - `@hostSet` + DOM/window helpers (`$d`, `$w`, `$q`, `$qa`, `$qi`) + `$this`
|
|
401
|
+
- `@helperSet` - DOM/window helpers only (`$d`, `$w`, `$q`, `$qa`, `$qi`), no host-tree info
|
|
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
|
+
|
|
405
|
+
The same mechanism (and the same 5 decorators, plus two more) also applies to:
|
|
406
|
+
- **`@onInitialize`/`@onConnectedBefore`/`@onConnectedAfter`/... lifecycle methods** — can freely mix `@hostSet`/`@helperHostSet`/`@helperSet` with `@inject(...)` on the same method.
|
|
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).
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
@onConnectedAfter
|
|
415
|
+
onconstructor(@inject(UserService.SYMBOL) userService: UserService, @hostSet hs: HostSet) { ... }
|
|
416
|
+
|
|
417
|
+
@subscribeSwcAppRouteChange
|
|
418
|
+
onRouteChanged(@helperSet helpers: HelperSet, @swcAppRouterEvent re: RouterEventType) { ... }
|
|
419
|
+
|
|
420
|
+
@subscribeSwcAppMessage
|
|
421
|
+
onMessage(@hostSet hs: HostSet, @appMessage msg: SwcAppMessage) { ... }
|
|
422
|
+
```
|
|
318
423
|
|
|
319
424
|
#### @emitCustomEvent
|
|
320
425
|
Emit custom events with data.
|
|
@@ -330,13 +435,16 @@ async onLogin() {
|
|
|
330
435
|
**@emitCustomEvent Decorator Variants:**
|
|
331
436
|
- `emitCustomEvent(target, type, options)` - Full form
|
|
332
437
|
- `emit(target, type, options)` - Short alias
|
|
333
|
-
- `
|
|
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
|
|
334
441
|
|
|
335
442
|
#### @publishSwcAppMessage
|
|
336
443
|
Publish messages through the message bus when method completes.
|
|
337
444
|
|
|
338
445
|
```typescript
|
|
339
|
-
|
|
446
|
+
// No arguments needed → bare form
|
|
447
|
+
@publishSwcAppMessage
|
|
340
448
|
async onLogin() {
|
|
341
449
|
const user = await this.authService.login();
|
|
342
450
|
return user; // Published as message with data: user
|
|
@@ -350,13 +458,13 @@ updateProfile() {
|
|
|
350
458
|
```
|
|
351
459
|
|
|
352
460
|
**@publishSwcAppMessage Decorator Variants:**
|
|
353
|
-
- `publishSwcAppMessage
|
|
461
|
+
- `publishSwcAppMessage` - Bare decorator, publish without message type
|
|
354
462
|
- `publishSwcAppMessage(messageType)` - Publish with specific message type
|
|
355
463
|
- `publishSwcAppMessage(messageType, { valueKey: 'customKey' })` - Publish with custom value extraction
|
|
356
464
|
- `publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' })` - Publish with options object
|
|
357
|
-
- `
|
|
358
|
-
- `
|
|
359
|
-
- `
|
|
465
|
+
- `publishMessage` - Short alias
|
|
466
|
+
- `publishMessage(messageType)` - Short alias with message type
|
|
467
|
+
- `publishMessage(messageType, options)` - Short alias with options
|
|
360
468
|
|
|
361
469
|
**Using valueKey for Multiple Decorators:**
|
|
362
470
|
|
|
@@ -371,38 +479,40 @@ handleMultipleEvents() {
|
|
|
371
479
|
}
|
|
372
480
|
```
|
|
373
481
|
|
|
374
|
-
####
|
|
375
|
-
|
|
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`).
|
|
376
485
|
|
|
377
486
|
```typescript
|
|
378
|
-
@
|
|
487
|
+
@emitCustomEvent('navigate', { attributeName: 'on-emit-navigate' })
|
|
379
488
|
onNavClick(e: any) {
|
|
380
489
|
return { path: e.target.dataset.path };
|
|
381
490
|
}
|
|
491
|
+
// Parent: <app-header on-emit-navigate="$host.onHeaderNavigate(event, $data)"></app-header>
|
|
382
492
|
```
|
|
383
493
|
|
|
384
|
-
####
|
|
385
|
-
|
|
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.
|
|
386
496
|
|
|
387
497
|
```typescript
|
|
388
|
-
@
|
|
498
|
+
@event('click')
|
|
389
499
|
onHostClick(event: Event) {
|
|
390
500
|
console.log('Host element clicked');
|
|
391
501
|
}
|
|
392
502
|
```
|
|
393
503
|
|
|
394
|
-
#### @
|
|
504
|
+
#### @eventAppHost
|
|
395
505
|
Listen to events on the app root host element (`$appHost` selector). Enables selective event handling with filters.
|
|
396
506
|
|
|
397
507
|
```typescript
|
|
398
508
|
// Listen to all user-action events from $appHost
|
|
399
|
-
@
|
|
509
|
+
@eventAppHost('user-action')
|
|
400
510
|
onUserAction(e: CustomEvent) {
|
|
401
511
|
console.log('User action:', e.detail);
|
|
402
512
|
}
|
|
403
513
|
|
|
404
514
|
// Filter specific events - loose coupling pattern
|
|
405
|
-
@
|
|
515
|
+
@eventAppHost('user-action', {
|
|
406
516
|
filter: (event, helper) => event.detail?.type === 'login'
|
|
407
517
|
})
|
|
408
518
|
onUserLogin(e: CustomEvent) {
|
|
@@ -410,7 +520,7 @@ onUserLogin(e: CustomEvent) {
|
|
|
410
520
|
}
|
|
411
521
|
|
|
412
522
|
// Different component filtering same event differently
|
|
413
|
-
@
|
|
523
|
+
@eventAppHost('user-action', {
|
|
414
524
|
filter: (event, helper) => event.detail?.type === 'logout'
|
|
415
525
|
})
|
|
416
526
|
onUserLogout(e: CustomEvent) {
|
|
@@ -421,7 +531,7 @@ onUserLogout(e: CustomEvent) {
|
|
|
421
531
|
### 5. **DOM Querying**
|
|
422
532
|
|
|
423
533
|
#### @query
|
|
424
|
-
Query single element
|
|
534
|
+
Query a single element. The default `root: 'auto'` searches the shadow root if one exists, otherwise the light DOM.
|
|
425
535
|
|
|
426
536
|
```typescript
|
|
427
537
|
@query('#form-input')
|
|
@@ -442,91 +552,86 @@ textInputs?: HTMLInputElement[];
|
|
|
442
552
|
listItems?: HTMLLIElement[];
|
|
443
553
|
```
|
|
444
554
|
|
|
445
|
-
####
|
|
446
|
-
|
|
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).
|
|
447
557
|
|
|
448
558
|
```typescript
|
|
449
|
-
@
|
|
450
|
-
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
|
|
451
561
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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[];
|
|
455
567
|
```
|
|
456
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
|
+
|
|
457
573
|
### 6. **Attribute Binding**
|
|
458
574
|
|
|
459
|
-
#### @attribute (Field Decorator)
|
|
460
|
-
|
|
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
|
|
461
579
|
|
|
462
|
-
|
|
580
|
+
**One string = attribute name on `$this`**, **two strings = (selector, attribute name)**.
|
|
463
581
|
|
|
464
582
|
```typescript
|
|
465
583
|
@elementDefine('product-card')
|
|
466
584
|
class ProductCard extends HTMLElement {
|
|
467
|
-
//
|
|
585
|
+
// Field — product-id on $this
|
|
468
586
|
@attribute('product-id')
|
|
469
587
|
productId: string;
|
|
470
588
|
|
|
471
|
-
//
|
|
589
|
+
// Field — data-id on the selector target
|
|
472
590
|
@attribute('#user', 'data-id')
|
|
473
591
|
userId: string;
|
|
474
592
|
|
|
475
|
-
//
|
|
593
|
+
// Field — bare (field name = attribute name)
|
|
476
594
|
@attribute
|
|
477
595
|
title: string;
|
|
478
596
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
597
|
+
// Method — return value goes to nav[data-active] (null → removed)
|
|
598
|
+
@attribute('nav', 'data-active')
|
|
599
|
+
updateNav() {
|
|
600
|
+
return this.section; // 'releases' | null
|
|
484
601
|
}
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
602
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
```typescript
|
|
492
|
-
@elementDefine('status-updater')
|
|
493
|
-
class StatusUpdater extends HTMLElement {
|
|
494
|
-
// Set attribute on $this
|
|
495
|
-
@setAttribute('data-status')
|
|
496
|
-
updateStatus() {
|
|
497
|
-
return this.isActive ? 'active' : 'inactive';
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Set attribute on selector
|
|
501
|
-
@setAttribute('#user', 'data-name')
|
|
603
|
+
// Method — data-name on $this
|
|
604
|
+
@attribute('data-name')
|
|
502
605
|
setUserName() {
|
|
503
606
|
return this.userName;
|
|
504
607
|
}
|
|
505
|
-
|
|
506
|
-
// Bare decorator (uses method name as attribute)
|
|
507
|
-
@setAttribute
|
|
508
|
-
setValue() {
|
|
509
|
-
return this.computedValue;
|
|
510
|
-
}
|
|
511
608
|
}
|
|
512
609
|
```
|
|
513
610
|
|
|
514
|
-
**
|
|
515
|
-
- `@attribute
|
|
516
|
-
- `@attribute('
|
|
517
|
-
- `@attribute('attr-name', options)` -
|
|
518
|
-
- `@attribute(
|
|
519
|
-
- `@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")
|
|
520
619
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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).
|
|
623
|
+
|
|
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
|
+
```
|
|
527
632
|
|
|
528
633
|
#### @changedAttribute (Method Decorator)
|
|
529
|
-
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).
|
|
530
635
|
|
|
531
636
|
```typescript
|
|
532
637
|
@elementDefine('reactive-component')
|
|
@@ -542,80 +647,59 @@ class ReactiveComponent extends HTMLElement {
|
|
|
542
647
|
console.log(`Status is now: ${newValue}`);
|
|
543
648
|
}
|
|
544
649
|
|
|
545
|
-
//
|
|
546
|
-
@changedAttribute
|
|
547
|
-
|
|
548
|
-
console.log('An attribute changed');
|
|
549
|
-
}
|
|
650
|
+
// Bare (or no name) → the method name is used as the attribute name
|
|
651
|
+
@changedAttribute
|
|
652
|
+
status(newValue: string) { }
|
|
550
653
|
}
|
|
551
654
|
```
|
|
552
655
|
|
|
553
656
|
**@changedAttribute Options:**
|
|
554
657
|
- `attributeName` - Attribute name to listen for (optional, defaults to method name)
|
|
555
658
|
- `type` - Type converter: `Number`, `Boolean`, or `String`
|
|
556
|
-
- `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`)
|
|
557
661
|
|
|
558
662
|
### 6.5 **Property Binding**
|
|
559
663
|
|
|
560
|
-
#### @property (Field Decorator)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
class InputWrapper extends HTMLElement {
|
|
566
|
-
// Property on $this (auto-detected)
|
|
567
|
-
@property('value')
|
|
568
|
-
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)
|
|
569
669
|
|
|
570
|
-
|
|
571
|
-
@property('#submit-btn', 'disabled')
|
|
572
|
-
isSubmitDisabled: boolean;
|
|
573
|
-
|
|
574
|
-
// Bare decorator (uses field name as property)
|
|
575
|
-
@property
|
|
576
|
-
checked: boolean;
|
|
577
|
-
}
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
#### @setProperty (Method Decorator)
|
|
581
|
-
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.
|
|
582
671
|
|
|
583
672
|
```typescript
|
|
584
673
|
@elementDefine('form-handler')
|
|
585
674
|
class FormHandler extends HTMLElement {
|
|
586
|
-
//
|
|
587
|
-
@
|
|
588
|
-
|
|
589
|
-
return !this.isValid();
|
|
590
|
-
}
|
|
675
|
+
// Field — bare: own field (SSR hydration target)
|
|
676
|
+
@property
|
|
677
|
+
declare rows: Row[];
|
|
591
678
|
|
|
592
|
-
//
|
|
593
|
-
@
|
|
679
|
+
// Field — property of the selector target
|
|
680
|
+
@property('#chart', 'data')
|
|
681
|
+
chartData: number[];
|
|
682
|
+
|
|
683
|
+
// Method — return value goes to #submit-btn.disabled
|
|
684
|
+
@property('#submit-btn', 'disabled')
|
|
594
685
|
updateSubmitState() {
|
|
595
686
|
return this.hasErrors;
|
|
596
687
|
}
|
|
597
688
|
|
|
598
|
-
//
|
|
599
|
-
@
|
|
600
|
-
|
|
601
|
-
return this.
|
|
689
|
+
// Method — same-named property on the selector target (input.value)
|
|
690
|
+
@property('input')
|
|
691
|
+
value() {
|
|
692
|
+
return this.initialValue;
|
|
602
693
|
}
|
|
603
694
|
}
|
|
604
695
|
```
|
|
605
696
|
|
|
606
|
-
**
|
|
607
|
-
- `@property
|
|
608
|
-
- `@property('
|
|
609
|
-
- `@property('propertyName', options)` -
|
|
610
|
-
- `@property(
|
|
611
|
-
- `@
|
|
612
|
-
|
|
613
|
-
**@setProperty Patterns:**
|
|
614
|
-
- `@setProperty('propertyName')` - Set property on $this
|
|
615
|
-
- `@setProperty('#selector', 'propertyName')` - Set property on selector
|
|
616
|
-
- `@setProperty('propertyName', options)` - Set property on $this with options
|
|
617
|
-
- `@setProperty('#selector', 'propertyName', options)` - Set property on selector with options
|
|
618
|
-
- `@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)
|
|
619
703
|
|
|
620
704
|
### 6.6 **DOM Observers (@mutationObserver, @resizeObserver, @intersectionObserver)**
|
|
621
705
|
|
|
@@ -642,8 +726,10 @@ class ListObserver extends HTMLElement {
|
|
|
642
726
|
```
|
|
643
727
|
|
|
644
728
|
**Callback signature:**
|
|
645
|
-
- `matchedEls: HTMLElement[]` - Elements matching the selector (1st arg)
|
|
646
|
-
- `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).
|
|
647
733
|
- `observer: MutationObserver` - The observer instance (3rd arg)
|
|
648
734
|
|
|
649
735
|
**How delegate works:**
|
|
@@ -674,7 +760,7 @@ Detect element size changes and react automatically.
|
|
|
674
760
|
@elementDefine('chart-widget')
|
|
675
761
|
class ChartWidget extends HTMLElement {
|
|
676
762
|
// Re-render when component size changes (selector defaults to $this)
|
|
677
|
-
@resizeObserverLight
|
|
763
|
+
@resizeObserverLight
|
|
678
764
|
onResize(matchedEls: HTMLElement[], entries: ResizeObserverEntry[], observer: ResizeObserver) {
|
|
679
765
|
this.redraw(entries[0]?.contentRect);
|
|
680
766
|
}
|
|
@@ -766,20 +852,20 @@ Surgically add, replace, or remove nodes in the DOM with fine-grained control.
|
|
|
766
852
|
```typescript
|
|
767
853
|
@elementDefine('content-updater')
|
|
768
854
|
class ContentUpdater extends HTMLElement {
|
|
769
|
-
@
|
|
770
|
-
@replaceChildren
|
|
855
|
+
@event('button', 'click')
|
|
856
|
+
@replaceChildren
|
|
771
857
|
updateContent() {
|
|
772
858
|
return `<div>New content</div>`;
|
|
773
859
|
}
|
|
774
860
|
|
|
775
|
-
@
|
|
776
|
-
@
|
|
861
|
+
@event('.append-btn', 'click')
|
|
862
|
+
@insertBeforeEnd
|
|
777
863
|
appendContent() {
|
|
778
864
|
return `<p>Appended content</p>`;
|
|
779
865
|
}
|
|
780
866
|
|
|
781
|
-
@
|
|
782
|
-
@
|
|
867
|
+
@event('.prepend-btn', 'click')
|
|
868
|
+
@insertAfterBegin
|
|
783
869
|
prependContent() {
|
|
784
870
|
return `<p>Prepended content</p>`;
|
|
785
871
|
}
|
|
@@ -798,15 +884,14 @@ class ContentUpdater extends HTMLElement {
|
|
|
798
884
|
```
|
|
799
885
|
|
|
800
886
|
**@applyNode Decorator Variants:**
|
|
801
|
-
- `
|
|
802
|
-
- `replaceChildrenLight
|
|
803
|
-
- `
|
|
804
|
-
- `
|
|
805
|
-
- `
|
|
806
|
-
- `
|
|
807
|
-
- `
|
|
808
|
-
- `
|
|
809
|
-
- `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') })`)
|
|
810
895
|
|
|
811
896
|
**Position Options:**
|
|
812
897
|
```typescript
|
|
@@ -819,7 +904,8 @@ export type ApplyNodePosition =
|
|
|
819
904
|
| 'replaceChildren' // Replace children
|
|
820
905
|
| 'innerHtml' // Set innerHTML
|
|
821
906
|
| 'innerText' // Set innerText
|
|
822
|
-
| 'remove'
|
|
907
|
+
| 'remove' // Remove element
|
|
908
|
+
| 'clearChildren'; // Remove all children
|
|
823
909
|
```
|
|
824
910
|
|
|
825
911
|
### 8. **Style & Class Management**
|
|
@@ -832,8 +918,8 @@ class StyledComponent extends HTMLElement {
|
|
|
832
918
|
@state('isActive')
|
|
833
919
|
isActive = false;
|
|
834
920
|
|
|
835
|
-
@
|
|
836
|
-
@updateStyle
|
|
921
|
+
@event('button', 'click')
|
|
922
|
+
@updateStyle
|
|
837
923
|
toggleStyle() {
|
|
838
924
|
return {
|
|
839
925
|
color: this.isActive ? 'green' : 'red',
|
|
@@ -841,9 +927,9 @@ class StyledComponent extends HTMLElement {
|
|
|
841
927
|
};
|
|
842
928
|
}
|
|
843
929
|
|
|
844
|
-
@
|
|
845
|
-
@updateClass
|
|
846
|
-
|
|
930
|
+
@event('.toggle-btn', 'click')
|
|
931
|
+
@updateClass
|
|
932
|
+
toggleState() {
|
|
847
933
|
return {
|
|
848
934
|
'active': this.isActive,
|
|
849
935
|
'disabled': !this.isActive
|
|
@@ -862,17 +948,18 @@ class StyledComponent extends HTMLElement {
|
|
|
862
948
|
}
|
|
863
949
|
```
|
|
864
950
|
|
|
865
|
-
**@applyStyle Variants:**
|
|
866
|
-
- `
|
|
867
|
-
- `
|
|
868
|
-
- `
|
|
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`)
|
|
869
956
|
|
|
870
|
-
**@applyClass Variants:**
|
|
871
|
-
- `
|
|
872
|
-
- `
|
|
873
|
-
- `
|
|
874
|
-
- `
|
|
875
|
-
- `
|
|
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
|
|
876
963
|
|
|
877
964
|
### 8.5 **Lifecycle Hooks**
|
|
878
965
|
|
|
@@ -889,17 +976,18 @@ class MyComponent extends HTMLElement {
|
|
|
889
976
|
|
|
890
977
|
@onConnectedBefore({ order: 0 })
|
|
891
978
|
beforeConnected() {
|
|
892
|
-
// Called
|
|
979
|
+
// Called on connect, before rendering
|
|
893
980
|
}
|
|
894
981
|
|
|
895
982
|
@onConnectedAfter({ order: 1 })
|
|
896
983
|
afterConnected() {
|
|
897
|
-
// Called after
|
|
984
|
+
// Called on connect, after rendering and observer setup
|
|
898
985
|
}
|
|
899
986
|
|
|
900
|
-
@
|
|
901
|
-
|
|
902
|
-
// 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>`;
|
|
903
991
|
}
|
|
904
992
|
|
|
905
993
|
@onDisconnectedBefore
|
|
@@ -907,7 +995,7 @@ class MyComponent extends HTMLElement {
|
|
|
907
995
|
// Called before element disconnects from DOM
|
|
908
996
|
}
|
|
909
997
|
|
|
910
|
-
@onDisconnected
|
|
998
|
+
@onDisconnected // alias of @onDisconnectedAfter
|
|
911
999
|
onDisconnected() {
|
|
912
1000
|
// Called when element leaves DOM
|
|
913
1001
|
}
|
|
@@ -917,15 +1005,15 @@ class MyComponent extends HTMLElement {
|
|
|
917
1005
|
// Called before element is adopted into new document
|
|
918
1006
|
}
|
|
919
1007
|
|
|
920
|
-
@onAdopted
|
|
1008
|
+
@onAdopted // alias of @onAdoptedAfter
|
|
921
1009
|
onAdopted() {
|
|
922
1010
|
// Called when element is adopted into new document
|
|
923
1011
|
}
|
|
924
1012
|
|
|
925
1013
|
@onConnectedSwcApp({ order: 0 })
|
|
926
1014
|
onSwcAppConnected() {
|
|
927
|
-
// Called
|
|
928
|
-
//
|
|
1015
|
+
// Called when the element registers with a connected SwcApp host
|
|
1016
|
+
// (deferred until SwcApp.connect() if the app is not connected yet)
|
|
929
1017
|
}
|
|
930
1018
|
|
|
931
1019
|
@onConnectedCompleted({ order: 0 })
|
|
@@ -935,17 +1023,16 @@ class MyComponent extends HTMLElement {
|
|
|
935
1023
|
}
|
|
936
1024
|
```
|
|
937
1025
|
|
|
938
|
-
**Lifecycle Execution Order:**
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
- `@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`).
|
|
949
1036
|
|
|
950
1037
|
**Order Parameter:**
|
|
951
1038
|
All lifecycle decorators support `order?: number` for controlling execution sequence:
|
|
@@ -957,17 +1044,19 @@ onFirst() { }
|
|
|
957
1044
|
onSecond() { }
|
|
958
1045
|
```
|
|
959
1046
|
|
|
960
|
-
### 8.6 **Timers (@setInterval, @setTimeout)**
|
|
1047
|
+
### 8.6 **Timers (@setInterval, @setTimeout, @requestAnimationFrame)**
|
|
961
1048
|
|
|
962
|
-
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):
|
|
963
1050
|
|
|
964
|
-
-
|
|
965
|
-
-
|
|
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.
|
|
966
1053
|
|
|
967
1054
|
```typescript
|
|
968
1055
|
@elementDefine('live-clock')
|
|
969
1056
|
class LiveClock extends HTMLElement {
|
|
1057
|
+
// Auto-start on connect
|
|
970
1058
|
@setInterval(1000, {
|
|
1059
|
+
type: 'onConnected',
|
|
971
1060
|
parameter: (set) => [Date.now()],
|
|
972
1061
|
created: (set, id) => console.log('interval started, timer id:', id)
|
|
973
1062
|
})
|
|
@@ -975,94 +1064,94 @@ class LiveClock extends HTMLElement {
|
|
|
975
1064
|
this.textContent = new Date(now).toLocaleTimeString();
|
|
976
1065
|
}
|
|
977
1066
|
|
|
978
|
-
// No options - called with no arguments, once per second
|
|
979
|
-
@setInterval(1000)
|
|
980
|
-
onTick() {
|
|
981
|
-
console.log('tick');
|
|
982
|
-
}
|
|
983
|
-
|
|
984
1067
|
// Fires once, 3 seconds after connect
|
|
985
|
-
@setTimeout(3000)
|
|
1068
|
+
@setTimeout(3000, { type: 'onConnected' })
|
|
986
1069
|
onceAfter3s() {
|
|
987
1070
|
console.log('3 seconds have passed');
|
|
988
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
|
+
}
|
|
989
1078
|
}
|
|
990
1079
|
```
|
|
991
1080
|
|
|
992
1081
|
**`@setInterval(interval, options?)` / `@setTimeout(delay, options?)`:**
|
|
993
1082
|
- `interval` / `delay` - milliseconds. `setInterval` repeats; `setTimeout` fires once.
|
|
994
|
-
- `
|
|
995
|
-
- `
|
|
996
|
-
- `
|
|
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`.
|
|
997
1090
|
|
|
998
1091
|
```typescript
|
|
999
|
-
@
|
|
1000
|
-
|
|
1001
|
-
return
|
|
1002
|
-
onTick: (id: number) => console.log('interval', id, 'ticked'),
|
|
1003
|
-
};
|
|
1092
|
+
@requestAnimationFrame
|
|
1093
|
+
animate() {
|
|
1094
|
+
return (ts: number, prev = 0) => (prev < 100 ? prev + 1 : null); // stops at 100
|
|
1004
1095
|
}
|
|
1005
1096
|
```
|
|
1006
1097
|
|
|
1007
|
-
|
|
1098
|
+
### 8.7 **@around (AOP) and @eventMedia**
|
|
1008
1099
|
|
|
1009
|
-
|
|
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.
|
|
1010
1101
|
|
|
1011
|
-
|
|
1102
|
+
```typescript
|
|
1103
|
+
@around({ before: (h, args: [string]) => [args[0].trim()], after: (h, r: string) => r + '!' })
|
|
1104
|
+
greet(name: string) { return `hi ${name}`; }
|
|
1012
1105
|
|
|
1013
|
-
|
|
1106
|
+
@around({ set: (h, v: string) => v?.trim() })
|
|
1107
|
+
label: string = '';
|
|
1108
|
+
```
|
|
1014
1109
|
|
|
1015
|
-
|
|
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`.
|
|
1016
1111
|
|
|
1017
|
-
|
|
1112
|
+
```typescript
|
|
1113
|
+
@eventMediaChange('(max-width: 600px)')
|
|
1114
|
+
onMobile(e: MediaQueryListEvent) { this.compact = e.matches; }
|
|
1115
|
+
```
|
|
1018
1116
|
|
|
1019
|
-
|
|
1020
|
-
- Used for boolean conditions and value comparisons
|
|
1021
|
-
- Syntax: `{{ expression }}`
|
|
1022
|
-
- Example: `{{ $value === 'pending' }}`, `{{ $host.isLoggedIn }}`
|
|
1023
|
-
- Context variables: `$value`, `$item`, `$index`, `$host`, `$parentHost`, `$appHost`, etc.
|
|
1117
|
+
### 9. **Attribute Expressions (`{{= }}`)**
|
|
1024
1118
|
|
|
1025
|
-
|
|
1026
|
-
- Evaluates JavaScript expressions and returns result
|
|
1027
|
-
- Syntax: `{{= functionCall() }}` or `{{= computedValue }}`
|
|
1028
|
-
- Automatically executes functions and captures return values
|
|
1029
|
-
- Example: `{{= $item.name }}`, `{{= $parentHost.getData() }}`, `{{= formatDate($item.date) }}`
|
|
1030
|
-
- Used in attributes for dynamic substitution
|
|
1031
|
-
- Result is converted to string for DOM attributes
|
|
1119
|
+
An attribute value of the form `{{= expression }}` is evaluated as JavaScript instead of being used as a string:
|
|
1032
1120
|
|
|
1033
|
-
|
|
1034
|
-
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
-
|
|
1038
|
-
-
|
|
1039
|
-
-
|
|
1040
|
-
|
|
1121
|
+
```html
|
|
1122
|
+
<product-detail product-id="{{= $host.selectedId }}"></product-detail>
|
|
1123
|
+
```
|
|
1124
|
+
|
|
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.
|
|
1041
1130
|
|
|
1042
1131
|
---
|
|
1043
1132
|
|
|
1044
|
-
###
|
|
1133
|
+
### 10. **Component Communication - Message Bus**
|
|
1045
1134
|
|
|
1046
1135
|
SWC provides a powerful message bus system for inter-component communication through SwcApp. Components can publish and subscribe to typed messages while connected.
|
|
1047
1136
|
|
|
1048
|
-
#### @
|
|
1137
|
+
#### @subscribeSwcAppMessage
|
|
1049
1138
|
Subscribe to messages while component is connected to DOM.
|
|
1050
1139
|
|
|
1051
1140
|
```typescript
|
|
1052
1141
|
@elementDefine('notification-panel')
|
|
1053
1142
|
class NotificationPanel extends HTMLElement {
|
|
1054
|
-
@
|
|
1143
|
+
@subscribeSwcAppMessage
|
|
1055
1144
|
onAnyMessage(message: SwcAppMessage) {
|
|
1056
1145
|
console.log('Received message:', message);
|
|
1057
1146
|
}
|
|
1058
1147
|
|
|
1059
|
-
@
|
|
1148
|
+
@subscribeSwcAppMessage('user-login')
|
|
1060
1149
|
onUserLogin(message: SwcAppMessage<{ username: string }>) {
|
|
1061
1150
|
console.log(`Welcome ${message.data?.username}`);
|
|
1062
1151
|
}
|
|
1063
1152
|
|
|
1064
|
-
@
|
|
1065
|
-
filter: (msg) => msg.data?.username === 'admin'
|
|
1153
|
+
@subscribeSwcAppMessage('user-login', {
|
|
1154
|
+
filter: (msg, currentThis) => msg.data?.username === 'admin'
|
|
1066
1155
|
})
|
|
1067
1156
|
onAdminLogin(message: SwcAppMessage) {
|
|
1068
1157
|
console.log('Admin logged in!');
|
|
@@ -1070,13 +1159,16 @@ class NotificationPanel extends HTMLElement {
|
|
|
1070
1159
|
}
|
|
1071
1160
|
```
|
|
1072
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
|
+
|
|
1073
1164
|
#### @publishSwcAppMessage
|
|
1074
1165
|
Publish a message from a method's return value.
|
|
1075
1166
|
|
|
1076
1167
|
```typescript
|
|
1077
1168
|
@elementDefine('login-form')
|
|
1078
1169
|
class LoginForm extends HTMLElement {
|
|
1079
|
-
|
|
1170
|
+
// No arguments needed → bare form
|
|
1171
|
+
@publishSwcAppMessage
|
|
1080
1172
|
async onSubmit() {
|
|
1081
1173
|
const user = await this.authService.login(
|
|
1082
1174
|
this.username,
|
|
@@ -1094,11 +1186,11 @@ class LoginForm extends HTMLElement {
|
|
|
1094
1186
|
```
|
|
1095
1187
|
|
|
1096
1188
|
**@publishSwcAppMessage Decorator Variants:**
|
|
1097
|
-
- `publishSwcAppMessage
|
|
1189
|
+
- `publishSwcAppMessage` - Publish without message type (bare)
|
|
1098
1190
|
- `publishSwcAppMessage(messageType)` - Publish with specific message type
|
|
1099
1191
|
- `publishSwcAppMessage(messageType, { valueKey: 'customKey' })` - Publish with custom value extraction
|
|
1100
1192
|
- `publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' })` - Publish with options object
|
|
1101
|
-
- `publishMessage
|
|
1193
|
+
- `publishMessage` - Short alias
|
|
1102
1194
|
- `publishMessage(messageType)` - Short alias with message type
|
|
1103
1195
|
- `publishMessage(messageType, options)` - Short alias with options
|
|
1104
1196
|
|
|
@@ -1130,7 +1222,7 @@ type SwcAppMessage<T = any> = {
|
|
|
1130
1222
|
- ✅ **Filter support** - use custom filter functions to handle specific conditions
|
|
1131
1223
|
- ✅ **Auto-publishing** - return value automatically becomes message payload
|
|
1132
1224
|
- ✅ **Async support** - works with async methods (promises)
|
|
1133
|
-
- ✅ **Centralized** - all messages routed through SwcApp host
|
|
1225
|
+
- ✅ **Centralized** - all messages routed through the nearest SwcApp host (`$appHost`)
|
|
1134
1226
|
- ✅ **Decoupled** - components don't need to know about each other
|
|
1135
1227
|
|
|
1136
1228
|
**Usage Example:**
|
|
@@ -1148,7 +1240,7 @@ class ProductList extends HTMLElement {
|
|
|
1148
1240
|
// Subscriber component
|
|
1149
1241
|
@elementDefine('product-detail')
|
|
1150
1242
|
class ProductDetail extends HTMLElement {
|
|
1151
|
-
@
|
|
1243
|
+
@subscribeSwcAppMessage('product-selected')
|
|
1152
1244
|
onProductSelected(message: SwcAppMessage<Product>) {
|
|
1153
1245
|
this.displayProduct(message.data);
|
|
1154
1246
|
}
|
|
@@ -1164,6 +1256,8 @@ SWC provides a flexible Mixin-based architecture for creating SwcApp elements th
|
|
|
1164
1256
|
#### Available SwcApp Variants
|
|
1165
1257
|
|
|
1166
1258
|
```typescript
|
|
1259
|
+
// SwcApp, SwcAppBody, ... are createElement helpers: SwcAppBody(w, data?) creates the element.
|
|
1260
|
+
// defineSwcApp, defineSwcAppBody, ... register the custom element.
|
|
1167
1261
|
import {
|
|
1168
1262
|
SwcApp, // swc-app (HTMLElement)
|
|
1169
1263
|
SwcAppBody, // swc-app-body (HTMLBodyElement, is="body")
|
|
@@ -1220,7 +1314,7 @@ w.document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1220
1314
|
path: '/',
|
|
1221
1315
|
routeType: 'path',
|
|
1222
1316
|
container: container,
|
|
1223
|
-
onStartedLazyDefineComponent: [yourComponentFactory1,
|
|
1317
|
+
onStartedLazyDefineComponent: [yourComponentFactory1, yourComponentFactory2],
|
|
1224
1318
|
window: w,
|
|
1225
1319
|
onEngineStarted: () => {
|
|
1226
1320
|
console.log('🚀 Application started successfully');
|
|
@@ -1230,6 +1324,10 @@ w.document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1230
1324
|
});
|
|
1231
1325
|
```
|
|
1232
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
|
+
|
|
1233
1331
|
#### Factory Pattern - Component Registration
|
|
1234
1332
|
|
|
1235
1333
|
```typescript
|
|
@@ -1269,7 +1367,7 @@ appElement.connect({
|
|
|
1269
1367
|
});
|
|
1270
1368
|
```
|
|
1271
1369
|
|
|
1272
|
-
#### 12. **Routing with @
|
|
1370
|
+
#### 12. **Routing with @subscribeSwcAppRouteChange**
|
|
1273
1371
|
|
|
1274
1372
|
SWC provides declarative routing with automatic path matching, order-based execution, and optional propagation control.
|
|
1275
1373
|
|
|
@@ -1278,26 +1376,26 @@ SWC provides declarative routing with automatic path matching, order-based execu
|
|
|
1278
1376
|
```typescript
|
|
1279
1377
|
@elementDefine('root-router')
|
|
1280
1378
|
class RootRouter extends HTMLElement {
|
|
1281
|
-
@
|
|
1379
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 })
|
|
1282
1380
|
@innerHtmlLight
|
|
1283
1381
|
handleHome(routerPathSet: RouterEventType) {
|
|
1284
1382
|
return `<landing-page/>`;
|
|
1285
1383
|
}
|
|
1286
1384
|
|
|
1287
|
-
@
|
|
1385
|
+
@subscribeSwcAppRouteChange(['/products'], { order: 1 })
|
|
1288
1386
|
@innerHtmlLight
|
|
1289
1387
|
handleProducts(routerPathSet: RouterEventType) {
|
|
1290
1388
|
return `<products-list/>`;
|
|
1291
1389
|
}
|
|
1292
1390
|
|
|
1293
|
-
@
|
|
1391
|
+
@subscribeSwcAppRouteChange(['/product/{id}'], { order: 2 })
|
|
1294
1392
|
@innerHtmlLight
|
|
1295
1393
|
handleProductDetail(routerPathSet: RouterEventType) {
|
|
1296
1394
|
const { id } = routerPathSet.pathData;
|
|
1297
1395
|
return `<product-detail product-id="${id}"/>`;
|
|
1298
1396
|
}
|
|
1299
1397
|
|
|
1300
|
-
@
|
|
1398
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 })
|
|
1301
1399
|
@innerHtmlLight
|
|
1302
1400
|
handle404(routerPathSet: RouterEventType) {
|
|
1303
1401
|
return `<not-found-page/>`;
|
|
@@ -1308,48 +1406,138 @@ class RootRouter extends HTMLElement {
|
|
|
1308
1406
|
#### Route Handler Features
|
|
1309
1407
|
|
|
1310
1408
|
**1. Path Matching**
|
|
1311
|
-
- **No path pattern** (omit path): `@
|
|
1312
|
-
- **
|
|
1313
|
-
- **
|
|
1314
|
-
- **
|
|
1315
|
-
- **
|
|
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)
|
|
1316
1415
|
|
|
1317
1416
|
**2. Order-Based Execution**
|
|
1318
|
-
|
|
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:
|
|
1319
1418
|
|
|
1320
1419
|
```typescript
|
|
1321
|
-
@
|
|
1322
|
-
@
|
|
1323
|
-
@
|
|
1420
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 }) // Checked first
|
|
1421
|
+
@subscribeSwcAppRouteChange(['/admin'], { order: 1 }) // Checked second
|
|
1422
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 }) // Checked last (404)
|
|
1324
1423
|
```
|
|
1325
1424
|
|
|
1326
1425
|
**3. Propagation Control**
|
|
1327
|
-
- **Return a value** (
|
|
1328
|
-
- **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
|
|
1329
1428
|
|
|
1330
1429
|
```typescript
|
|
1331
1430
|
// This handler stops propagation (returns HTML)
|
|
1332
|
-
@
|
|
1431
|
+
@subscribeSwcAppRouteChange(['/admin'], { order: 1 })
|
|
1333
1432
|
@innerHtmlLight
|
|
1334
1433
|
handleAdmin(routerPathSet: RouterEventType) {
|
|
1335
1434
|
return `<admin-panel/>`; // ✅ Stops here
|
|
1336
1435
|
}
|
|
1337
1436
|
|
|
1338
1437
|
// This handler continues propagation (no return value)
|
|
1339
|
-
@
|
|
1438
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
1340
1439
|
onRouteChange(routerPathSet: RouterEventType) {
|
|
1341
1440
|
console.log('Route changed:', routerPathSet.path);
|
|
1342
1441
|
// No return value → continues to next handler
|
|
1343
1442
|
}
|
|
1344
1443
|
```
|
|
1345
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
|
+
|
|
1346
1534
|
#### Advanced Example with Logging
|
|
1347
1535
|
|
|
1348
1536
|
```typescript
|
|
1349
1537
|
@elementDefine('accommodation-router')
|
|
1350
1538
|
class AccommodationRouter extends HTMLElement {
|
|
1351
1539
|
// Global route logger (order: -1 runs first, no path pattern = matches all routes)
|
|
1352
|
-
@
|
|
1540
|
+
@subscribeSwcAppRouteChange({ order: -1 })
|
|
1353
1541
|
onRouteChange(routerPathSet: RouterEventType) {
|
|
1354
1542
|
console.log('[Route Change]', {
|
|
1355
1543
|
path: routerPathSet.path,
|
|
@@ -1360,7 +1548,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1360
1548
|
}
|
|
1361
1549
|
|
|
1362
1550
|
// Home route
|
|
1363
|
-
@
|
|
1551
|
+
@subscribeSwcAppRouteChange(['', '/'], { order: 0 })
|
|
1364
1552
|
@innerHtmlLight
|
|
1365
1553
|
handleHome(routerPathSet: RouterEventType) {
|
|
1366
1554
|
console.log('[Route Handler] Home');
|
|
@@ -1368,7 +1556,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1368
1556
|
}
|
|
1369
1557
|
|
|
1370
1558
|
// List route
|
|
1371
|
-
@
|
|
1559
|
+
@subscribeSwcAppRouteChange(['/list'], { order: 1 })
|
|
1372
1560
|
@innerHtmlLight
|
|
1373
1561
|
handleList(routerPathSet: RouterEventType) {
|
|
1374
1562
|
console.log('[Route Handler] List');
|
|
@@ -1376,7 +1564,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1376
1564
|
}
|
|
1377
1565
|
|
|
1378
1566
|
// Detail route with dynamic parameter
|
|
1379
|
-
@
|
|
1567
|
+
@subscribeSwcAppRouteChange(['/detail/{productId}'], { order: 2 })
|
|
1380
1568
|
@innerHtmlLight
|
|
1381
1569
|
handleDetail(routerPathSet: RouterEventType) {
|
|
1382
1570
|
const { productId } = routerPathSet.pathData;
|
|
@@ -1385,7 +1573,7 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1385
1573
|
}
|
|
1386
1574
|
|
|
1387
1575
|
// 404 fallback (order: 999 runs last)
|
|
1388
|
-
@
|
|
1576
|
+
@subscribeSwcAppRouteChange(['/{tail:.*}'], { order: 999 })
|
|
1389
1577
|
@innerHtmlLight
|
|
1390
1578
|
handle404(routerPathSet: RouterEventType) {
|
|
1391
1579
|
console.log('[Route Handler] 404 Not Found', routerPathSet.path);
|
|
@@ -1398,15 +1586,21 @@ class AccommodationRouter extends HTMLElement {
|
|
|
1398
1586
|
|
|
1399
1587
|
```typescript
|
|
1400
1588
|
interface SwcAppRouteChangeOptions {
|
|
1401
|
-
path?: RoutePathType; //
|
|
1589
|
+
path?: RoutePathType; // string | string[] | (currentThis) => string | string[]
|
|
1402
1590
|
order?: number; // Execution order (default: 0)
|
|
1403
|
-
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")
|
|
1404
1596
|
}
|
|
1405
1597
|
```
|
|
1406
1598
|
|
|
1599
|
+
Forms: `@subscribeSwcAppRouteChange` (bare, all routes), `('/path')`, `(['/a', '/b'])`, `(path, options)`, `(options)`. Alias: `@changedRoute`.
|
|
1600
|
+
|
|
1407
1601
|
**Usage:**
|
|
1408
1602
|
```typescript
|
|
1409
|
-
@
|
|
1603
|
+
@subscribeSwcAppRouteChange(
|
|
1410
1604
|
['/admin/{section}'],
|
|
1411
1605
|
{
|
|
1412
1606
|
order: 5,
|
|
@@ -1420,13 +1614,43 @@ handleAdminSection(routerPathSet: RouterEventType) {
|
|
|
1420
1614
|
}
|
|
1421
1615
|
```
|
|
1422
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
|
+
|
|
1423
1642
|
#### RouterEventType Structure
|
|
1424
1643
|
|
|
1425
1644
|
```typescript
|
|
1426
1645
|
type RouterEventType = {
|
|
1427
|
-
path: string;
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
|
1430
1654
|
};
|
|
1431
1655
|
```
|
|
1432
1656
|
|
|
@@ -1459,10 +1683,10 @@ import {
|
|
|
1459
1683
|
|
|
1460
1684
|
@elementDefine('multi-decorator-example')
|
|
1461
1685
|
class MultiDecoratorExample extends HTMLElement {
|
|
1462
|
-
@
|
|
1463
|
-
@
|
|
1464
|
-
@updateStyle
|
|
1465
|
-
@updateClass
|
|
1686
|
+
@attribute('selector', 'data-state')
|
|
1687
|
+
@property('selector', 'value')
|
|
1688
|
+
@updateStyle
|
|
1689
|
+
@updateClass
|
|
1466
1690
|
handleUpdate() {
|
|
1467
1691
|
return {
|
|
1468
1692
|
[ATTRIBUTE_METADATA_KEY]: 'attribute-value',
|
|
@@ -1481,10 +1705,10 @@ For more readable code, use the `valueKey` option to specify custom keys:
|
|
|
1481
1705
|
```typescript
|
|
1482
1706
|
@elementDefine('custom-key-example')
|
|
1483
1707
|
class CustomKeyExample extends HTMLElement {
|
|
1484
|
-
@
|
|
1485
|
-
@
|
|
1486
|
-
@updateStyle({ valueKey: 'styleValue' })
|
|
1487
|
-
@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`
|
|
1488
1712
|
handleUpdate() {
|
|
1489
1713
|
return {
|
|
1490
1714
|
attrValue: 'attribute-value',
|
|
@@ -1503,9 +1727,9 @@ You can mix both approaches in the same method:
|
|
|
1503
1727
|
```typescript
|
|
1504
1728
|
@elementDefine('mixed-keys-example')
|
|
1505
1729
|
class MixedKeysExample extends HTMLElement {
|
|
1506
|
-
@
|
|
1507
|
-
@
|
|
1508
|
-
@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
|
|
1509
1733
|
handleUpdate() {
|
|
1510
1734
|
return {
|
|
1511
1735
|
[ATTRIBUTE_METADATA_KEY]: 'attr-value',
|
|
@@ -1517,15 +1741,16 @@ class MixedKeysExample extends HTMLElement {
|
|
|
1517
1741
|
```
|
|
1518
1742
|
|
|
1519
1743
|
**Supported Decorators with valueKey:**
|
|
1520
|
-
- `@applyAttribute` / `@
|
|
1521
|
-
- `@applyProperty` / `@
|
|
1522
|
-
- `@applySlot`
|
|
1523
|
-
- `@applyNode` / `@
|
|
1744
|
+
- `@applyAttribute` / `@attribute`
|
|
1745
|
+
- `@applyProperty` / `@property`
|
|
1746
|
+
- `@applySlot(id, { position, valueKey })` (the `clearSlot` / `appendHtmlSlot` / ... shorthands take no options)
|
|
1747
|
+
- `@applyNode` / `@replaceChildren` / `@innerHtml` / etc.
|
|
1524
1748
|
- `@applyStyle` / `@updateStyle` / etc.
|
|
1525
1749
|
- `@applyClass` / `@updateClass` / etc.
|
|
1526
1750
|
- `@emitCustomEvent` / `@emit`
|
|
1527
|
-
- `@publishSwcAppMessage` / `@
|
|
1528
|
-
- `@
|
|
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.
|
|
1529
1754
|
|
|
1530
1755
|
Each decorator will use only its corresponding value from the return object, preventing conflicts and allowing clean separation of concerns.
|
|
1531
1756
|
|
|
@@ -1576,7 +1801,7 @@ appElement.connect({
|
|
|
1576
1801
|
|
|
1577
1802
|
### Simplified Decorator API
|
|
1578
1803
|
|
|
1579
|
-
|
|
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.
|
|
1580
1805
|
|
|
1581
1806
|
#### Current Usage Pattern
|
|
1582
1807
|
```typescript
|
|
@@ -1584,7 +1809,7 @@ The decorator API uses a consolidated pattern where all decorators support **opt
|
|
|
1584
1809
|
@applyClass('selector', 'update')
|
|
1585
1810
|
method() { ... }
|
|
1586
1811
|
|
|
1587
|
-
@applyClass('update') //
|
|
1812
|
+
@applyClass('$this', 'update') // a single string is treated as the selector, so name $this explicitly (or use bare @updateClass)
|
|
1588
1813
|
method() { ... }
|
|
1589
1814
|
|
|
1590
1815
|
@applyClass // Bare decorator - selector defaults to $this
|
|
@@ -1594,7 +1819,7 @@ method() { ... }
|
|
|
1594
1819
|
#### Decorator Functions (Current API)
|
|
1595
1820
|
|
|
1596
1821
|
**applyClass.ts:**
|
|
1597
|
-
- `applyClass(selector?, options?)` - Set/update/add/remove/toggle classes
|
|
1822
|
+
- `applyClass(selector?, action?, options?)` - Set/update/add/remove/toggle classes
|
|
1598
1823
|
- `setClass(selector?, options?)` - Replace all classes
|
|
1599
1824
|
- `updateClass(selector?, options?)` - Toggle classes
|
|
1600
1825
|
- `addClass(selector?, options?)` - Add classes
|
|
@@ -1602,52 +1827,51 @@ method() { ... }
|
|
|
1602
1827
|
- `toggleClass(selector?, options?)` - Toggle classes
|
|
1603
1828
|
|
|
1604
1829
|
**applyStyle.ts:**
|
|
1605
|
-
- `applyStyle(selector?, options?)` - Set/update/remove styles
|
|
1830
|
+
- `applyStyle(selector?, action?, options?)` - Set/update/remove styles
|
|
1606
1831
|
- `setStyle(selector?, options?)` - Clear and set styles
|
|
1607
1832
|
- `updateStyle(selector?, options?)` - Update/merge styles
|
|
1608
1833
|
- `removeStyle(selector?, options?)` - Remove specific styles
|
|
1609
1834
|
|
|
1610
1835
|
**applyProperty.ts:**
|
|
1611
|
-
- `property(selector?, options?)` -
|
|
1612
|
-
- `setProperty(selector?, options?)` - Set element properties
|
|
1836
|
+
- `property(selector?, propertyKey?, options?)` - field: property get/set; method: return value → property
|
|
1613
1837
|
|
|
1614
1838
|
**applyAttribute.ts:**
|
|
1615
|
-
- `attribute(selector?, options?)` -
|
|
1616
|
-
- `setAttribute(selector?, options?)` - Set element attributes
|
|
1839
|
+
- `attribute(attrName | selector?, attrName?, options?)` - field: attribute get/set; method: return value → attribute (one string = $this)
|
|
1617
1840
|
|
|
1618
1841
|
**query.ts:**
|
|
1619
|
-
- `query(selector
|
|
1620
|
-
- `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'`)
|
|
1621
1844
|
|
|
1622
1845
|
**emitCustomEvent.ts:**
|
|
1623
1846
|
- `emitCustomEvent(target, type, options?)` - Emit custom events
|
|
1624
1847
|
- `emit(target, type, options?)` - Short alias
|
|
1848
|
+
- `emitCustomEvent(type, options?)` - Emit from `$this`
|
|
1625
1849
|
|
|
1626
1850
|
#### Usage Examples
|
|
1627
1851
|
|
|
1628
1852
|
```typescript
|
|
1629
1853
|
@elementDefine('my-component')
|
|
1630
1854
|
class MyComponent extends HTMLElement {
|
|
1631
|
-
// Class management -
|
|
1632
|
-
@updateClass
|
|
1855
|
+
// Class management - bare form targets $this
|
|
1856
|
+
@updateClass
|
|
1633
1857
|
toggleActive() {
|
|
1634
1858
|
return { 'active': this.isActive };
|
|
1635
1859
|
}
|
|
1636
1860
|
|
|
1637
|
-
// Style management -
|
|
1638
|
-
@updateStyle
|
|
1861
|
+
// Style management - bare form targets $this
|
|
1862
|
+
@updateStyle
|
|
1639
1863
|
applyTheme() {
|
|
1640
1864
|
return { color: this.theme.color, fontSize: '16px' };
|
|
1641
1865
|
}
|
|
1642
1866
|
|
|
1643
|
-
// Property binding -
|
|
1644
|
-
@
|
|
1867
|
+
// Property binding - first string is the selector
|
|
1868
|
+
@property('input', 'value')
|
|
1645
1869
|
updateValue() {
|
|
1646
1870
|
return this.computedValue;
|
|
1647
1871
|
}
|
|
1648
1872
|
|
|
1649
|
-
// Attribute binding -
|
|
1650
|
-
@
|
|
1873
|
+
// Attribute binding - one string = attribute on $this
|
|
1874
|
+
@attribute('data-id')
|
|
1651
1875
|
updateId() {
|
|
1652
1876
|
return this.elementId;
|
|
1653
1877
|
}
|
|
@@ -1660,14 +1884,14 @@ class MyComponent extends HTMLElement {
|
|
|
1660
1884
|
listItems?: HTMLLIElement[];
|
|
1661
1885
|
|
|
1662
1886
|
// Emit custom events
|
|
1663
|
-
@
|
|
1887
|
+
@event('button', 'click')
|
|
1664
1888
|
@emitCustomEvent('$this', 'item-selected')
|
|
1665
1889
|
onItemSelect() {
|
|
1666
1890
|
return { itemId: this.selectedId };
|
|
1667
1891
|
}
|
|
1668
1892
|
|
|
1669
|
-
// With selector
|
|
1670
|
-
@
|
|
1893
|
+
// With selector and action
|
|
1894
|
+
@applyClass('.card', 'update')
|
|
1671
1895
|
updateCardClass() {
|
|
1672
1896
|
return { 'highlighted': true };
|
|
1673
1897
|
}
|
|
@@ -1676,7 +1900,7 @@ class MyComponent extends HTMLElement {
|
|
|
1676
1900
|
|
|
1677
1901
|
#### Key Features
|
|
1678
1902
|
|
|
1679
|
-
1. **
|
|
1903
|
+
1. **Optional Selector** - Omit the selector (or use the bare form) to target `$this`
|
|
1680
1904
|
2. **Cleaner Code** - Single function name with multiple overloads
|
|
1681
1905
|
3. **Better Discoverability** - Intuitive parameter patterns
|
|
1682
1906
|
4. **Flexible Usage** - Supports bare decorators, with options, and with selectors
|
|
@@ -1703,17 +1927,16 @@ class UserWidget extends w.HTMLElement { }
|
|
|
1703
1927
|
class UserWidget extends w.HTMLElement { }
|
|
1704
1928
|
```
|
|
1705
1929
|
|
|
1706
|
-
### Factory Always Returns
|
|
1930
|
+
### Factory Always Returns (the tag name)
|
|
1707
1931
|
```typescript
|
|
1708
1932
|
export default (w: Window) => {
|
|
1709
1933
|
const tagName = 'my-element';
|
|
1710
|
-
|
|
1711
|
-
if (existing) return existing;
|
|
1934
|
+
if (w.customElements.get(tagName)) return tagName;
|
|
1712
1935
|
|
|
1713
1936
|
@elementDefine(tagName, { window: w })
|
|
1714
1937
|
class MyElement extends w.HTMLElement { }
|
|
1715
|
-
|
|
1716
|
-
return
|
|
1938
|
+
|
|
1939
|
+
return tagName;
|
|
1717
1940
|
};
|
|
1718
1941
|
```
|
|
1719
1942
|
|
|
@@ -1731,6 +1954,20 @@ constructor(private service: Service) { super(); } // Web Components can't have
|
|
|
1731
1954
|
|
|
1732
1955
|
---
|
|
1733
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
|
+
|
|
1734
1971
|
## 📚 Examples
|
|
1735
1972
|
|
|
1736
1973
|
- **[Commerce (E-Commerce SPA)](https://github.com/dooboostore-develop/packages/tree/main/%40dooboostore/simple-web-component/examples/commerce)** - Full shopping cart example
|
|
@@ -1750,3 +1987,219 @@ constructor(private service: Service) { super(); } // Web Components can't have
|
|
|
1750
1987
|
## 📄 License
|
|
1751
1988
|
[MIT License](https://github.com/dooboostore-develop/packages/tree/main/%40dooboostore/simple-web-component/LICENSE.md)
|
|
1752
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).
|