@dooboostore/simple-web-component 1.0.50 → 1.0.52

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