@dooboostore/simple-web-component 1.0.23 → 1.0.25

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 (149) hide show
  1. package/README.md +64 -3212
  2. package/dist/cjs/decorators/addEventListener.js +1 -1
  3. package/dist/cjs/decorators/addEventListener.js.map +2 -2
  4. package/dist/cjs/decorators/applyAttribute.js +148 -0
  5. package/dist/cjs/decorators/applyAttribute.js.map +7 -0
  6. package/dist/cjs/decorators/applyClass.js +164 -0
  7. package/dist/cjs/decorators/applyClass.js.map +7 -0
  8. package/dist/cjs/decorators/applyNode.js +97 -109
  9. package/dist/cjs/decorators/applyNode.js.map +2 -2
  10. package/dist/cjs/decorators/applyStyle.js +135 -0
  11. package/dist/cjs/decorators/applyStyle.js.map +7 -0
  12. package/dist/cjs/decorators/attribute.js +24 -11
  13. package/dist/cjs/decorators/attribute.js.map +2 -2
  14. package/dist/cjs/decorators/changedAttributeHost.js +1 -1
  15. package/dist/cjs/decorators/changedAttributeHost.js.map +2 -2
  16. package/dist/cjs/decorators/elementDefine.js +14 -23
  17. package/dist/cjs/decorators/elementDefine.js.map +2 -2
  18. package/dist/cjs/decorators/emitCustomEvent.js +5 -2
  19. package/dist/cjs/decorators/emitCustomEvent.js.map +2 -2
  20. package/dist/cjs/decorators/index.js +3 -3
  21. package/dist/cjs/decorators/index.js.map +2 -2
  22. package/dist/cjs/decorators/lifecycles.js +9 -9
  23. package/dist/cjs/decorators/lifecycles.js.map +2 -2
  24. package/dist/cjs/decorators/query.js +78 -3
  25. package/dist/cjs/decorators/query.js.map +2 -2
  26. package/dist/cjs/decorators/queryAll.js +78 -3
  27. package/dist/cjs/decorators/queryAll.js.map +2 -2
  28. package/dist/cjs/elements/SwcAppEngine.js +19 -4
  29. package/dist/cjs/elements/SwcAppEngine.js.map +2 -2
  30. package/dist/cjs/elements/SwcAppMixin.js +2 -0
  31. package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
  32. package/dist/cjs/index.js +4 -4
  33. package/dist/cjs/index.js.map +2 -2
  34. package/dist/cjs/utils/Utils.js +141 -2
  35. package/dist/cjs/utils/Utils.js.map +2 -2
  36. package/dist/esm/decorators/addEventListener.js +1 -1
  37. package/dist/esm/decorators/addEventListener.js.map +2 -2
  38. package/dist/esm/decorators/applyAttribute.js +129 -0
  39. package/dist/esm/decorators/applyAttribute.js.map +7 -0
  40. package/dist/esm/decorators/applyClass.js +145 -0
  41. package/dist/esm/decorators/applyClass.js.map +7 -0
  42. package/dist/esm/decorators/applyNode.js +97 -109
  43. package/dist/esm/decorators/applyNode.js.map +2 -2
  44. package/dist/esm/decorators/applyStyle.js +116 -0
  45. package/dist/esm/decorators/applyStyle.js.map +7 -0
  46. package/dist/esm/decorators/attribute.js +24 -11
  47. package/dist/esm/decorators/attribute.js.map +2 -2
  48. package/dist/esm/decorators/changedAttributeHost.js +1 -1
  49. package/dist/esm/decorators/changedAttributeHost.js.map +2 -2
  50. package/dist/esm/decorators/elementDefine.js +14 -23
  51. package/dist/esm/decorators/elementDefine.js.map +2 -2
  52. package/dist/esm/decorators/emitCustomEvent.js +5 -2
  53. package/dist/esm/decorators/emitCustomEvent.js.map +2 -2
  54. package/dist/esm/decorators/index.js +3 -3
  55. package/dist/esm/decorators/index.js.map +1 -1
  56. package/dist/esm/decorators/lifecycles.js +9 -9
  57. package/dist/esm/decorators/lifecycles.js.map +2 -2
  58. package/dist/esm/decorators/query.js +78 -3
  59. package/dist/esm/decorators/query.js.map +2 -2
  60. package/dist/esm/decorators/queryAll.js +78 -3
  61. package/dist/esm/decorators/queryAll.js.map +2 -2
  62. package/dist/esm/elements/SwcAppEngine.js +19 -4
  63. package/dist/esm/elements/SwcAppEngine.js.map +2 -2
  64. package/dist/esm/elements/SwcAppMixin.js +2 -0
  65. package/dist/esm/elements/SwcAppMixin.js.map +2 -2
  66. package/dist/esm/index.js +4 -4
  67. package/dist/esm/index.js.map +2 -2
  68. package/dist/esm/utils/Utils.js +141 -2
  69. package/dist/esm/utils/Utils.js.map +2 -2
  70. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +3191 -506
  71. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
  72. package/dist/types/decorators/addEventListener.d.ts.map +1 -1
  73. package/dist/types/decorators/applyAttribute.d.ts +22 -0
  74. package/dist/types/decorators/applyAttribute.d.ts.map +1 -0
  75. package/dist/types/decorators/applyClass.d.ts +65 -0
  76. package/dist/types/decorators/applyClass.d.ts.map +1 -0
  77. package/dist/types/decorators/applyNode.d.ts +18 -104
  78. package/dist/types/decorators/applyNode.d.ts.map +1 -1
  79. package/dist/types/decorators/applyStyle.d.ts +21 -0
  80. package/dist/types/decorators/applyStyle.d.ts.map +1 -0
  81. package/dist/types/decorators/attribute.d.ts +12 -14
  82. package/dist/types/decorators/attribute.d.ts.map +1 -1
  83. package/dist/types/decorators/changedAttributeHost.d.ts.map +1 -1
  84. package/dist/types/decorators/elementDefine.d.ts.map +1 -1
  85. package/dist/types/decorators/emitCustomEvent.d.ts +1 -1
  86. package/dist/types/decorators/emitCustomEvent.d.ts.map +1 -1
  87. package/dist/types/decorators/index.d.ts +3 -3
  88. package/dist/types/decorators/index.d.ts.map +1 -1
  89. package/dist/types/decorators/lifecycles.d.ts.map +1 -1
  90. package/dist/types/decorators/query.d.ts +68 -4
  91. package/dist/types/decorators/query.d.ts.map +1 -1
  92. package/dist/types/decorators/queryAll.d.ts +70 -1
  93. package/dist/types/decorators/queryAll.d.ts.map +1 -1
  94. package/dist/types/elements/SwcAppEngine.d.ts +7 -0
  95. package/dist/types/elements/SwcAppEngine.d.ts.map +1 -1
  96. package/dist/types/elements/SwcAppMixin.d.ts +1 -1
  97. package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
  98. package/dist/types/index.d.ts +1 -2
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/utils/Utils.d.ts +49 -0
  101. package/dist/types/utils/Utils.d.ts.map +1 -1
  102. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +3191 -506
  103. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
  104. package/package.json +3 -3
  105. package/src/decorators/addEventListener.ts +1 -1
  106. package/src/decorators/applyAttribute.ts +147 -0
  107. package/src/decorators/applyClass.ts +158 -0
  108. package/src/decorators/applyNode.ts +126 -225
  109. package/src/decorators/applyStyle.ts +128 -0
  110. package/src/decorators/attribute.ts +42 -43
  111. package/src/decorators/changedAttributeHost.ts +2 -2
  112. package/src/decorators/elementDefine.ts +18 -39
  113. package/src/decorators/emitCustomEvent.ts +6 -3
  114. package/src/decorators/index.ts +3 -3
  115. package/src/decorators/lifecycles.ts +9 -11
  116. package/src/decorators/query.ts +148 -6
  117. package/src/decorators/queryAll.ts +149 -3
  118. package/src/elements/SwcAppEngine.ts +32 -4
  119. package/src/elements/SwcAppMixin.ts +2 -0
  120. package/src/index.ts +6 -6
  121. package/src/utils/Utils.ts +146 -1
  122. package/dist/cjs/decorators/classList.js +0 -134
  123. package/dist/cjs/decorators/classList.js.map +0 -7
  124. package/dist/cjs/decorators/setAttribute.js +0 -111
  125. package/dist/cjs/decorators/setAttribute.js.map +0 -7
  126. package/dist/cjs/decorators/style.js +0 -114
  127. package/dist/cjs/decorators/style.js.map +0 -7
  128. package/dist/cjs/utils/TemplateUtils.js +0 -109
  129. package/dist/cjs/utils/TemplateUtils.js.map +0 -7
  130. package/dist/esm/decorators/classList.js +0 -115
  131. package/dist/esm/decorators/classList.js.map +0 -7
  132. package/dist/esm/decorators/setAttribute.js +0 -92
  133. package/dist/esm/decorators/setAttribute.js.map +0 -7
  134. package/dist/esm/decorators/style.js +0 -95
  135. package/dist/esm/decorators/style.js.map +0 -7
  136. package/dist/esm/utils/TemplateUtils.js +0 -90
  137. package/dist/esm/utils/TemplateUtils.js.map +0 -7
  138. package/dist/types/decorators/classList.d.ts +0 -61
  139. package/dist/types/decorators/classList.d.ts.map +0 -1
  140. package/dist/types/decorators/setAttribute.d.ts +0 -23
  141. package/dist/types/decorators/setAttribute.d.ts.map +0 -1
  142. package/dist/types/decorators/style.d.ts +0 -49
  143. package/dist/types/decorators/style.d.ts.map +0 -1
  144. package/dist/types/utils/TemplateUtils.d.ts +0 -59
  145. package/dist/types/utils/TemplateUtils.d.ts.map +0 -1
  146. package/src/decorators/classList.ts +0 -172
  147. package/src/decorators/setAttribute.ts +0 -113
  148. package/src/decorators/style.ts +0 -140
  149. package/src/utils/TemplateUtils.ts +0 -92
package/README.md CHANGED
@@ -1,3262 +1,114 @@
1
1
  # @dooboostore/simple-web-component (SWC)
2
+ [![NPM version](https://img.shields.io/npm/v/@dooboostore/simple-web-component.svg?color=cb3837&style=flat-square)](https://www.npmjs.com/package/@dooboostore/simple-web-component)
3
+ [![Build and Test](https://github.com/dooboostore-develop/packages/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/dooboostore-develop/packages/actions/workflows/main.yaml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
2
5
 
3
6
  > **"Pure, Standards-Compliant, and Blazing Fast Web Components Framework."**
4
7
 
5
- `simple-web-component` (SWC) is a minimalist library designed for building high-performance Web Components and Single Page Applications (SPA). It strips away heavy "magic" features like Proxy-based reactivity and continuous `MutationObserver` scans in favor of a **Pure Architecture** that adheres strictly to browser standards.
8
+ `simple-web-component` (SWC) is a minimalist library designed for building high-performance Web Components and Single Page Applications (SPA). It follows a **Pure Architecture** that adheres strictly to browser standards, avoiding heavy Proxy traps or Virtual DOM overhead.
6
9
 
7
10
  ---
8
11
 
9
12
  ## 🚀 Why SWC?
10
13
 
11
- - **Zero Magic**: No Proxy traps, no Virtual DOM diffing. Predictive and standard-compliant.
12
- - **Native Inheritance**: Inherit directly from `HTMLElement`, `HTMLDivElement`, or `HTMLTemplateElement`.
13
- - **Extreme Performance**: Ideal for high-load UIs (1,000+ nodes) with minimal memory overhead.
14
+ - **Zero Magic**: No hidden reactivity traps. Predictive and standard-compliant.
15
+ - **Native Inheritance**: Inherit directly from `HTMLElement` or built-in elements (`is` attribute).
16
+ - **Extreme Performance**: Optimized for high-load UIs with minimal memory footprint.
14
17
  - **Micro-Frontend Ready**: Independent routing and DI containers for every component scope.
15
- - **Standard-First**: Works directly with native Custom Elements, Shadow DOM, and Slots.
18
+ - **Modern CSS**: First-class support for Container Queries and Shadow DOM.
16
19
 
17
20
  ---
18
21
 
19
- ## 📦 Installation & Setup
20
-
21
- ```bash
22
- npm install @dooboostore/simple-web-component reflect-metadata
23
- ```
24
-
25
- ### Browser Support
26
- For **Safari** support of the `is` attribute, add this to your `<head>`:
27
- ```html
28
- <script src="https://unpkg.com/@ungap/custom-elements"></script>
29
- ```
30
-
31
- ---
32
-
33
- ## 💡 Complete Decorator Reference
34
-
35
- ### Lifecycle Decorators
36
-
37
- #### 1. `@onConstructor()`
38
- Called during component construction (in `constructor`). Perfect for dependency injection setup.
39
-
40
- ```typescript
41
- @elementDefine('my-component')
42
- class MyComponent extends HTMLElement {
43
- private dataService: DataService;
44
-
45
- @onConstructor
46
- setupDI(dataService: DataService) {
47
- this.dataService = dataService;
48
- console.log('Component constructed with DI');
49
- }
50
- }
51
- ```
52
-
53
- **Execution Order**: First - during `new MyComponent()`
54
-
55
- #### 2. `@onBeforeConnected()`
56
- **Dependency Injection & Pre-connection Setup**
57
-
58
- Called before the component connects to the DOM. Perfect for initializing services via DI.
59
-
60
- **⚠️ Important**: Lifecycle DI works **WITHOUT `@Sim`** - regular `@elementDefine` components can receive services here!
61
-
62
- ```typescript
63
- @elementDefine('my-component') // ← NO @Sim needed for lifecycle DI
64
- class MyComponent extends HTMLElement {
65
- #cartService: CartService;
66
- #subscription?: Subscription;
67
-
68
- // Lifecycle DI injection - works even without @Sim
69
- @onBeforeConnected
70
- async setupComponent(cartService: CartService) {
71
- this.#cartService = cartService;
72
-
73
- // Can be async - waits before rendering
74
- await this.#cartService.load();
75
-
76
- // Subscribe to reactive updates
77
- this.#subscription = this.#cartService.store.subscribe(state => {
78
- this.updateUI();
79
- });
80
- }
81
-
82
- disconnectedCallback() {
83
- this.#subscription?.unsubscribe();
84
- }
85
-
86
- @onConnectedInnerHtml
87
- render() {
88
- return `<div>Component ready</div>`;
89
- }
90
- }
91
- ```
92
-
93
- **Key Features:**
94
- - ✅ Services automatically injected via lifecycle decorators
95
- - ✅ **NO `@Sim` required** for lifecycle DI injection
96
- - ✅ Can be async - waits before rendering
97
- - ✅ Called once, before `connectedCallback`
98
- - ✅ Perfect for initialization and subscriptions
99
-
100
- **DI Injection Methods:**
101
-
102
- | Method | Requires `@Sim`? | Location |
103
- |--------|-----------------|----------|
104
- | Lifecycle (`@onBeforeConnected`) | ❌ NO | Lifecycle decorators |
105
- | Constructor Injection | ✅ YES | Inside `<swc-app>` only |
106
- | Static Registration | ✅ YES | `<swc-app>` DI container |
107
-
108
- **Execution Order**: Before `connectedCallback()`
109
-
110
- #### 3. `@onAfterConnected()` / `@onConnected()`
111
- Called after the component connects to the DOM. For post-render operations.
112
-
113
- ```typescript
114
- @elementDefine('my-element')
115
- class MyElement extends HTMLElement {
116
- @onAfterConnected // or @onConnected (alias)
117
- initializeAfterConnect() {
118
- console.log('Element fully connected to DOM');
119
- this.focus(); // Can interact with DOM now
120
-
121
- // Measure element size after render
122
- const rect = this.getBoundingClientRect();
123
- console.log('Size:', rect.width, rect.height);
124
- }
125
- }
126
- ```
127
-
128
- **Execution Order**: After `connectedCallback()`
129
-
130
- #### 4. `@onBeforeDisconnected()`
131
- Called before the component disconnects from the DOM. For cleanup preparation.
132
-
133
- ```typescript
134
- @elementDefine('event-listener-component')
135
- class EventListenerComponent extends HTMLElement {
136
- private resizeObserver: ResizeObserver;
137
-
138
- @onBeforeDisconnected
139
- prepareForDisconnect() {
140
- console.log('About to disconnect, saving state...');
141
- // Save state, cancel pending requests, etc.
142
- const state = {
143
- scrollPosition: window.scrollY,
144
- formData: this.getFormData()
145
- };
146
- localStorage.setItem('component-state', JSON.stringify(state));
147
- }
148
-
149
- @onAfterDisconnected
150
- cleanup() {
151
- this.resizeObserver?.disconnect();
152
- console.log('Cleanup completed');
153
- }
154
- }
155
- ```
156
-
157
- **Execution Order**: Before `disconnectedCallback()`
158
-
159
- #### 5. `@onAfterDisconnected()` / `@onDisconnected()`
160
- Called after the component disconnects from the DOM. Final cleanup.
161
-
162
- ```typescript
163
- @elementDefine('resource-component')
164
- class ResourceComponent extends HTMLElement {
165
- private timerId: number;
166
- private subscription: Subscription;
167
-
168
- @onAfterDisconnected // or @onDisconnected (alias)
169
- cleanup() {
170
- // Cancel timers
171
- clearTimeout(this.timerId);
172
-
173
- // Unsubscribe from observables
174
- this.subscription?.unsubscribe();
175
-
176
- console.log('Resources cleaned up');
177
- }
178
- }
179
- ```
180
-
181
- **Execution Order**: After `disconnectedCallback()`
182
-
183
- #### 6. `@onBeforeAdopted()`
184
- Called before the element is adopted by a new document (useful for SSR/iframe scenarios).
185
-
186
- ```typescript
187
- @elementDefine('document-aware')
188
- class DocumentAware extends HTMLElement {
189
- @onBeforeAdopted
190
- prepareForAdoption() {
191
- console.log('Adopting to new document...');
192
- // Prepare for document change
193
- }
194
- }
195
- ```
196
-
197
- #### 7. `@onAfterAdopted()` / `@onAdopted()`
198
- Called after the element is adopted by a new document.
199
-
200
- ```typescript
201
- @elementDefine('iframe-component')
202
- class IframeComponent extends HTMLElement {
203
- @onAfterAdopted // or @onAdopted (alias)
204
- onDocumentAdopted() {
205
- console.log('Now in new document context');
206
- // Re-attach to new document's resources
207
- this.initializeInNewDocument();
208
- }
209
-
210
- private initializeInNewDocument() {
211
- // Setup for new document
212
- }
213
- }
214
- ```
215
-
216
- #### 8. `@onAddEventListener()`
217
- Called when an event listener is added to the element. For event tracking/analytics.
218
-
219
- ```typescript
220
- @elementDefine('event-tracked')
221
- class EventTracked extends HTMLElement {
222
- @onAddEventListener
223
- onListenerAdded(type: string, listener: EventListener, options?: AddEventListenerOptions) {
224
- console.log(`Event listener added: ${type}`);
225
- // Track event listeners for debugging
226
- }
227
- }
228
- ```
229
-
230
- #### 9. `@onConnectedInnerHtml(options?)`
231
- Defines the component's template. Renders to Shadow DOM or Light DOM based on `useShadow` option.
232
-
233
- ```typescript
234
- @elementDefine('styled-card')
235
- class StyledCard extends HTMLElement {
236
- @onConnectedInnerHtml({ useShadow: true })
237
- render() {
238
- return `
239
- <style>
240
- :host {
241
- display: block;
242
- padding: 20px;
243
- border-radius: 8px;
244
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
245
- }
246
- </style>
247
- <div class="card-content">
248
- <slot></slot>
249
- </div>
250
- `;
251
- }
252
- }
253
- ```
254
-
255
- **Options:**
256
- - `useShadow: true` - Render to Shadow DOM (style encapsulation)
257
- - `useShadow: false` - Render to Light DOM (default)
258
-
259
- ---
260
-
261
- ### Attribute & Property Decorators
262
-
263
- #### 10. `@attributeHost(name, options?)`
264
- Bidirectional attribute binding with automatic type conversion.
265
-
266
- ```typescript
267
- @elementDefine('input-field')
268
- class InputField extends HTMLElement {
269
- @attributeHost('value', { connectedInitialize: true })
270
- value: string = '';
271
-
272
- @attributeHost('disabled')
273
- disabled: boolean = false;
274
-
275
- @attributeHost('count')
276
- count: number = 0;
277
-
278
- @onConnectedInnerHtml
279
- render() {
280
- return `
281
- <input type="text" value="${this.value}" ${this.disabled ? 'disabled' : ''}>
282
- <p>Count: ${this.count}</p>
283
- `;
284
- }
285
- }
286
- ```
287
-
288
- **Options:**
289
- - `connectedInitialize: true` - Push class value to DOM on connect
290
- - `connectedInitialize: false` - Pull DOM value on connect (default)
291
- - Auto-converts: `String`, `Number`, `Boolean`
292
-
293
- #### 11. `@changedAttributeHost(name)`
294
- Reactive handler triggered when attribute changes.
295
-
296
- ```typescript
297
- @elementDefine('reactive-input')
298
- class ReactiveInput extends HTMLElement {
299
- private internalValue = '';
300
-
301
- @changedAttributeHost('value')
302
- onValueChange(newVal: string, oldVal: string) {
303
- console.log(`Value changed: ${oldVal} → ${newVal}`);
304
- this.internalValue = newVal;
305
- this.updateDisplay();
306
- }
307
-
308
- updateDisplay() {
309
- const input = this.querySelector('input') as HTMLInputElement;
310
- if (input) input.value = this.internalValue;
311
- }
312
- }
313
- ```
314
-
315
- #### 12. `@setAttribute(selector, name, options?)`
316
- Reflect method's return value to a target element's DOM attribute. One-way binding (class → DOM).
317
-
318
- ```typescript
319
- @elementDefine('status-badge')
320
- class StatusBadge extends HTMLElement {
321
- private status: 'active' | 'inactive' | 'pending' = 'pending';
322
- private items: any[] = [];
323
-
324
- // Reflect status to host element's data-status attribute
325
- @setAttribute(':host', 'data-status')
326
- getStatus() {
327
- return this.status;
328
- }
329
-
330
- // Reflect computed data to specific element
331
- @setAttribute('#count-display', 'data-count')
332
- getItemCount() {
333
- return this.items.length.toString();
334
- }
335
-
336
- // Reflect to Shadow DOM element's aria-label
337
- @setAttribute('.badge-label', 'aria-label', { root: 'shadow' })
338
- getAriaLabel() {
339
- return `Status is ${this.status}`;
340
- }
341
-
342
- @onConnectedInnerHtml
343
- render() {
344
- return `
345
- <div data-status="${this.status}">
346
- <span class="badge-label">Status: ${this.status}</span>
347
- <span id="count-display" data-count="${this.items.length}">Items: ${this.items.length}</span>
348
- </div>
349
- `;
350
- }
351
-
352
- // Change status and update DOM attributes
353
- setStatus(newStatus: typeof this.status) {
354
- this.status = newStatus;
355
- this.getStatus(); // Updates :host[data-status]
356
- this.getItemCount(); // Updates #count-display[data-count]
357
- this.getAriaLabel(); // Updates .badge-label[aria-label]
358
- }
359
- }
360
- ```
361
-
362
- **Signature:**
363
- - `@setAttribute(selector, attributeName, options?)`
364
- - Returns method's value → reflects to target's attribute
365
-
366
- **Selectors:**
367
- - `':host'` - The component itself
368
- - `'#element-id'` - Element by ID
369
- - `'.class-name'` - Element by class
370
- - `selector` - Any CSS selector
371
-
372
- #### 12b. `@setAttributeHost(name, options?)` - Shorthand for `:host`
373
- Simplified version of `@setAttribute` that always targets `:host`.
374
-
375
- ```typescript
376
- @elementDefine('counter')
377
- class Counter extends HTMLElement {
378
- private count = 0;
379
-
380
- // Equivalent to @setAttribute(':host', 'aria-label')
381
- @setAttributeHost('aria-label')
382
- getAriaLabel() {
383
- return `Count is ${this.count}`;
384
- }
385
-
386
- @setAttributeHost('data-count')
387
- getCountValue() {
388
- return this.count.toString();
389
- }
390
-
391
- @setAttributeHost('aria-disabled')
392
- getDisabledState() {
393
- return (this.count >= 10).toString();
394
- }
395
-
396
- @onConnectedInnerHtml
397
- render() {
398
- return `
399
- <div>
400
- <p>Count: ${this.count}</p>
401
- <button swc-on-click="$host.increment()">+</button>
402
- </div>
403
- `;
404
- }
405
-
406
- increment() {
407
- this.count++;
408
- this.getAriaLabel(); // Updates :host[aria-label]
409
- this.getCountValue(); // Updates :host[data-count]
410
- this.getDisabledState(); // Updates :host[aria-disabled]
411
- }
412
- }
413
- ```
414
-
415
- **Usage:**
416
- ```typescript
417
- // Full form
418
- @setAttribute(':host', 'data-status')
419
- // vs
420
- // Shorthand for :host
421
- @setAttributeHost('data-status')
422
- ```
423
-
424
- **Key Differences:**
425
- - **`@attributeHost`** - Two-way binding: DOM ↔ Class (Bidirectional)
426
- - **`@setAttribute`** - One-way binding: Class → DOM (Any selector)
427
- - **`@setAttributeHost`** - One-way binding: Class → DOM (`:host` only)
428
- - **`@changedAttributeHost`** - Watch DOM attribute changes (Reactive handler)
429
-
430
- **Options:**
431
- - `root: 'light'` - Set attribute on light DOM element
432
- - `root: 'shadow'` - Set attribute on shadow DOM element
433
- - `root: 'auto'` - Auto-detect (default)
434
-
435
- ---
436
-
437
- ### Query & DOM Selection Decorators
438
-
439
- #### 13. `@query(selector, options?)`
440
- Inject a single element into a property.
441
-
442
- ```typescript
443
- @elementDefine('form-handler')
444
- class FormHandler extends HTMLElement {
445
- @query('#submit-btn') submitBtn?: HTMLButtonElement;
446
- @query('.form-field', { root: 'shadow' }) formField?: HTMLElement;
447
- @query(':host') self?: HTMLElement;
448
-
449
- @onConnectedInnerHtml({ useShadow: true })
450
- render() {
451
- return `
452
- <div class="form-field">
453
- <input type="text" placeholder="Enter name">
454
- <button id="submit-btn">Submit</button>
455
- </div>
456
- `;
457
- }
458
- }
459
- ```
460
-
461
- **Root Options:**
462
- - `'auto'` (default) - Search shadow DOM first, then light DOM
463
- - `'light'` - Search only light DOM
464
- - `'shadow'` - Search only shadow DOM
465
- - `'all'` - Search both, return `[lightElements, shadowElements]`
466
-
467
- **Special Targets:**
468
- ```typescript
469
- @query(':host') self?: HTMLElement; // Element itself
470
- @query(':parentHost') parent?: HTMLElement; // Parent SWC component
471
- @query(':appHost') app?: SwcAppInterface; // Nearest <swc-app>
472
- @query(':window') win?: Window; // Global window
473
- @query(':document') doc?: Document; // Global document
474
- ```
475
-
476
- #### 14. `@queryAll(selector, options?)`
477
- Inject multiple elements as an array.
478
-
479
- ```typescript
480
- @elementDefine('list-renderer')
481
- class ListRenderer extends HTMLElement {
482
- @queryAll('.item') items?: HTMLElement[];
483
- @queryAll('button', { root: 'shadow' }) buttons?: HTMLButtonElement[];
484
-
485
- getItemCount() {
486
- return this.items?.length ?? 0;
487
- }
488
- }
489
- ```
490
-
491
- #### 14b. Removing Elements via `null` or `undefined`
492
- Both `@query` and `@queryAll` support element removal by setting the property to `null` or `undefined`. This triggers automatic removal of the matching element(s).
493
-
494
- ```typescript
495
- @elementDefine('todo-list')
496
- class TodoList extends HTMLElement {
497
- @query('.notification') notification?: HTMLElement;
498
- @queryAll('.todo-item') todoItems?: HTMLElement[];
499
-
500
- removeNotification() {
501
- // Setting to null triggers automatic removal
502
- this.notification = null;
503
- }
504
-
505
- removeTodoItem(id: string) {
506
- // Find and remove a specific todo item
507
- const item = this.querySelector(`[data-id="${id}"]`);
508
- if (item) {
509
- // Setting @query to null removes the element from DOM
510
- this.notification = null;
511
- }
512
- }
513
-
514
- clearAllTodos() {
515
- // Setting @queryAll to null/undefined removes all matching elements
516
- this.todoItems = null; // Removes all .todo-item elements
517
- }
518
-
519
- @onConnectedInnerHtml
520
- render() {
521
- return `
522
- <div class="notification">New todos available!</div>
523
- <ul>
524
- <li class="todo-item" data-id="1">Task 1</li>
525
- <li class="todo-item" data-id="2">Task 2</li>
526
- <li class="todo-item" data-id="3">Task 3</li>
527
- </ul>
528
- <button swc-on-click="$host.removeNotification()">Clear Notification</button>
529
- <button swc-on-click="$host.clearAllTodos()">Clear All Tasks</button>
530
- `;
531
- }
532
- }
533
- ```
534
-
535
- **Removal Rules:**
536
- - `@query`: Setting to `null` or `undefined` removes the single element
537
- - `@queryAll`: Setting to `null` or `undefined` removes all matching elements
538
- - Special selectors (`:host`, `:window`, `:document`, etc.) are **NOT removed** (protected)
539
- - Only affects Light DOM by default; use `{ root: 'shadow' }` to remove from Shadow DOM
540
-
541
- **Use Cases:**
542
- - Close notification banners
543
- - Clear dynamic list items
544
- - Remove temporary overlays
545
- - Clean up modal dialogs
546
-
547
- ---
548
-
549
- ### Event & Messaging Decorators
550
-
551
- #### 15. `@addEventListener(selector, event, options?)`
552
- Declarative event binding with automatic delegation.
553
-
554
- ```typescript
555
- @elementDefine('todo-app')
556
- class TodoApp extends HTMLElement {
557
- private todos: any[] = [];
558
-
559
- // Direct binding to specific element
560
- @addEventListener('#add-btn', 'click')
561
- onAddTodo(event: MouseEvent) {
562
- console.log('Add button clicked');
563
- }
564
-
565
- // Event delegation for dynamic elements
566
- @addEventListener('.todo-item', 'click', { delegate: true })
567
- onTodoClick(event: MouseEvent, hostSet: any, target: HTMLElement) {
568
- const itemId = target.getAttribute('data-id');
569
- console.log('Clicked todo:', itemId);
570
- }
571
-
572
- // Global window events
573
- @addEventListener(':window', 'resize', { removeOnDisconnected: true })
574
- onWindowResize(event: Event) {
575
- console.log('Window resized');
576
- }
577
-
578
- // Parent app events
579
- @addEventListener(':appHost', 'custom-event')
580
- onCustomEvent(event: CustomEvent) {
581
- console.log('Event received:', event.detail);
582
- }
583
-
584
- @onConnectedInnerHtml
585
- render() {
586
- return `
587
- <div>
588
- <input type="text" id="todo-input" placeholder="Add todo...">
589
- <button id="add-btn">Add</button>
590
- <ul class="todo-list">
591
- ${this.todos.map(t => `
592
- <li class="todo-item" data-id="${t.id}">${t.text}</li>
593
- `).join('')}
594
- </ul>
595
- </div>
596
- `;
597
- }
598
- }
599
- ```
600
-
601
- **Features:**
602
- - ✅ Direct binding to static selectors
603
- - ✅ Event delegation for dynamic elements
604
- - ✅ Global events (`:window`, `:document`)
605
- - ✅ Parent communication (`:parentHost`, `:appHost`)
606
- - ✅ Auto-cleanup on `disconnectedCallback`
607
-
608
- #### 16. `@emitCustomEvent(target, type, options?)`
609
- Emit custom events to component hierarchy or global targets.
610
-
611
- ```typescript
612
- @elementDefine('auth-panel')
613
- class AuthPanel extends HTMLElement {
614
- constructor(private authService: AuthService) { super(); }
615
-
616
- // Emit to parent app when user logs in
617
- @emitCustomEvent(':appHost', 'user-logged-in')
618
- async onLogin() {
619
- const user = await this.authService.login();
620
- return { user, timestamp: Date.now() };
621
- }
622
-
623
- // Emit globally when logout happens
624
- @emitCustomEvent(':window', 'user-logged-out')
625
- async onLogout() {
626
- await this.authService.logout();
627
- return { timestamp: Date.now() };
628
- }
629
-
630
- // Emit to direct parent
631
- @emitCustomEvent(':parentHost', 'child-ready')
632
- notifyParent() {
633
- return { childElement: this };
634
- }
635
-
636
- @onConnectedInnerHtml
637
- render() {
638
- return `
639
- <div>
640
- <button swc-on-click="$host.onLogin()">Login</button>
641
- <button swc-on-click="$host.onLogout()">Logout</button>
642
- </div>
643
- `;
644
- }
645
- }
646
-
647
- // Listen to the emitted event
648
- @elementDefine('user-widget')
649
- class UserWidget extends HTMLElement {
650
- private user: any = null;
651
-
652
- @addEventListener(':appHost', 'user-logged-in')
653
- onUserLoggedIn(event: CustomEvent) {
654
- this.user = event.detail.user;
655
- this.render();
656
- }
657
-
658
- @onConnectedInnerHtml
659
- render() {
660
- return `<p>User: ${this.user?.name ?? 'Anonymous'}</p>`;
661
- }
662
- }
663
- ```
664
-
665
- **Emission Targets:**
666
- - `':appHost'` - Nearest parent `<swc-app>` (contained within app)
667
- - `':parentHost'` - Direct parent SWC component
668
- - `':firstHost'` / `':lastHost'` - First/last component in hierarchy
669
- - `':firstAppHost'` / `':lastAppHost'` - First/last `<swc-app>` in hierarchy
670
- - `':hosts'` - All SWC ancestors
671
- - `':appHosts'` - All `<swc-app>` ancestors
672
- - `':window'` - Global window (all apps can listen)
673
- - `':document'` - Global document
674
- - `selector` - CSS selector (emit to matching elements)
675
-
676
- **Options:**
677
- - `bubbles: true` (default) - Event bubbles up
678
- - `composed: true` (default) - Event crosses Shadow DOM boundary
679
- - `cancelable: true` - Event can be canceled
680
- - `root: 'auto' | 'light' | 'shadow'` - Where to find target elements
681
-
682
- ---
683
-
684
- ### DOM Manipulation Decorators
685
-
686
- #### 17. `@applyNode(selector, options?)`
687
- Surgically add, update, or replace DOM nodes at specific positions.
688
-
689
- ```typescript
690
- @elementDefine('counter-display')
691
- class CounterDisplay extends HTMLElement {
692
- private count = 0;
693
- private errorMessage = '';
694
-
695
- // Replace all children with new content
696
- @applyNode('#display', { position: 'replaceChildren' })
697
- updateDisplayCount() {
698
- return `<span>${this.count}</span>`;
699
- }
700
-
701
- // Append to container
702
- @applyNode('#history', { position: 'beforeEnd' })
703
- addHistoryEntry() {
704
- return `<p>Count was: ${this.count}</p>`;
705
- }
706
-
707
- // Set HTML content
708
- @applyNode('#status', { position: 'innerHtml' })
709
- setStatus() {
710
- return `<strong>Status: Active</strong>`;
711
- }
712
-
713
- // Set text content
714
- @applyNode('#counter', { position: 'innerText' })
715
- updateCounter() {
716
- return this.count.toString();
717
- }
718
-
719
- // Remove element if condition is truthy
720
- @applyNode('#error-banner', { position: 'remove' })
721
- dismissError() {
722
- return this.errorMessage === ''; // Remove if no error
723
- }
724
-
725
- @onConnectedInnerHtml
726
- render() {
727
- return `
728
- <div id="display">0</div>
729
- <div id="counter">0</div>
730
- <div id="status"></div>
731
- <button swc-on-click="$host.count++; $host.updateDisplayCount?.()">+</button>
732
- <div id="history"></div>
733
- <div id="error-banner" style="display: ${this.errorMessage ? 'block' : 'none'};"></div>
734
- `;
735
- }
736
- }
737
- ```
738
-
739
- **Position Options:**
740
- - `'beforeEnd'` (default) - Append to end
741
- - `'afterBegin'` - Prepend to start
742
- - `'beforeBegin'` - Insert before element
743
- - `'afterEnd'` - Insert after element
744
- - `'replace'` - Replace entire element
745
- - `'replaceChildren'` - Replace all children
746
- - `'innerHtml'` - Set `innerHTML`
747
- - `'innerText'` - Set `innerText`
748
- - `'remove'` - Remove element if returned value is truthy
749
-
750
- **Root Option:**
751
- Control which DOM tree is searched with the `root` option:
752
- - `'auto'` (default) - Search shadow DOM first, then light DOM
753
- - `'shadow'` - Search only shadow DOM
754
- - `'light'` - Search only light DOM
755
- - `'all'` - Search all ancestors
756
-
757
- #### Convenience Decorators
758
-
759
- Shorthand decorators for common position-specific operations:
760
-
761
- **Position-Specific Variants:**
762
- ```typescript
763
- @applyBeforeBeginNode('#target') prependTarget() { return `<div>Before</div>`; }
764
- @applyAfterBeginNode('#target') afterBeginTarget() { return `<div>After Begin</div>`; }
765
- @applyBeforeEndNode('#target') beforeEndTarget() { return `<div>Before End</div>`; }
766
- @applyAfterEndNode('#target') afterEndTarget() { return `<div>After</div>`; }
767
- @applyReplaceNode('#target') replaceTarget() { return `<div>Replaced</div>`; }
768
- @applyReplaceChildrenNode('#target') replaceChildrenTarget() { return `<div>New Children</div>`; }
769
- @applyInnerHtmlNode('#target') setInnerHtml() { return `<b>Bold</b>`; }
770
- @applyInnerTextNode('#target') setInnerText() { return 'Plain text'; }
771
- @applyRemoveNode('#target') removeTarget() { return true; }
772
- ```
773
-
774
- **Host-Targeted Variants (`:host` selector):**
775
- ```typescript
776
- @applyBeforeBeginNodeHost() prependHost() { return `<div>Before Host</div>`; }
777
- @applyAfterBeginNodeHost() afterBeginHost() { return `<div>After Begin</div>`; }
778
- @applyBeforeEndNodeHost() beforeEndHost() { return `<div>Before End</div>`; }
779
- @applyAfterEndNodeHost({ root: 'shadow' }) afterEndHost() { return `<div>After Host</div>`; }
780
- @applyReplaceNodeHost() replaceHost() { return `<div>Replaced Host</div>`; }
781
- @applyReplaceChildrenNodeHost() replaceChildrenHost() { return `<div>Shadow Children</div>`; }
782
- @applyInnerHtmlNodeHost() setHostInnerHtml() { return `<b>Host Content</b>`; }
783
- @applyInnerTextNodeHost() setHostInnerText() { return 'Host text'; }
784
- @applyRemoveNodeHost() removeHost() { return this.shouldRemove; }
785
- ```
786
-
787
- ---
788
-
789
- #### 18. `@setStyle(selector, options?)` / `@updateStyle(selector, options?)` / `@removeStyle(selector, options?)`
790
-
791
- Manipulate CSS styles on elements with full Shadow DOM support.
792
-
793
- **Core Features:**
794
- - `@setStyle` - Set inline style (replaces all)
795
- - `@updateStyle` - Merge with existing styles
796
- - `@removeStyle` - Remove specific properties
797
- - `root` option for Shadow DOM / Light DOM targeting
798
- - Support for multiple elements with `querySelectorAll`
799
-
800
- ```typescript
801
- @elementDefine('styled-component')
802
- class StyledComponent extends HTMLElement {
803
- private isActive = false;
804
-
805
- @setStyle('.button', { root: 'shadow' })
806
- setPrimaryButtonStyle() {
807
- return {
808
- backgroundColor: '#0066cc',
809
- color: 'white',
810
- padding: '10px 20px',
811
- border: 'none',
812
- borderRadius: '4px'
813
- };
814
- }
815
-
816
- @updateStyle('.button', { root: 'shadow' })
817
- highlightButton() {
818
- return {
819
- boxShadow: '0 0 10px rgba(0, 102, 204, 0.5)',
820
- transform: 'scale(1.05)'
821
- };
822
- }
823
-
824
- @removeStyle('.button', { root: 'shadow' })
825
- clearHighlight() {
826
- return ['boxShadow', 'transform'];
827
- }
828
-
829
- @onConnectedInnerHtml({ useShadow: true })
830
- render() {
831
- return `
832
- <style>
833
- .button { cursor: pointer; transition: all 0.2s; }
834
- </style>
835
- <button class="button">Styled</button>
836
- `;
837
- }
838
- }
839
- ```
840
-
841
- **Host-Targeted Variants:**
842
- ```typescript
843
- @setStyleHost({ root: 'shadow' })
844
- setHostStyle() {
845
- return { display: 'flex', gap: '10px' };
846
- }
847
-
848
- @updateStyleHost({ root: 'shadow' })
849
- updateHostStyle() {
850
- return { backgroundColor: '#f0f0f0' };
851
- }
852
-
853
- @removeStyleHost({ root: 'shadow' })
854
- clearHostStyle() {
855
- return ['backgroundColor'];
856
- }
857
- ```
858
-
859
- ---
860
-
861
- #### 19. `@setClassList(selector, options?)` / `@addClassList(selector, options?)` / `@removeClassList(selector, options?)` / `@toggleClassList(selector, options?)` / `@updateClassList(selector, options?)`
862
-
863
- Manipulate CSS classes on elements declaratively with Shadow DOM support.
864
-
865
- **Core Features:**
866
- - `@addClassList` - Add classes
867
- - `@removeClassList` - Remove classes
868
- - `@toggleClassList` - Toggle classes (returns boolean or bool array)
869
- - `@setClassList` - Replace all classes
870
- - `@updateClassList` - Merge with existing classes
871
- - `root` option for DOM layer targeting
872
- - Support for multiple elements
873
-
874
- ```typescript
875
- @elementDefine('tab-component')
876
- class TabComponent extends HTMLElement {
877
- activeTab = 'home';
878
-
879
- @addClassList('.tab', { root: 'shadow' })
880
- addTabClasses() {
881
- return ['interactive', 'focusable'];
882
- }
883
-
884
- @toggleClassList('.tab', { root: 'shadow' })
885
- toggleActiveTab() {
886
- return this.activeTab === 'home'; // Returns boolean for all matched elements
887
- }
888
-
889
- @removeClassList('.tab-inactive', { root: 'shadow' })
890
- removeInactiveClass() {
891
- return ['disabled'];
892
- }
893
-
894
- @setClassList('.tab-pane', { root: 'shadow' })
895
- updateTabPane() {
896
- return [`pane-${this.activeTab}`, 'content'];
897
- }
898
-
899
- @onConnectedInnerHtml({ useShadow: true })
900
- render() {
901
- return `
902
- <style>
903
- .tab { cursor: pointer; padding: 10px; }
904
- .tab.active { border-bottom: 2px solid blue; }
905
- </style>
906
- <div class="tabs">
907
- <div class="tab" swc-on-click="$host.activeTab='home'; $host.toggleActiveTab?.();">Home</div>
908
- <div class="tab" swc-on-click="$host.activeTab='about'; $host.toggleActiveTab?.();">About</div>
909
- </div>
910
- <div class="tab-pane">Content here</div>
911
- `;
912
- }
913
- }
914
- ```
915
-
916
- **Host-Targeted Variants:**
917
- ```typescript
918
- @addClassListHost({ root: 'shadow' })
919
- addHostClasses() {
920
- return ['component', 'interactive'];
921
- }
922
-
923
- @removeClassListHost({ root: 'shadow' })
924
- removeHostClasses() {
925
- return ['loading'];
926
- }
927
-
928
- @toggleClassListHost({ root: 'shadow' })
929
- toggleHostClass() {
930
- return this.isOpen; // Apply 'open' class if true
931
- }
932
-
933
- @setClassListHost({ root: 'shadow' })
934
- setHostClasses() {
935
- return this.isOpen ? ['open', 'expanded'] : ['closed'];
936
- }
937
-
938
- @updateClassListHost({ root: 'shadow' })
939
- updateHostClasses() {
940
- return ['visible'];
941
- }
942
- ```
943
-
944
- ---
945
-
946
- ### Dependency Injection & Singleton
947
-
948
- #### 20. `@Sim`
949
- Marks a class as a **Singleton** in the `<swc-app>` DI container for **constructor injection**.
950
-
951
- **Two DI Mechanisms:**
952
-
953
- 1. **Lifecycle DI** (`@onBeforeConnected`, etc.) - ❌ NO `@Sim` needed
954
- - Works on any `@elementDefine` component
955
- - Services injected via method parameters
956
- - Can be used outside `<swc-app>`
957
-
958
- 2. **Constructor DI** - ✅ `@Sim` REQUIRED
959
- - Only works inside `<swc-app>` boundary
960
- - Both provider AND consumer need `@Sim`
961
- - Singleton pattern within the app
962
-
963
- **Example: Both Approaches**
964
-
965
- ```typescript
966
- // ============ SERVICE ============
967
- @Sim
968
- class CartService {
969
- private items: any[] = [];
970
-
971
- addItem(item: any) {
972
- this.items.push(item);
973
- }
974
-
975
- getItems() {
976
- return this.items;
977
- }
978
- }
979
-
980
- // ============ APPROACH 1: Lifecycle DI (NO @Sim needed) ============
981
- @elementDefine('cart-header') // ← NO @Sim
982
- class CartHeader extends HTMLElement {
983
- #cartService: CartService;
984
-
985
- @onBeforeConnected
986
- async setup(cartService: CartService) { // ← Injected here
987
- this.#cartService = cartService;
988
- await this.#cartService.load?.();
989
- }
990
-
991
- @onConnectedInnerHtml
992
- render() {
993
- return `<div>Cart: ${this.#cartService?.getItems().length}</div>`;
994
- }
995
- }
996
-
997
- // ============ APPROACH 2: Constructor DI (NEEDS @Sim) ============
998
- @Sim // ← Required for constructor injection
999
- @elementDefine('cart-component')
1000
- class CartComponent extends HTMLElement {
1001
- // Constructor injection - CartService is auto-injected
1002
- constructor(private cartService: CartService) { super(); }
1003
-
1004
- @onConnectedInnerHtml
1005
- render() {
1006
- const itemCount = this.cartService.getItems().length;
1007
- return `<div>Items: ${itemCount}</div>`;
1008
- }
1009
- }
1010
-
1011
- // ============ ROUTING & BOOTSTRAP ============
1012
- @Sim
1013
- @Router({
1014
- path: '/',
1015
- route: {
1016
- '/home': HomePage,
1017
- '/cart': CartComponent // Constructor DI works here
1018
- }
1019
- })
1020
- @elementDefine('app-router')
1021
- class AppRouter extends HTMLElement {
1022
- @onConnectedInnerHtml
1023
- render() {
1024
- return `<div><slot></slot></div>`;
1025
- }
1026
- }
1027
-
1028
- // ============ HTML ============
1029
- <!-- Lifecycle DI works OUTSIDE <swc-app> -->
1030
- <cart-header></cart-header>
1031
-
1032
- <!-- Constructor DI works INSIDE <swc-app> -->
1033
- <swc-app swc-get-application-config="appConfig()">
1034
- <app-router></app-router>
1035
- </swc-app>
1036
-
1037
- <script>
1038
- window.appConfig = () => ({
1039
- rootRouter: AppRouter,
1040
- routeType: 'element',
1041
- path: '/'
1042
- });
1043
- </script>
1044
- ```
1045
-
1046
- **When to Use Each:**
1047
-
1048
- | Scenario | Use | Mark |
1049
- |----------|-----|------|
1050
- | Component needs services anywhere | Lifecycle DI in `@onBeforeConnected` | NO `@Sim` |
1051
- | Component inside `<swc-app>` route | Constructor DI | YES `@Sim` |
1052
- | Service/Singleton registration | Register in DI container | YES `@Sim` |
1053
- | Standalone component | Lifecycle DI | NO `@Sim` |
1054
-
1055
- **Key Points:**
1056
- - `@Sim` = Mark as singleton for **constructor injection inside `<swc-app>`**
1057
- - Lifecycle DI = Inject via method parameters (works everywhere, no `@Sim`)
1058
- - Each `<swc-app>` has its own isolated DI container
1059
- - Services are local to their `<swc-app>` instance (for constructor DI)
1060
-
1061
- ---
1062
-
1063
- ## Core Decorators
1064
-
1065
- #### Root Options
1066
- The `root` option controls which DOM tree is searched:
1067
- - **`'auto'`** (default): Searches shadow DOM first, then light DOM
1068
- - **`'light'`**: Searches only light DOM
1069
- - **`'shadow'`**: Searches only shadow DOM
1070
- - **`'all'`**: Searches all ancestors, returning `[lightNodes, shadowNodes]` array
1071
-
1072
- #### Usage with Special Targets:
1073
- Special targets (starting with `:`) navigate the hierarchy and environment.
1074
- ```typescript
1075
- @query(':host') self!: HTMLElement;
1076
- @query(':parentHost') parent!: HTMLElement;
1077
- @query(':appHost') app!: SwcAppInterface;
1078
- @query(':window') win!: Window;
1079
- @query(':document') doc!: Document;
1080
- @queryAll(':hosts') ancestors!: HTMLElement[];
1081
- ```
1082
-
1083
- #### Usage with Selectors & Root Options:
1084
- Standard CSS selectors support precise root control.
1085
- ```typescript
1086
- // root options: 'light' | 'shadow' | 'all' | 'auto' (default)
1087
- @query('.btn', { root: 'shadow' }) shadowBtn!: HTMLButtonElement;
1088
- ```
1089
-
1090
- ### 5. Event Binding (`@addEventListener`)
1091
-
1092
- Bind events declaratively with built-in **Event Delegation**, **Auto-Cleanup**, and **Context Injection**. Events flow through the component hierarchy with full access to parent scopes.
1093
-
1094
- #### Core Features:
1095
- - **Direct Binding**: Bind to static selectors present at initialization
1096
- - **Event Delegation**: Automatically handle dynamically added elements
1097
- - **Auto-Cleanup**: Automatically remove listeners on `disconnectedCallback`
1098
- - **Context Injection**: Receive `$host`, `$parentHost`, `$appHost` automatically
1099
- - **Special Targets**: `:window`, `:document`, `:parentHost`, `:appHost`
1100
-
1101
- #### 6. Surgical DOM Manipulation (`@applyNode`)
1102
-
1103
- Surgically add, prepend, insert, or replace nodes with a single decorator. Perfect for dynami content updates without imperatively manipulating the DOM.
1104
-
1105
- ```typescript
1106
- @elementDefine('list-manager')
1107
- class ListManager extends HTMLElement {
1108
- @applyNode('#list') addItem() {
1109
- return '<li>New Item</li>';
1110
- }
1111
-
1112
- @applyNode('#header', { position: 'afterBegin' }) prependTitle() {
1113
- return '<h1>Title</h1>';
1114
- }
1115
-
1116
- @applyNode('#item', { position: 'replace' }) replaceItem() {
1117
- return '<div>Replaced Content</div>';
1118
- }
1119
- }
1120
- ```
1121
-
1122
- **Positions:**
1123
- - `'beforeEnd'` (default): Append to end of target
1124
- - `'afterBegin'`: Prepend to start of target
1125
- - `'beforeBegin'`: Insert before target
1126
- - `'afterEnd'`: Insert after target
1127
- - `'replace'`: Replace entire target element
1128
- - `'replaceChildren'`: Replace all children of target element
1129
- - `'innerHtml'`: Set target's `innerHTML` with string content
1130
- - `'innerText'`: Set target's `innerText` with string content
1131
-
1132
- ---
1133
-
1134
- ### 6. DOM Manipulation (`@applyNode`)
1135
-
1136
- Surgically add, prepend, insert, or replace nodes. Perfect for reactive updates without full re-renders.
1137
-
1138
- #### Pattern 1: Direct Binding & Static Selectors
1139
-
1140
- ```typescript
1141
- @Sim
1142
- @elementDefine('form-handler')
1143
- class FormHandler extends HTMLElement {
1144
- @query('#submit-btn') submitBtn?: HTMLButtonElement;
1145
- @query('#form') form?: HTMLFormElement;
1146
-
1147
- // Bind to a specific static element
1148
- @addEventListener('#submit-btn', 'click')
1149
- onSubmit(event: MouseEvent) {
1150
- event.preventDefault();
1151
- console.log('Form submitted');
1152
- this.form?.submit();
1153
- }
1154
-
1155
- // Bind to multiple elements with selector
1156
- @addEventListener('input[type="text"]', 'input')
1157
- onTextInput(event: Event) {
1158
- console.log('Input value:', (event.target as HTMLInputElement).value);
1159
- }
1160
-
1161
- @onConnectedInnerHtml
1162
- render() {
1163
- return `
1164
- <form id="form">
1165
- <input type="text" placeholder="Enter name">
1166
- <input type="email" placeholder="Enter email">
1167
- <button id="submit-btn">Submit</button>
1168
- </form>
1169
- `;
1170
- }
1171
- }
1172
- ```
1173
-
1174
- #### Pattern 2: Event Delegation (Dynamic Content)
1175
-
1176
- Automatically handle elements added **after component initialization**.
1177
-
1178
- ```typescript
1179
- @Sim
1180
- @elementDefine('todo-app')
1181
- class TodoApp extends HTMLElement {
1182
- private todos: any[] = [];
1183
-
1184
- constructor(private todoService: TodoService) { super(); }
1185
-
1186
- // Delegate: Handles all current AND future .todo-item elements
1187
- @addEventListener('.todo-item', 'click', { delegate: true })
1188
- onTodoClick(event: MouseEvent, hostSet: any, target: HTMLElement) {
1189
- const todoId = target.getAttribute('data-id');
1190
- console.log('Clicked todo:', todoId);
1191
- this.selectTodo(todoId);
1192
- }
1193
-
1194
- // Event delegation with checkbox - even newly added items react
1195
- @addEventListener('.todo-checkbox', 'change', { delegate: true })
1196
- onTodoToggle(event: Event, hostSet: any, target: HTMLInputElement) {
1197
- const todoId = target.closest('.todo-item')?.getAttribute('data-id');
1198
- const isChecked = target.checked;
1199
- console.log(`Todo ${todoId} ${isChecked ? 'completed' : 'uncompleted'}`);
1200
- }
1201
-
1202
- async addTodo(text: string) {
1203
- const newTodo = await this.todoService.create(text);
1204
- this.todos.push(newTodo);
1205
- this.render();
1206
- // New .todo-item elements automatically get event listeners!
1207
- }
1208
-
1209
- selectTodo(id: string) {
1210
- const todo = this.todos.find(t => t.id === id);
1211
- console.log('Selected:', todo);
1212
- }
1213
-
1214
- @onConnectedInnerHtml
1215
- render() {
1216
- return `
1217
- <div>
1218
- <input type="text" placeholder="Add todo">
1219
- <button swc-on-click="$host.addTodo(this.previousElementSibling.value)">Add</button>
1220
- <ul>
1221
- ${this.todos.map(t => `
1222
- <li class="todo-item" data-id="${t.id}">
1223
- <input type="checkbox" class="todo-checkbox" ${t.done ? 'checked' : ''}>
1224
- <span>${t.text}</span>
1225
- </li>
1226
- `).join('')}
1227
- </ul>
1228
- </div>
1229
- `;
1230
- }
1231
- }
1232
- ```
1233
-
1234
- #### Pattern 3: Global Events with Context Access
1235
-
1236
- ```typescript
1237
- @Sim
1238
- @elementDefine('responsive-layout')
1239
- class ResponsiveLayout extends HTMLElement {
1240
- private isMobile = false;
1241
-
1242
- // Listen to window resize with auto-cleanup on disconnect
1243
- @addEventListener(':window', 'resize', { removeOnDisconnected: true })
1244
- onWindowResize() {
1245
- this.isMobile = window.innerWidth < 768;
1246
- this.updateLayout();
1247
- }
1248
-
1249
- // Listen to global scroll with host context
1250
- @addEventListener(':window', 'scroll', { removeOnDisconnected: true })
1251
- onScroll(event: Event, { $appHost }: any) {
1252
- const scrollPos = window.scrollY;
1253
- console.log('Scrolled in app:', $appHost?.tagName, 'Position:', scrollPos);
1254
- }
1255
-
1256
- updateLayout() {
1257
- this.className = this.isMobile ? 'mobile-layout' : 'desktop-layout';
1258
- }
1259
-
1260
- @onConnectedInnerHtml
1261
- render() {
1262
- return `<div class="${this.isMobile ? 'mobile' : 'desktop'}"><slot></slot></div>`;
1263
- }
1264
- }
1265
- ```
1266
-
1267
- ### 7. Dynamic Content Updates with `@applyNode`
1268
-
1269
- #### Advanced Patterns
1270
-
1271
- **Pattern 1: Simple Content Replacement with `replaceChildren`**
1272
-
1273
- ```typescript
1274
- @Sim
1275
- @elementDefine('search-results')
1276
- class SearchResults extends HTMLElement {
1277
- private results: any[] = [];
1278
- private isLoading = false;
1279
-
1280
- // Replace #results-container with new search results
1281
- @replaceChildren('#results-container')
1282
- async displayResults(query: string) {
1283
- this.isLoading = true;
1284
- const startTime = Date.now();
1285
-
1286
- try {
1287
- this.results = await fetch(`/api/search?q=${query}`)
1288
- .then(r => r.json());
1289
-
1290
- return htmlFragment`
1291
- <div class="results-header">
1292
- <p>Found ${this.results.length} results in ${Date.now() - startTime}ms</p>
1293
- </div>
1294
- <div class="results-list">
1295
- ${this.results.map(item => `
1296
- <div class="result-item" data-id="${item.id}">
1297
- <h3>${item.title}</h3>
1298
- <p>${item.description}</p>
1299
- <a href="${item.url}">View</a>
1300
- </div>
1301
- `).join('')}
1302
- </div>
1303
- `;
1304
- } catch (error) {
1305
- return htmlFragment`<p class="error">Search failed: ${error.message}</p>`;
1306
- }
1307
- }
1308
-
1309
- @onConnectedInnerHtml
1310
- render() {
1311
- return `
1312
- <div>
1313
- <input type="text" class="search-input" placeholder="Search...">
1314
- <button swc-on-click="$host.search($el.previousElementSibling.value)">Search</button>
1315
- <div id="results-container">
1316
- <p>Enter search term...</p>
1317
- </div>
1318
- </div>
1319
- `;
1320
- }
1321
-
1322
- search(query: string) {
1323
- if (query.trim()) {
1324
- this.displayResults(query);
1325
- }
1326
- }
1327
- }
1328
- ```
1329
-
1330
- **Pattern 2: Setting HTML Content**
1331
-
1332
- ```typescript
1333
- @applyNode('#message', { position: 'innerHtml' })
1334
- displayMessage(text: string) {
1335
- return `<p style="color: blue;">${text}</p>`;
1336
- }
1337
-
1338
- @applyNode('#count', { position: 'innerText' })
1339
- updateCounter() {
1340
- return this.count.toString();
1341
- }
1342
- ```
1343
-
1344
- **Pattern 3: Tab Content Switching
1345
-
1346
- ```typescript
1347
- @Sim
1348
- @elementDefine('tabbed-panel')
1349
- class TabbedPanel extends HTMLElement {
1350
- private activeTab = 'overview';
1351
- private tabData: any = {
1352
- overview: '<h3>Overview</h3><p>General information...</p>',
1353
- details: '<h3>Details</h3><p>Detailed specifications...</p>',
1354
- reviews: '<h3>Reviews</h3><p>Customer reviews...</p>'
1355
- };
1356
-
1357
- // Click tab and automatically replace content
1358
- @replaceChildren('.tab-content')
1359
- switchTab(tabName: string) {
1360
- this.activeTab = tabName;
1361
-
1362
- // Update tab buttons visual state
1363
- Array.from(this.querySelectorAll('.tab-btn')).forEach(btn => {
1364
- btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
1365
- });
1366
-
1367
- // Return new content
1368
- return htmlFragment`${this.tabData[tabName]}`;
1369
- }
1370
-
1371
- @onConnectedInnerHtml
1372
- render() {
1373
- return `
1374
- <div class="tabs">
1375
- <button class="tab-btn active" data-tab="overview"
1376
- swc-on-click="$host.switchTab('overview')">Overview</button>
1377
- <button class="tab-btn" data-tab="details"
1378
- swc-on-click="$host.switchTab('details')">Details</button>
1379
- <button class="tab-btn" data-tab="reviews"
1380
- swc-on-click="$host.switchTab('reviews')">Reviews</button>
1381
- </div>
1382
- <div class="tab-content">${this.tabData.overview}</div>
1383
- `;
1384
- }
1385
- }
1386
- ```
1387
-
1388
- ### 8. `@appendChild(selector, options?)`
1389
-
1390
- Appends or inserts the method's return value relative to a target element. Perfect for dynamically building lists, stacking notifications, or adding children without replacing existing content.
1391
-
1392
- **Position Options:**
1393
- - **`'lastChild'`** (default): Append to end
1394
- - **`'firstChild'`**: Prepend to beginning
1395
- - **`'nextSibling'`**: Insert after element
1396
- - **`'previousSibling'`**: Insert before element
1397
-
1398
- #### Pattern 1: Dynamic List Building
1399
-
1400
- ```typescript
1401
- @Sim
1402
- @elementDefine('task-list')
1403
- class TaskList extends HTMLElement {
1404
- private tasks: any[] = [];
1405
- private taskIdCounter = 0;
1406
-
1407
- // Append new task to bottom of list
1408
- @appendChild('#task-list', { position: 'lastChild' })
1409
- addTask(title: string, priority: 'low' | 'medium' | 'high' = 'medium') {
1410
- const id = ++this.taskIdCounter;
1411
- const bgColor = { low: '#e8f5e9', medium: '#fff3e0', high: '#ffebee' }[priority];
1412
-
1413
- return htmlFragment`
1414
- <div class="task-item" data-id="${id}" style="background: ${bgColor}; padding: 12px; margin: 8px 0; border-radius: 4px;">
1415
- <div class="task-header">
1416
- <input type="checkbox" class="task-checkbox" data-id="${id}">
1417
- <span class="task-title">${title}</span>
1418
- <span class="task-priority">${priority.toUpperCase()}</span>
1419
- </div>
1420
- <button class="task-delete" swc-on-click="$host.deleteTask(${id})">🗑️ Delete</button>
1421
- </div>
1422
- `;
1423
- }
1424
-
1425
- // Prepend urgent task to top of list
1426
- @appendChild('#task-list', { position: 'firstChild' })
1427
- addUrgentTask(title: string) {
1428
- return htmlFragment`
1429
- <div class="task-item urgent" style="background: #ffcdd2; padding: 12px; border: 2px solid #f44336;">
1430
- <strong>🚨 URGENT: ${title}</strong>
1431
- <button swc-on-click="$host.deleteTask()">Close</button>
1432
- </div>
1433
- `;
1434
- }
1435
-
1436
- deleteTask(id?: number) {
1437
- console.log('Delete task:', id);
1438
- }
1439
-
1440
- @onConnectedInnerHtml
1441
- render() {
1442
- return `
1443
- <div>
1444
- <div class="task-input">
1445
- <input type="text" id="new-task" placeholder="Enter task...">
1446
- <select id="priority">
1447
- <option value="low">Low</option>
1448
- <option value="medium" selected>Medium</option>
1449
- <option value="high">High</option>
1450
- </select>
1451
- <button swc-on-click="
1452
- const input = document.getElementById('new-task');
1453
- const priority = document.getElementById('priority').value;
1454
- if (priority === 'high') {
1455
- $host.addUrgentTask(input.value);
1456
- } else {
1457
- $host.addTask(input.value, priority);
1458
- }
1459
- input.value = '';
1460
- input.focus();
1461
- ">Add Task</button>
1462
- </div>
1463
- <div id="task-list"></div>
1464
- </div>
1465
- `;
1466
- }
1467
- }
1468
- ```
1469
-
1470
- #### Pattern 2: Notification Stack
1471
-
1472
- ```typescript
1473
- @Sim
1474
- @elementDefine('notification-stack')
1475
- class NotificationStack extends HTMLElement {
1476
- private notificationId = 0;
1477
-
1478
- // Append notifications to stack (they auto-dismiss)
1479
- @appendChild('.notifications', { position: 'lastChild' })
1480
- notify(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info', duration = 5000) {
1481
- const id = ++this.notificationId;
1482
- const bgColor = { info: '#e3f2fd', success: '#e8f5e9', warning: '#fff3e0', error: '#ffebee' }[type];
1483
- const borderColor = { info: '#2196f3', success: '#4caf50', warning: '#ff9800', error: '#f44336' }[type];
1484
-
1485
- // Auto-remove after duration
1486
- setTimeout(() => {
1487
- const el = document.querySelector(`[data-notify-id="${id}"]`);
1488
- if (el) el.remove();
1489
- }, duration);
1490
-
1491
- return htmlFragment`
1492
- <div class="notification" data-notify-id="${id}"
1493
- style="background: ${bgColor}; border-left: 4px solid ${borderColor}; padding: 16px; margin: 8px 0; border-radius: 4px; display: flex; justify-content: space-between;">
1494
- <span>${message}</span>
1495
- <button swc-on-click="this.closest('[data-notify-id]').remove()"
1496
- style="background: none; border: none; cursor: pointer; font-size: 18px;">×</button>
1497
- </div>
1498
- `;
1499
- }
1500
-
1501
- @onConnectedInnerHtml
1502
- render() {
1503
- return `
1504
- <div class="notifications" style="position: fixed; top: 20px; right: 20px; width: 300px; z-index: 1000;"></div>
1505
- `;
1506
- }
1507
- }
1508
-
1509
- // Usage
1510
- const notifier = document.querySelector('notification-stack');
1511
- notifier.notify('Operation successful!', 'success');
1512
- notifier.notify('Something went wrong', 'error');
1513
- ```
1514
-
1515
- ---
1516
-
1517
- ## 🛠 Template Utilities
1518
-
1519
- SWC provides powerful utilities to create DOM nodes using **Tagged Template Literals**. These are perfect for use with surgical decorators like `@replaceChildren`, `@appendChild`, and `@replaceChildren`.
1520
-
1521
- ### Complete Factory List
1522
-
1523
- SWC provides element factories for **all common HTML tags**:
1524
-
1525
- ```typescript
1526
- import {
1527
- htmlFragment, textNode,
1528
- htmlDivElement, htmlSpanElement, htmlButtonElement,
1529
- htmlInputElement, htmlFormElement,
1530
- htmlTableElement, htmlTrElement, htmlTdElement,
1531
- htmlUlElement, htmlLiElement,
1532
- htmlHeadingElement, htmlImageElement,
1533
- // ... 50+ more factories
1534
- } from '@dooboostore/simple-web-component';
1535
- ```
1536
-
1537
- ### 1. `htmlFragment` - DOM Fragments
1538
-
1539
- Returns a `DocumentFragment` from an HTML string. Perfect for multi-element insertion.
1540
-
1541
- ```typescript
1542
- @Sim
1543
- @elementDefine('fragment-demo')
1544
- class FragmentDemo extends HTMLElement {
1545
- @replaceChildren('#container')
1546
- renderMultipleElements() {
1547
- // All elements wrapped in one fragment for efficient insertion
1548
- return htmlFragment`
1549
- <h2>Title</h2>
1550
- <p>First paragraph</p>
1551
- <p>Second paragraph</p>
1552
- <ul>
1553
- <li>Item 1</li>
1554
- <li>Item 2</li>
1555
- </ul>
1556
- `;
1557
- }
1558
-
1559
- @onConnectedInnerHtml
1560
- render() {
1561
- return `<div id="container"></div>`;
1562
- }
1563
- }
1564
- ```
1565
-
1566
- ### 2. `textNode` - Pure Text Nodes
1567
-
1568
- Create raw text nodes (useful for escaping HTML or building text content).
1569
-
1570
- ```typescript
1571
- @Sim
1572
- @elementDefine('text-renderer')
1573
- class TextRenderer extends HTMLElement {
1574
- @appendChild('.text-list', { position: 'lastChild' })
1575
- addPlainText(userInput: string) {
1576
- // Create pure text node - prevents XSS
1577
- return textNode`User wrote: ${userInput}`;
1578
- }
1579
-
1580
- @onConnectedInnerHtml
1581
- render() {
1582
- return `
1583
- <input type="text" id="user-input" placeholder="Enter text...">
1584
- <button swc-on-click="
1585
- const input = document.getElementById('user-input');
1586
- $host.addPlainText(input.value);
1587
- input.value = '';
1588
- ">Add Text</button>
1589
- <div class="text-list"></div>
1590
- `;
1591
- }
1592
- }
1593
- ```
1594
-
1595
- ### 2b. `commentNode` - HTML Comments
1596
-
1597
- Create HTML comment nodes (useful for debugging, markers, or template documentation).
1598
-
1599
- ```typescript
1600
- @elementDefine('debug-component')
1601
- class DebugComponent extends HTMLElement {
1602
- @appendChild('#debug-area')
1603
- addDebugComment(message: string) {
1604
- // Create HTML comment node
1605
- return commentNode`DEBUG: ${message} at ${new Date().toISOString()}`;
1606
- }
1607
-
1608
- @onConnectedInnerHtml
1609
- render() {
1610
- return `<div id="debug-area"></div>`;
1611
- }
1612
- }
1613
- ```
1614
-
1615
- ### 3. Element Factories (`html***Element`)
1616
-
1617
- Specific factories for common tags. Each returns a **typed element instance**.
1618
-
1619
- ```typescript
1620
- @Sim
1621
- @elementDefine('element-factory-demo')
1622
- class ElementFactoryDemo extends HTMLElement {
1623
- // htmlDivElement returns HTMLDivElement (typed)
1624
- @appendChild('#container')
1625
- addCard(title: string, description: string) {
1626
- return htmlDivElement`
1627
- <div class="card">
1628
- <h3>${title}</h3>
1629
- <p>${description}</p>
1630
- <button class="card-btn">Learn More</button>
1631
- </div>
1632
- `;
1633
- }
1634
-
1635
- // htmlTableElement for building tables
1636
- @replaceChildren('#table-container')
1637
- buildTable(data: Array<{ name: string; age: number; role: string }>) {
1638
- const headerRow = htmlTrElement`
1639
- <tr style="background: #f0f0f0; font-weight: bold;">
1640
- <td>Name</td>
1641
- <td>Age</td>
1642
- <td>Role</td>
1643
- </tr>
1644
- `;
1645
-
1646
- const rows = data.map(item =>
1647
- htmlTrElement`
1648
- <tr>
1649
- <td>${item.name}</td>
1650
- <td>${item.age}</td>
1651
- <td>${item.role}</td>
1652
- </tr>
1653
- `
1654
- );
1655
-
1656
- return htmlTableElement`
1657
- ${headerRow}
1658
- ${rows}
1659
- `;
1660
- }
1661
-
1662
- // htmlUlElement for lists
1663
- @appendChild('.sidebar')
1664
- addMenuList(items: string[]) {
1665
- const listItems = items.map(item =>
1666
- htmlLiElement`<li><a href="#">${item}</a></li>`
1667
- );
1668
-
1669
- return htmlUlElement`
1670
- <ul class="menu">
1671
- ${listItems}
1672
- </ul>
1673
- `;
1674
- }
1675
-
1676
- @onConnectedInnerHtml
1677
- render() {
1678
- return `
1679
- <div id="container"></div>
1680
- <div id="table-container"></div>
1681
- <div class="sidebar"></div>
1682
- `;
1683
- }
1684
- }
1685
- ```
1686
-
1687
- ---
1688
-
1689
- ## ⚡ Quick Start
1690
-
1691
- ```ts
1692
- import 'reflect-metadata';
1693
- import swcRegister, { elementDefine, onConnectedInnerHtml } from '@dooboostore/simple-web-component';
1694
-
1695
- swcRegister(window);
1696
-
1697
- @elementDefine('hello-swc')
1698
- class HelloSwc extends HTMLElement {
1699
- @onConnectedInnerHtml({ useShadow: true })
1700
- render() {
1701
- return `<style>:host{display:block;padding:12px;background:#111;color:#fefefe}</style><slot>Hello SWC</slot>`;
1702
- }
1703
- }
1704
-
1705
- // <hello-swc> contents render automatically once the element is connected.
1706
- ```
1707
-
1708
- - Call `swcRegister(window)` once per window to register structural elements (`swc-if`, `swc-loop`, etc.) globally.
1709
- - Decorators rely on `reflect-metadata`, so make sure it is imported before defining elements.
1710
-
1711
- ---
1712
-
1713
- ## 🧪 Interactive Test Cases
1714
-
1715
- Every decorator and directive ships with runnable playgrounds under [test/case/src](test/case/src). They double as living documentation.
1716
-
1717
- 1. Install dependencies at the repo root: `pnpm install`.
1718
- 2. Start the playground server:
1719
- ```bash
1720
- cd packages/@dooboostore/simple-web-component/test/case
1721
- pnpm run dev
1722
- ```
1723
- 3. Open `http://localhost:3005` and pick a scenario from the menu.
1724
-
1725
- | Scenario | Folder | Highlights |
1726
- | --- | --- | --- |
1727
- | Lifecycle hooks | `lifecycle/` | All pre/post connect, disconnect, adopt hooks with parent-child ordering. |
1728
- | Attribute flow | `attr/` and `attribute-test/` | DOM-first attribute sync, `@setAttribute`, `@changedAttribute`, `connectedInitialize` edge cases. |
1729
- | Events | `event/` and `event-attribute/` | Direct binding, delegation, special selectors, `swc-on-*`, custom event emission. |
1730
- | Queries | `query/` | `@query`, `@queryAll`, special selectors like `:appHost`, element injection safety. |
1731
- | Structural directives | `swc-loop/`, `swc-if/`, `swc-choose/`, `swc-async/` | Surgical list handling, portal rendering, async template states. |
1732
- | SPA & DI | `spa/` | Multiple `<swc-app>` instances, DI containers, nested routers. |
1733
- | Performance | `load-test/` | 1,000+ node stress tests showcasing zero MutationObserver overhead. |
1734
-
1735
- Each folder contains an `index.ts` that demonstrates the decorator usage and an `index.html` you can load standalone if needed.
1736
-
1737
- ---
1738
-
1739
- ## 🧱 Project Layout
1740
-
1741
- - [src/](src) holds the production TypeScript sources (decorators, structural elements, router, utilities).
1742
- - [test/case/src](test/case/src) aggregates example applications used in documentation and regression checks.
1743
- - [test/case/src/README.md](test/case/src/README.md) provides an overview of every scenario along with the architectural ideas they validate.
1744
-
1745
- ---
1746
-
1747
- ## 🛠 Developer Scripts
1748
-
1749
- Inside [package.json](package.json) you will find task shortcuts:
1750
-
1751
- - `pnpm run build` – produce ESM, CJS, and bundle artifacts (`dist/`).
1752
- - `pnpm run watch` – rebuild all targets on file changes.
1753
- - `pnpm run build:esm` / `build:cjs` – individual module formats when you only care about one output.
1754
- - `pnpm run test:bundle` – launches the bundle regression playground (`@dooboostore/simple-web-component-bundle-test`).
1755
- - `pnpm run typecheck` – validate TypeScript types without emitting output.
1756
-
1757
- Publishing uses `publish:npm`, which runs a clean build, packs artifacts, and pushes them with the correct metadata.
1758
-
1759
- ---
1760
-
1761
- ## 🌐 Server-Side Rendering (SSR) & Hydration
1762
-
1763
- SWC provides seamless SSR support with automatic tag-name preservation and client-side hydration strategies.
1764
-
1765
- ### SSR Features
1766
- - **Tag Name Preservation**: Custom element tags (e.g., `<user-route>`, `<index-router>`) are preserved through the SSR virtual DOM layer.
1767
- - **Metadata-Based Registration**: The `CustomElementRegistry` stores tagname metadata during `define()`, allowing `HTMLElementBase` to auto-infer correct tag names during construction.
1768
- - **Virtual Window Support**: Pass a custom window object to `@elementDefine()` for SSR rendering environments.
1769
-
1770
- ### Hydration Modes
1771
- When connecting a server-rendered app to the client, choose a connection strategy via the `connectMode` config:
1772
-
1773
- ```typescript
1774
- const config = {
1775
- rootRouter: RootRouter,
1776
- routeType: 'element',
1777
- connectMode: 'swap' // or 'direct' (default)
1778
- };
1779
-
1780
- app.connect(config);
1781
- ```
1782
-
1783
- #### Mode Behaviors:
1784
- - **`'direct'`** (default): Directly connects the existing rendered DOM tree.
1785
- - **`'swap'`**: Creates a clone of the rendered app, connects it, and smoothly replaces the original DOM (Zero FOUC - Flash of Unstyled Content).
1786
-
1787
- The swap mode is ideal for SSR workflows where you want to:
1788
- 1. Render components on the server with proper tag names
1789
- 2. Send minimal HTML to the client
1790
- 3. Clone and connect on the client side without visible DOM flashing
1791
-
1792
- ---
1793
-
1794
- ## 🌐 Revolutionary SPA Architecture
1795
-
1796
- SWC introduces a **paradigm shift** in SPA design: **Each component scope is an independent, self-contained application** with its own DI container, router, and lifecycle. This enables true micro-frontend architectures without iframe overhead while maintaining perfect encapsulation.
1797
-
1798
- ### Core Philosophy: Multi-App Composition
1799
- Unlike traditional SPAs that force a global router and monolithic state tree, SWC allows:
1800
- - **Nested Independent Apps**: Multiple `<swc-app>` instances on the same page, each with zero cross-contamination
1801
- - **Autonomous Routing**: Each app handles its own navigation, history, and component lifecycle independently
1802
- - **Decentralized DI**: Service dependencies are resolved within app scope, enabling natural code splitting and lazy loading
1803
- - **Island Architecture**: Apps boot only when needed, reducing initial JavaScript payload
1804
-
1805
- ### ✨ Complete Commerce App Example
1806
-
1807
- See the **[Commerce Example](examples/commerce/)** for a production-ready SPA implementation featuring:
1808
-
1809
- - **Multi-page routing** (Home, Product Detail, Cart, Orders)
1810
- - **Service layer** (ProductService, CartService, OrderService) with localStorage persistence
1811
- - **Reactive state management** using BehaviorSubject
1812
- - **Modern UI design** with gradient backgrounds, responsive layouts, and sticky header
1813
- - **Product images** from dynamic CDN (picsum.photos)
1814
- - **DI integration** with `@onBeforeConnected` lifecycle hooks
1815
- - **Shadow DOM** for style encapsulation
1816
- - **Real-time updates** with cart count synchronization
1817
-
1818
- ```bash
1819
- cd packages/@dooboostore/simple-web-component/examples/commerce
1820
- pnpm install
1821
- pnpm build
1822
- ```
1823
-
1824
- This example demonstrates all decorators working together in a real-world e-commerce scenario.
1825
-
1826
- ---
1827
-
1828
- ### Advanced Patterns
1829
-
1830
- #### Isolated App Scopes & Micro-Frontend Pattern
1831
-
1832
- Create self-contained apps with `<swc-app>` where routing and DI are completely isolated:
1833
-
1834
- ```html
1835
- <div class="dashboard">
1836
- <swc-app swc-get-application-config="createUserModule()"></swc-app>
1837
- <swc-app swc-get-application-config="createReportsModule()"></swc-app>
1838
- </div>
1839
- ```
1840
-
1841
- Each app is independent: state, services, and routes don't cross boundaries.
1842
-
1843
- ---
1844
-
1845
- #### DI-Powered Routing with Lifecycle Hooks
1846
-
1847
- SWC routing injects services directly into components using `canActivate` lifecycle hook for permission checks and data preloading:
1848
-
1849
- ```typescript
1850
- // ============================================
1851
- // 1. SERVICE LAYER (Shared by all routes)
1852
- // ============================================
1853
- @Sim
1854
- class AuthService {
1855
- private user: { id: string; role: 'admin' | 'user' } | null = null;
1856
-
1857
- async login(credentials: { username: string; password: string }) {
1858
- // Simulate API call
1859
- this.user = { id: 'user123', role: 'admin' };
1860
- return this.user;
1861
- }
1862
-
1863
- async logout() {
1864
- this.user = null;
1865
- }
1866
-
1867
- getCurrentUser() {
1868
- return this.user;
1869
- }
1870
-
1871
- hasRole(role: string) {
1872
- return this.user?.role === role;
1873
- }
1874
- }
1875
-
1876
- @Sim
1877
- class UserService {
1878
- constructor(private auth: AuthService) {}
1879
-
1880
- async getUsers() {
1881
- if (!this.auth.hasRole('admin')) {
1882
- throw new Error('Unauthorized');
1883
- }
1884
- return [
1885
- { id: '1', name: 'John Doe', email: 'john@example.com' },
1886
- { id: '2', name: 'Jane Smith', email: 'jane@example.com' }
1887
- ];
1888
- }
1889
-
1890
- async getUserById(id: string) {
1891
- return { id, name: 'User ' + id, email: `user${id}@example.com` };
1892
- }
1893
- }
1894
-
1895
- // ============================================
1896
- // 2. ROUTE COMPONENTS (Nested routing)
1897
- // ============================================
1898
- @Sim
1899
- @Router({
1900
- path: '/admin',
1901
- route: {
1902
- '/users': UserListPage,
1903
- '/users/:id': UserDetailPage,
1904
- '/settings': SettingsPage
1905
- }
1906
- })
1907
- @elementDefine('admin-router')
1908
- class AdminRouter extends HTMLElement implements RouterAction.CanActivate {
1909
- constructor(private auth: AuthService) { super(); }
1910
-
1911
- @onConnectedInnerHtml({ useShadow: true })
1912
- render() {
1913
- return `
1914
- <style>
1915
- :host { display: flex; height: 100vh; }
1916
- .sidebar { width: 200px; background: #333; color: white; padding: 20px; }
1917
- .sidebar nav a { display: block; margin: 10px 0; color: #0066cc; cursor: pointer; }
1918
- .content { flex: 1; padding: 20px; }
1919
- </style>
1920
- <div class="sidebar">
1921
- <h3>Admin Panel</h3>
1922
- <nav>
1923
- <a swc-on-click="$host.router.go('/admin/users')">Users</a>
1924
- <a swc-on-click="$host.router.go('/admin/settings')">Settings</a>
1925
- </nav>
1926
- </div>
1927
- <div class="content">
1928
- <slot></slot>
1929
- </div>
1930
- `;
1931
- }
1932
-
1933
- // Lifecycle hook: Called BEFORE route transition
1934
- // Used for permission checks, data preloading, etc.
1935
- async canActivate(url: RoutingDataSet, data?: any) {
1936
- // Only admin can enter this route
1937
- if (!this.auth.hasRole('admin')) {
1938
- console.warn('Access Denied: Admin role required');
1939
- return false;
1940
- }
1941
-
1942
- // If data is a rendered component (Node), inject it into DOM
1943
- if (data instanceof Node) {
1944
- this.replaceChildren(data);
1945
- }
1946
-
1947
- return true;
1948
- }
1949
- }
1950
-
1951
- // ============================================
1952
- // 3. PAGE COMPONENTS (Leaf routes)
1953
- // ============================================
1954
- @Sim
1955
- @elementDefine('user-list-page')
1956
- class UserListPage extends HTMLElement {
1957
- private users: any[] = [];
1958
-
1959
- constructor(
1960
- private userService: UserService,
1961
- private router: Router,
1962
- private auth: AuthService
1963
- ) {
1964
- super();
1965
- }
1966
-
1967
- @onConnected
1968
- async loadUsers() {
1969
- try {
1970
- this.users = await this.userService.getUsers();
1971
- this.render();
1972
- } catch (error) {
1973
- console.error('Failed to load users:', error);
1974
- this.innerHTML = '<p style="color: red;">Failed to load users</p>';
1975
- }
1976
- }
1977
-
1978
- @classList('active')
1979
- isActive = false;
1980
-
1981
- @onConnectedInnerHtml
1982
- render() {
1983
- return `
1984
- <style>
1985
- .user-table { width: 100%; border-collapse: collapse; }
1986
- .user-table th, .user-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
1987
- .user-table tr:hover { background: #f5f5f5; }
1988
- </style>
1989
- <h2>Users</h2>
1990
- <table class="user-table">
1991
- <thead>
1992
- <tr><th>Name</th><th>Email</th><th>Actions</th></tr>
1993
- </thead>
1994
- <tbody>
1995
- ${this.users.map(u => `
1996
- <tr>
1997
- <td>${u.name}</td>
1998
- <td>${u.email}</td>
1999
- <td>
2000
- <button swc-on-click="$host.router.go('/admin/users/${u.id}')">View</button>
2001
- <button swc-on-click="$host.deleteUser('${u.id}')">Delete</button>
2002
- </td>
2003
- </tr>
2004
- `).join('')}
2005
- </tbody>
2006
- </table>
2007
- `;
2008
- }
2009
-
2010
- deleteUser(id: string) {
2011
- if (confirm('Delete this user?')) {
2012
- this.users = this.users.filter(u => u.id !== id);
2013
- this.render();
2014
- }
2015
- }
2016
- }
2017
-
2018
- @Sim
2019
- @elementDefine('user-detail-page')
2020
- class UserDetailPage extends HTMLElement {
2021
- private user: any = null;
2022
-
2023
- constructor(
2024
- private userService: UserService,
2025
- private router: Router
2026
- ) {
2027
- super();
2028
- }
2029
-
2030
- @onConnected
2031
- async loadUser() {
2032
- // Extract route parameter (:id)
2033
- const id = this.router.getIntent()?.params?.id;
2034
- if (id) {
2035
- this.user = await this.userService.getUserById(id);
2036
- this.render();
2037
- }
2038
- }
2039
-
2040
- @onConnectedInnerHtml
2041
- render() {
2042
- if (!this.user) return '<p>Loading...</p>';
2043
-
2044
- return `
2045
- <style>
2046
- .detail { max-width: 600px; background: #f9f9f9; padding: 20px; border-radius: 8px; }
2047
- .detail-row { margin: 10px 0; }
2048
- .detail-label { font-weight: bold; color: #333; }
2049
- </style>
2050
- <h2>User Details</h2>
2051
- <div class="detail">
2052
- <div class="detail-row">
2053
- <span class="detail-label">ID:</span> ${this.user.id}
2054
- </div>
2055
- <div class="detail-row">
2056
- <span class="detail-label">Name:</span> ${this.user.name}
2057
- </div>
2058
- <div class="detail-row">
2059
- <span class="detail-label">Email:</span> ${this.user.email}
2060
- </div>
2061
- </div>
2062
- <button swc-on-click="$host.router.back()">Back</button>
2063
- `;
2064
- }
2065
- }
2066
-
2067
- // ============================================
2068
- // 4. ROOT ROUTER (Entry point)
2069
- // ============================================
2070
- @Sim
2071
- @Router({
2072
- path: '/',
2073
- route: {
2074
- '/': HomePage,
2075
- '/login': LoginPage,
2076
- '/admin': AdminRouter
2077
- }
2078
- })
2079
- @elementDefine('app-router')
2080
- class RootRouter extends HTMLElement implements RouterAction.CanActivate {
2081
- constructor(private auth: AuthService) { super(); }
2082
-
2083
- @onConnectedInnerHtml({ useShadow: true })
2084
- render() {
2085
- const user = this.auth.getCurrentUser();
2086
- return `
2087
- <style>
2088
- :host { display: block; font-family: Arial, sans-serif; }
2089
- header { background: #222; color: white; padding: 15px; }
2090
- header .user-info { float: right; font-size: 12px; }
2091
- </style>
2092
- <header>
2093
- <h1>SWC App</h1>
2094
- <div class="user-info">
2095
- ${user ? `Logged in as: ${user.id} (${user.role})` : 'Not logged in'}
2096
- </div>
2097
- </header>
2098
- <main><slot></slot></main>
2099
- `;
2100
- }
2101
-
2102
- async canActivate(url: RoutingDataSet, data?: any) {
2103
- if (data instanceof Node) {
2104
- this.replaceChildren(data);
2105
- }
2106
- return true;
2107
- }
2108
- }
2109
- ```
2110
-
2111
- ---
2112
-
2113
- ### 3. Route Lifecycle & Guard Patterns
2114
-
2115
- **The `canActivate` lifecycle hook** is called **before** entering a route. Use it for:
2116
- - **Permission Checks**: Verify user roles and access rights
2117
- - **Data Preloading**: Fetch required data before rendering
2118
- - **Route Interception**: Redirect to login or error pages
2119
- - **Analytics**: Track user navigation
2120
-
2121
- ```typescript
2122
- @Router({ path: '/protected', route: { '/data': ProtectedDataPage } })
2123
- @elementDefine('protected-router')
2124
- class ProtectedRouter extends HTMLElement implements RouterAction.CanActivate {
2125
- constructor(private auth: AuthService) { super(); }
2126
-
2127
- async canActivate(url: RoutingDataSet, data?: any) {
2128
- // Guard: Redirect if not authenticated
2129
- if (!this.auth.getCurrentUser()) {
2130
- console.log('Redirecting to login...');
2131
- this.parentElement?.querySelector('app-router')?.routing('/login');
2132
- return false;
2133
- }
2134
-
2135
- // Data injection: Component receives pre-fetched data
2136
- if (data instanceof Node) {
2137
- this.replaceChildren(data);
2138
- }
2139
-
2140
- return true;
2141
- }
2142
- }
2143
- ```
2144
-
2145
- ---
2146
-
2147
- ### 4. Advanced Patterns: Nested Routers & State Management
2148
-
2149
- ```typescript
2150
- // Real-world: Multi-level nested routing with shared state
2151
- @Sim
2152
- class GridStateService {
2153
- private sortBy = 'name';
2154
- private filterRole = '';
2155
-
2156
- setSortBy(field: string) { this.sortBy = field; }
2157
- setFilterRole(role: string) { this.filterRole = role; }
2158
- getSortBy() { return this.sortBy; }
2159
- getFilterRole() { return this.filterRole; }
2160
- }
2161
-
2162
- @Sim
2163
- @Router({
2164
- path: '/crm',
2165
- route: {
2166
- '/contacts': ContactsRouter,
2167
- '/accounts': AccountsRouter
2168
- }
2169
- })
2170
- @elementDefine('crm-app')
2171
- class CRMApp extends HTMLElement implements RouterAction.CanActivate {
2172
- async canActivate(url: RoutingDataSet, data?: any) {
2173
- if (data instanceof Node) this.replaceChildren(data);
2174
- return true;
2175
- }
2176
-
2177
- @onConnectedInnerHtml
2178
- render() {
2179
- return `<div><slot></slot></div>`;
2180
- }
2181
- }
2182
- ```
2183
-
2184
- ---
2185
-
2186
- ## ✉️ Declarative & Scoped Messaging System
2187
-
2188
- SWC provides a **hierarchical messaging architecture** using native browser events with intelligent scoping. No external event bus or state manager needed—messages flow naturally through the DOM hierarchy.
2189
-
2190
- ### Message Scope Hierarchy
2191
-
2192
- Messages can target different scopes using **Special Selectors**:
2193
-
2194
- | Selector | Scope | Use Case |
2195
- |----------|-------|----------|
2196
- | **`:appHost`** | Same `<swc-app>` instance | Module-to-module communication (confined) |
2197
- | **`:parentHost`** | Direct parent SWC component | Parent-child coordination |
2198
- | **`:window` / `:document`** | Entire page | Global broadcasts (Cross-app signals) |
2199
- | **`:firstHost` / `:firstAppHost`** | Root of hierarchy | Communication with top-level container |
2200
- | **`selector`** (CSS) | Direct query result | Point-to-point messaging |
2201
-
2202
- ### Pattern 1: App-Scoped Communication
2203
-
2204
- Communicate within a micro-app without affecting siblings.
2205
-
2206
- ```typescript
2207
- // ========== SENDER COMPONENT ==========
2208
- @Sim
2209
- @elementDefine('auth-panel')
2210
- class AuthPanel extends HTMLElement {
2211
- constructor(private auth: AuthService) { super(); }
2212
-
2213
- @onConnectedInnerHtml
2214
- render() {
2215
- return `
2216
- <div style="padding: 10px; background: #f0f0f0; border-radius: 4px;">
2217
- <button swc-on-click="$host.onLogin()">Login</button>
2218
- <button swc-on-click="$host.onLogout()">Logout</button>
2219
- </div>
2220
- `;
2221
- }
2222
-
2223
- // Emit event to the nearest parent <swc-app>
2224
- @emitCustomEvent(':appHost', 'auth-changed')
2225
- async onLogin() {
2226
- await this.auth.login({ username: 'demo', password: 'demo' });
2227
- return { user: this.auth.getCurrentUser(), timestamp: Date.now() };
2228
- }
2229
-
2230
- @emitCustomEvent(':appHost', 'auth-changed')
2231
- async onLogout() {
2232
- await this.auth.logout();
2233
- return { user: null, timestamp: Date.now() };
2234
- }
2235
- }
2236
-
2237
- // ========== RECEIVER COMPONENT (SAME APP) ==========
2238
- @Sim
2239
- @elementDefine('user-widget')
2240
- class UserWidget extends HTMLElement {
2241
- private user: any = null;
2242
-
2243
- @onConnectedInnerHtml
2244
- render() {
2245
- return `
2246
- <div style="padding: 10px; background: #e8f4f8; border-radius: 4px;">
2247
- ${this.user ? `<p>Welcome, ${this.user.name}!</p>` : '<p>Not logged in</p>'}
2248
- </div>
2249
- `;
2250
- }
2251
-
2252
- // Listen for auth changes within the same app
2253
- @addEventListener(':appHost', 'auth-changed')
2254
- onAuthChanged(event: CustomEvent) {
2255
- const { detail } = event;
2256
- this.user = detail.user;
2257
- this.render();
2258
- console.log('Auth state changed at:', new Date(detail.timestamp).toLocaleTimeString());
2259
- }
2260
- }
2261
-
2262
- // HTML
2263
- <swc-app swc-get-application-config="dashboardConfig()">
2264
- <auth-panel></auth-panel>
2265
- <user-widget></user-widget> <!-- Reacts to auth-changed event -->
2266
- </swc-app>
2267
- ```
2268
-
2269
- **Result**: When user clicks "Login" in `<auth-panel>`, `<user-widget>` is notified and updates automatically. No explicit imperative connection needed.
2270
-
2271
- ---
2272
-
2273
- ### Pattern 2: Cross-Module (Global) Broadcasting
2274
-
2275
- Send a signal that reaches all apps on the page.
2276
-
2277
- ```typescript
2278
- // ========== SENDER (In Module A) ==========
2279
- @Sim
2280
- @elementDefine('notification-sender')
2281
- class NotificationSender extends HTMLElement {
2282
- @emitCustomEvent(':window', 'system-notification')
2283
- announceSystemMaintenance() {
2284
- return {
2285
- title: 'System Maintenance',
2286
- message: 'Server will be down for 2 hours',
2287
- severity: 'warning',
2288
- timestamp: new Date().toISOString()
2289
- };
2290
- }
2291
-
2292
- @onConnectedInnerHtml
2293
- render() {
2294
- return `
2295
- <button swc-on-click="$host.announceSystemMaintenance()">
2296
- 📢 Broadcast Maintenance Alert
2297
- </button>
2298
- `;
2299
- }
2300
- }
2301
-
2302
- // ========== RECEIVER (In Module B, Different App) ==========
2303
- @Sim
2304
- @elementDefine('notification-display')
2305
- class NotificationDisplay extends HTMLElement {
2306
- private notifications: any[] = [];
2307
-
2308
- @addEventListener(':window', 'system-notification')
2309
- onSystemNotification(event: CustomEvent) {
2310
- this.notifications.push(event.detail);
2311
- this.render();
2312
- }
2313
-
2314
- @onConnectedInnerHtml
2315
- render() {
2316
- return `
2317
- <div style="position: fixed; top: 10px; right: 10px; font-size: 12px;">
2318
- ${this.notifications.map(n => `
2319
- <div style="background: ${n.severity === 'warning' ? '#fff3cd' : '#fff'};
2320
- border: 1px solid #ffc107; padding: 10px; margin: 5px 0;
2321
- border-radius: 4px; max-width: 300px;">
2322
- <strong>${n.title}</strong><br>
2323
- ${n.message}
2324
- </div>
2325
- `).join('')}
2326
- </div>
2327
- `;
2328
- }
2329
- }
2330
-
2331
- <!-- Two independent apps receiving the same broadcast -->
2332
- <swc-app swc-get-application-config="moduleAConfig()">
2333
- <notification-sender></notification-sender>
2334
- </swc-app>
2335
-
2336
- <swc-app swc-get-application-config="moduleBConfig()">
2337
- <notification-display></notification-display>
2338
- </swc-app>
2339
- ```
2340
-
2341
- **Result**: Clicking the button in Module A instantly triggers the notification in Module B, even though they're in separate `<swc-app>` instances.
2342
-
2343
- ---
2344
-
2345
- ### Pattern 3: Hierarchical Parent-Child Messaging
2346
-
2347
- Communicate up and down the component tree with `$parentHost` and `:hosts` selector.
2348
-
2349
- ```typescript
2350
- // ========== PARENT CONTAINER ==========
2351
- @Sim
2352
- @elementDefine('modal-container')
2353
- class ModalContainer extends HTMLElement {
2354
- private modals: any[] = [];
2355
-
2356
- @onConnectedInnerHtml
2357
- render() {
2358
- return `
2359
- <div style="position: relative;">
2360
- <slot></slot>
2361
- ${this.modals.map(m => `
2362
- <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
2363
- background: white; border: 2px solid #333; padding: 20px;
2364
- border-radius: 8px; z-index: 1000;">
2365
- ${m.content}
2366
- <button swc-on-click="$host.closeModal()">Close</button>
2367
- </div>
2368
- `).join('')}
2369
- </div>
2370
- `;
2371
- }
2372
-
2373
- // Listen for child components requesting to open a modal
2374
- @addEventListener(':hosts', 'request-modal')
2375
- onModalRequest(event: CustomEvent) {
2376
- this.modals.push({ id: Date.now(), content: event.detail.html });
2377
- this.render();
2378
- event.stopPropagation();
2379
- }
2380
-
2381
- closeModal() {
2382
- this.modals.pop();
2383
- this.render();
2384
- }
2385
- }
2386
-
2387
- // ========== CHILD COMPONENT ==========
2388
- @Sim
2389
- @elementDefine('form-with-modal')
2390
- class FormWithModal extends HTMLElement {
2391
- @emitCustomEvent(':parentHost', 'request-modal')
2392
- showHelpModal() {
2393
- return {
2394
- html: '<h3>Help</h3><p>Fill in all required fields and click Submit.</p>'
2395
- };
2396
- }
2397
-
2398
- @onConnectedInnerHtml
2399
- render() {
2400
- return `
2401
- <form>
2402
- <input type="text" placeholder="Full Name" required>
2403
- <button type="button" swc-on-click="$host.showHelpModal()">?</button>
2404
- <button type="submit">Submit</button>
2405
- </form>
2406
- `;
2407
- }
2408
- }
2409
-
2410
- <!-- Structure -->
2411
- <modal-container>
2412
- <form-with-modal></form-with-modal>
2413
- </modal-container>
2414
- ```
2415
-
2416
- **Result**: Clicking "?" in the child form opens a modal managed by the parent container.
22
+ ## 💡 Complete Decorator Reference
2417
23
 
2418
- ---
24
+ ### 1. Attribute Management (`@attr`, `@applyAttribute`)
2419
25
 
2420
- ### Pattern 4: Event Detail Payloads
26
+ Sync properties with DOM attributes or manage multiple attributes via methods.
2421
27
 
2422
- Send rich data structures through events.
28
+ - **Field Decorators**:
29
+ - `@attr(selector, options?)`: Syncs a field with an element's attribute.
30
+ - **Method Decorators**:
31
+ - `@applyAttribute(selector, action?)`: Universal attribute controller.
32
+ - `@setAttribute(selector, name?)`: Sets a specific attribute based on return value.
33
+ - `@updateAttribute(selector)`: Merges/updates multiple attributes.
34
+ - `@removeAttribute(selector)`: Removes attributes.
2423
35
 
2424
36
  ```typescript
2425
- @Sim
2426
- @elementDefine('data-grid')
2427
- class DataGrid extends HTMLElement {
2428
- private rows: any[] = [];
2429
-
2430
- @onConnected
2431
- loadData() {
2432
- this.rows = [
2433
- { id: 1, name: 'Alice', age: 28, department: 'Engineering' },
2434
- { id: 2, name: 'Bob', age: 34, department: 'Sales' }
2435
- ];
2436
- this.render();
2437
- }
37
+ class UserProfile extends HTMLElement {
38
+ @attr(':host') name = 'Guest';
2438
39
 
2439
- // Emit complex payload when row is clicked
2440
- @emitCustomEvent(':appHost', 'row-selected', false) // 3rd arg: whether to bubble
2441
- selectRow(rowId: number) {
2442
- const row = this.rows.find(r => r.id === rowId);
40
+ @updateAttribute('.avatar')
41
+ syncStatus(active: boolean) {
2443
42
  return {
2444
- rowId,
2445
- rowData: row,
2446
- timestamp: Date.now(),
2447
- gridId: this.id,
2448
- userAgent: navigator.userAgent
43
+ 'data-active': active,
44
+ 'aria-hidden': (el) => !active
2449
45
  };
2450
46
  }
2451
-
2452
- @onConnectedInnerHtml
2453
- render() {
2454
- return `
2455
- <table>
2456
- <tbody>
2457
- ${this.rows.map(row => `
2458
- <tr swc-on-click="$host.selectRow(${row.id})">
2459
- <td>${row.name}</td>
2460
- <td>${row.age}</td>
2461
- <td>${row.department}</td>
2462
- </tr>
2463
- `).join('')}
2464
- </tbody>
2465
- </table>
2466
- `;
2467
- }
2468
- }
2469
-
2470
- // Listener receives all payload details
2471
- @Sim
2472
- @elementDefine('detail-panel')
2473
- class DetailPanel extends HTMLElement {
2474
- private selectedData: any = null;
2475
-
2476
- @addEventListener(':appHost', 'row-selected')
2477
- onRowSelect(event: CustomEvent) {
2478
- const { rowData, timestamp } = event.detail;
2479
- this.selectedData = rowData;
2480
- console.log(`Selected at ${new Date(timestamp).toLocaleTimeString()}`);
2481
- this.render();
2482
- }
2483
-
2484
- @onConnectedInnerHtml
2485
- render() {
2486
- if (!this.selectedData) return '<p>No selection</p>';
2487
- return `
2488
- <div>
2489
- <h3>${this.selectedData.name}</h3>
2490
- <p>Age: ${this.selectedData.age}</p>
2491
- <p>Department: ${this.selectedData.department}</p>
2492
- </div>
2493
- `;
2494
- }
2495
- }
2496
- ```
2497
-
2498
- ---
2499
-
2500
- ## 🏰 Host Hierarchy, Context Propagation & Template Events
2501
-
2502
- SWC automatically traverses the component tree to provide meaningful **context** to scripts and event handlers. This eliminates the need for prop drilling and global state managers.
2503
-
2504
- ### Context Variables (In Templates & Event Handlers)
2505
-
2506
- When you use `swc-on-*` inline events or callback functions, these variables are automatically injected:
2507
-
2508
- | Variable | Type | Description |
2509
- |----------|------|-------------|
2510
- | **`$host`** | `HTMLElement` | The component that owns the event handler (the element itself) |
2511
- | **`$parentHost`** | `HTMLElement` | The direct parent SWC component |
2512
- | **`$appHost`** | `SwcAppInterface` | The nearest parent `<swc-app>` boundary |
2513
- | **`$hosts`** | `HTMLElement[]` | Array of ALL SWC ancestors: `[Root, Parent, ..., Self]` |
2514
- | **`$el`** | `HTMLElement` | The actual DOM element that triggered the event |
2515
- | **`$item`** | `any` | (In `swc-loop`) Current iteration item |
2516
- | **`$index`** | `number` | (In `swc-loop`) Current iteration index |
2517
-
2518
- ### Pattern 1: Declarative Inline Events (`swc-on-*`)
2519
-
2520
- Bind events directly in HTML templates. The context is automatically available.
2521
-
2522
- ```typescript
2523
- @Sim
2524
- @elementDefine('todo-item')
2525
- class TodoItem extends HTMLElement {
2526
- private title = '';
2527
- private done = false;
2528
-
2529
- constructor(private todoService: TodoService) { super(); }
2530
-
2531
- @onConnectedInnerHtml
2532
- render() {
2533
- return `
2534
- <style>
2535
- .completed { text-decoration: line-through; color: #ccc; }
2536
- button { margin-left: 10px; padding: 2px 8px; cursor: pointer; }
2537
- </style>
2538
- <li class="${this.done ? 'completed' : ''}">
2539
- <!-- Access $host (self), $appHost (parent app), and $item -->
2540
- <span>${this.title}</span>
2541
-
2542
- <!-- Inline script with full context -->
2543
- <button swc-on-click="
2544
- $host.toggleDone();
2545
- console.log('Current item:', $item);
2546
- console.log('App instance:', $appHost);
2547
- ">
2548
- ${this.done ? '✓ Done' : '○ Pending'}
2549
- </button>
2550
-
2551
- <button swc-on-click="$host.deleteTodo($index)">
2552
- 🗑️ Delete
2553
- </button>
2554
-
2555
- <!-- Access element coordinates -->
2556
- <button swc-on-click="
2557
- console.log('Clicked at:', $el.getBoundingClientRect());
2558
- ">
2559
- Info
2560
- </button>
2561
- </li>
2562
- `;
2563
- }
2564
-
2565
- toggleDone() {
2566
- this.done = !this.done;
2567
- this.render();
2568
- }
2569
-
2570
- deleteTodo(index: number) {
2571
- console.log('Deleting todo at index:', index);
2572
- }
2573
- }
2574
- ```
2575
-
2576
- ### Pattern 2: Loop Iteration Context
2577
-
2578
- `swc-loop` provides special iteration variables you can access in inline events.
2579
-
2580
- ```typescript
2581
- @Sim
2582
- @elementDefine('todo-list')
2583
- class TodoList extends HTMLElement {
2584
- private todos = [
2585
- { id: 1, title: 'Learn SWC', done: false },
2586
- { id: 2, title: 'Build an app', done: false }
2587
- ];
2588
-
2589
- @onConnectedInnerHtml
2590
- render() {
2591
- return `
2592
- <ul>
2593
- <template is="swc-loop" on-get-value="todos"
2594
- on-clone-node="
2595
- if ($isElement) {
2596
- console.log('Cloning item:', $item, 'at index:', $index);
2597
- $node.querySelector('.index').textContent = $index + 1;
2598
- }
2599
- ">
2600
- <li class="todo-item">
2601
- <span class="index">0</span>.
2602
- <span class="title">${'${item.title}'}</span>
2603
- <!-- Callback receives full context -->
2604
- <button swc-on-click="
2605
- $host.editTodo($item.id, $index, $el);
2606
- console.log('Hosts hierarchy:', $hosts.map(h => h.tagName));
2607
- ">
2608
- Edit
2609
- </button>
2610
- </li>
2611
- </template>
2612
- </ul>
2613
- `;
2614
- }
2615
-
2616
- editTodo(id: number, index: number, element: HTMLElement) {
2617
- console.log(`Editing todo #${id} (index: ${index}):`, element);
2618
- }
2619
- }
2620
- ```
2621
-
2622
- ### Pattern 3: Accessing Ancestor Services via Context
2623
-
2624
- Use `$appHost` to access services from the parent app's DI container.
2625
-
2626
- ```typescript
2627
- @Sim
2628
- @elementDefine('child-component')
2629
- class ChildComponent extends HTMLElement {
2630
- @onConnectedInnerHtml
2631
- render() {
2632
- return `
2633
- <div>
2634
- <!-- Access parent app's router to navigate -->
2635
- <button swc-on-click="$appHost.routing('/dashboard')">
2636
- Go to Dashboard
2637
- </button>
2638
-
2639
- <!-- Access other components in the app -->
2640
- <button swc-on-click="
2641
- const sibling = document.querySelector('other-component');
2642
- sibling.triggerUpdate();
2643
- ">
2644
- Notify Sibling
2645
- </button>
2646
-
2647
- <!-- Emit event up the tree to parent app -->
2648
- <button swc-on-click="
2649
- $appHost.dispatchEvent(new CustomEvent('child-ready', {
2650
- detail: { child: $host, timestamp: Date.now() }
2651
- }));
2652
- ">
2653
- Notify Parent App
2654
- </button>
2655
- </div>
2656
- `;
2657
- }
2658
- }
2659
- ```
2660
-
2661
- ### Pattern 4: Host Hierarchy Traversal
2662
-
2663
- Manually traverse the component tree to find ancestors or siblings.
2664
-
2665
- ```typescript
2666
- @Sim
2667
- @elementDefine('deep-child')
2668
- class DeepChild extends HTMLElement {
2669
- findRootApp() {
2670
- // Traverse up until you find <swc-app> or root
2671
- let current = this.parentElement as any;
2672
- while (current && current.tagName !== 'SWC-APP') {
2673
- current = current.parentElement;
2674
- }
2675
- return current;
2676
- }
2677
-
2678
- getSiblingCount() {
2679
- // Count siblings at same level
2680
- return this.parentElement?.querySelectorAll(':scope > *').length || 0;
2681
- }
2682
-
2683
- @onConnectedInnerHtml
2684
- render() {
2685
- return `
2686
- <div>
2687
- <p>Root app: ${this.findRootApp()?.getAttribute('id')}</p>
2688
- <p>Sibling count: ${this.getSiblingCount()}</p>
2689
- </div>
2690
- `;
2691
- }
2692
- }
2693
- ```
2694
-
2695
- ### Real-World Example: Multi-Level Navigation with Context
2696
-
2697
- ```typescript
2698
- // ============ ROOT ROUTER ============
2699
- @Sim
2700
- @Router({ path: '/', route: { '/dashboard': Dashboard, '/profile': Profile } })
2701
- @elementDefine('app-root')
2702
- class AppRoot extends HTMLElement {
2703
- private breadcrumbs: string[] = [];
2704
-
2705
- @onConnectedInnerHtml
2706
- render() {
2707
- return `
2708
- <div>
2709
- <header>
2710
- <h1>My App</h1>
2711
- <nav>
2712
- <button swc-on-click="$host.router.go('/dashboard')">Dashboard</button>
2713
- <button swc-on-click="$host.router.go('/profile')">Profile</button>
2714
- </nav>
2715
- <p>Breadcrumbs: ${this.breadcrumbs.join(' > ')}</p>
2716
- </header>
2717
- <main><slot></slot></main>
2718
- </div>
2719
- `;
2720
- }
2721
- }
2722
-
2723
- // ============ CHILD ROUTE ============
2724
- @Sim
2725
- @elementDefine('dashboard-page')
2726
- class DashboardPage extends HTMLElement {
2727
- @onConnectedInnerHtml
2728
- render() {
2729
- return `
2730
- <div>
2731
- <h2>Dashboard</h2>
2732
- <button swc-on-click="
2733
- console.log('App router:', $appHost.routing);
2734
- console.log('My tag:', $host.tagName);
2735
- console.log('All ancestors:', $hosts.map(h => h.tagName).join(' -> '));
2736
- ">
2737
- Debug Context
2738
- </button>
2739
- </div>
2740
- `;
2741
- }
2742
- }
2743
- ```
2744
-
2745
- ---
2746
-
2747
- ## 🏰 Advanced Host Context: Query & Portal Patterns
2748
-
2749
- ### Custom Query with Root Options
2750
-
2751
- Combine `@query/@queryAll` with the `root` option from host methods to precision-target elements.
2752
-
2753
- ```typescript
2754
- @Sim
2755
- @elementDefine('form-validator')
2756
- class FormValidator extends HTMLElement {
2757
- @query('form', { root: 'light' }) lightForm?: HTMLFormElement;
2758
- @query('.hidden-form', { root: 'shadow' }) shadowForm?: HTMLFormElement;
2759
- @queryAll('input', { root: 'all' }) allInputs?: HTMLInputElement[];
2760
-
2761
- @onConnectedInnerHtml({ useShadow: true })
2762
- render() {
2763
- return `
2764
- <form class="shadow-form">
2765
- <input type="text" placeholder="Shadow DOM input">
2766
- </form>
2767
- <slot></slot>
2768
- `;
2769
- }
2770
-
2771
- validateAllForms() {
2772
- // Validate both light and shadow DOM forms
2773
- const allInputs = Array.from(this.allInputs || []);
2774
- return allInputs.every(input => input.value.trim() !== '');
2775
- }
2776
- }
2777
- ```
2778
-
2779
- ### Portal Pattern: Render Content Anywhere
2780
-
2781
- Use `on-get-portal` to render content at a completely different location in the DOM.
2782
-
2783
- ```typescript
2784
- @Sim
2785
- @elementDefine('tooltip-manager')
2786
- class TooltipManager extends HTMLElement {
2787
- private tooltips: any[] = [];
2788
-
2789
- @onConnectedInnerHtml
2790
- render() {
2791
- return `
2792
- <style>
2793
- .tooltip { position: fixed; background: #222; color: white;
2794
- padding: 8px 12px; border-radius: 4px; font-size: 12px; }
2795
- </style>
2796
- <template is="swc-loop" on-get-value="tooltips"
2797
- on-get-portal="document.body">
2798
- <div class="tooltip" style="left: ${'${item.x}px'}; top: ${'${item.y}px'}">
2799
- ${'${item.text}'}
2800
- </div>
2801
- </template>
2802
- `;
2803
- }
2804
-
2805
- addTooltip(text: string, x: number, y: number) {
2806
- this.tooltips.push({ text, x, y });
2807
- this.render();
2808
- }
2809
- }
2810
-
2811
- // Usage: Tooltip renders outside component hierarchy
2812
- const tooltipMgr = document.querySelector('tooltip-manager');
2813
- tooltipMgr.addTooltip('Hover info', 100, 50);
2814
- ```
2815
-
2816
-
2817
-
2818
- ## 🔄 Structural Directives (Advanced)
2819
-
2820
- SWC provides 4 core templates that empower your HTML without wrapper elements: `swc-loop`, `swc-if`, `swc-choose`, and `swc-async`. They all share a consistent, high-performance API with **zero wrapper elements** and **surgical DOM updates**.
2821
-
2822
- ### Common Attributes:
2823
- - **`on-get-value`**: Script to fetch data (evaluated in component context).
2824
- - **`on-clone-node`**: Callback fired for **each individual node clone** (Args: `$node`, `$item`, `$index`, `$isElement`, etc.).
2825
- - **`on-clone-nodes`**: Callback fired **once after all nodes cloned** (Args: `$nodes`, `$elements`, `$state`, etc.).
2826
- - **`on-get-portal`**: Render nodes at a different DOM location using a selector.
2827
- - **`on-state-changed`**: Track rendering state changes.
2828
-
2829
- ### 1. `swc-if` - Binary Conditional Rendering
2830
-
2831
- Show/hide content based on a boolean expression. No DOM wrapper.
2832
-
2833
- ```typescript
2834
- @Sim
2835
- @elementDefine('access-control')
2836
- class AccessControl extends HTMLElement {
2837
- private user: any = null;
2838
- private isLoading = false;
2839
-
2840
- @onConnectedInnerHtml
2841
- render() {
2842
- return `
2843
- <div class="access-panel">
2844
- <!-- Show ONLY if loading (no wrapper) -->
2845
- <template is="swc-if" on-get-value="isLoading">
2846
- <div class="loading-spinner">
2847
- <spinner-component></spinner-component>
2848
- <p>Verifying access...</p>
2849
- </div>
2850
- </template>
2851
-
2852
- <!-- Show ONLY if user is authenticated -->
2853
- <template is="swc-if" on-get-value="user">
2854
- <template is="swc-default">
2855
- <p class="not-authorized">⛔ You are not authorized to view this content.</p>
2856
- </template>
2857
- <div class="admin-content">
2858
- <h2>Welcome, \${user?.name}! 🎉</h2>
2859
- <p>VIP Access Level: \${user?.role}</p>
2860
- </div>
2861
- </template>
2862
- </div>
2863
- `;
2864
- }
2865
- }
2866
- ```
2867
-
2868
- ### 2. `swc-loop` - High-Performance List Rendering
2869
-
2870
- Iterate over arrays with **index-preserving stability** and zero virtual DOM overhead.
2871
-
2872
- ```typescript
2873
- @Sim
2874
- @elementDefine('product-grid')
2875
- class ProductGrid extends HTMLElement {
2876
- private products = [
2877
- { id: 1, name: 'Laptop', price: 999 },
2878
- { id: 2, name: 'Mouse', price: 29 }
2879
- ];
2880
-
2881
- @onConnectedInnerHtml
2882
- render() {
2883
- return `
2884
- <template is="swc-loop" on-get-value="products"
2885
- on-clone-node="
2886
- if ($isElement) {
2887
- $node.querySelector('.price').textContent = '$' + $item.price;
2888
- }
2889
- ">
2890
- <div class="product-card">
2891
- <h3>\${item.name}</h3>
2892
- <p class="price">$\${item.price}</p>
2893
- </div>
2894
- </template>
2895
- `;
2896
- }
2897
47
  }
2898
48
  ```
2899
49
 
2900
- ### 3. `swc-choose` - Multi-Branch Conditional
2901
-
2902
- Switch-like behavior for multiple conditions.
2903
-
2904
- ```typescript
2905
- @templateIsSim
2906
- @elementDefine('order-status')
2907
- class OrderStatus extends HTMLElement {
2908
- private order = { status: 'shipped' };
2909
-
2910
- @onConnectedInnerHtml
2911
- render() {
2912
- return `
2913
- <template is="swc-choose" on-get-value="order.status">
2914
- <template is="swc-when" on-get-value="'pending'">
2915
- <p>⏳ Your order is being prepared...</p>
2916
- </template>
2917
- <template is="swc-when" on-get-value="'shipped'">
2918
- <p>📦 Your order has shipped!</p>
2919
- </template>
2920
- <template is="swc-default">
2921
- <p>❌ Order status unknown</p>
2922
- </template>
2923
- </template>
2924
- `;
2925
- }
2926
- }
2927
- ```
50
+ ### 2. Class Manipulation (`@applyClass`)
2928
51
 
2929
- ### 4. `swc-async` - Async Content Loading
52
+ Declaratively manage CSS classes. Supports complex logic via filter functions.
2930
53
 
2931
- Automatically handle Promise rendering states.
54
+ - **Convenience Methods**:
55
+ - `@setClass(selector)`: Replaces all classes (Replace Strategy).
56
+ - `@updateClass(selector)`: Updates specific classes (Merge Strategy).
57
+ - `@toggleClass(selector)`: Toggles classes.
2932
58
 
2933
59
  ```typescript
2934
- @Sim
2935
- @elementDefine('async-data')
2936
- class AsyncData extends HTMLElement {
2937
- async fetchUserData(userId: string) {
2938
- const response = await fetch(`/api/users/\${userId}`);
2939
- return response.json();
2940
- }
2941
-
2942
- @onConnectedInnerHtml
2943
- render() {
2944
- return `
2945
- <template is="swc-async" on-get-value="fetchUserData('user123')">
2946
- <template is="swc-pending">
2947
- <p>Loading...</p>
2948
- </template>
2949
- <template is="swc-fulfilled">
2950
- <h2>\${data.name}</h2>
2951
- <p>Email: \${data.email}</p>
2952
- </template>
2953
- <template is="swc-rejected">
2954
- <p>❌ Error: \${error.message}</p>
2955
- </template>
2956
- </template>
2957
- `;
2958
- }
60
+ @updateClass('.nav-item')
61
+ updateActive(path: string) {
62
+ return {
63
+ // el: Target element, $set: HelperSet including $this
64
+ active: (el, $set) => path.startsWith(el.dataset.path!)
65
+ };
2959
66
  }
2960
67
  ```
2961
68
 
2962
- ---
2963
-
2964
- ## 🚀 Server-Side Rendering (SSR) Project Example
2965
-
2966
- SWC integrates seamlessly with `@dooboostore/simple-boot-http-server-ssr` to provide **full-stack SSR** applications with automatic tag-name preservation, hydration, and isomorphic routing.
2967
-
2968
- ### Project Structure
2969
-
2970
- ```
2971
- ssr-project/
2972
- ├── back-end/ # Express/HTTP server with SSR
2973
- │ └── index.ts # SSR entry point
2974
- ├── front-end/ # Client-side app
2975
- │ └── index.ts # Client bootstrap
2976
- ├── src/
2977
- │ ├── bootfactory.ts # Universal app configuration
2978
- │ ├── components/ # Shared components
2979
- │ └── pages/ # Route components (routers + pages)
2980
- ```
2981
-
2982
- ### 1. Backend SSR Setup
2983
-
2984
- The backend renders components on the server and sends HTML to the client:
2985
-
2986
- ```typescript
2987
- // back-end/index.ts
2988
- import { SimpleBootHttpSSRServer } from '@dooboostore/simple-boot-http-server-ssr';
2989
- import { SSRSimpleWebComponentFilter } from '@dooboostore/simple-boot-http-server-ssr';
2990
- import swcRegister from '@dooboostore/simple-web-component';
2991
- import bootfactory from '@swc-src/bootfactory';
2992
-
2993
- const swcFilter = new SSRSimpleWebComponentFilter({
2994
- frontDistPath: path.resolve(__dirname, '../dist-front-end'),
2995
- frontDistIndexFileName: 'index.html',
2996
- registerComponents: async (window: any) => {
2997
- // 1. Register core SWC elements (swc-if, swc-loop, etc.)
2998
- swcRegister(window);
2999
-
3000
- // 2. Register and bootstrap your app with routing
3001
- const urlPath = window.location.pathname;
3002
- bootfactory(window, urlPath);
3003
-
3004
- console.log('✅ Components registered for SSR');
3005
- }
3006
- });
3007
-
3008
- const server = new SimpleBootHttpSSRServer({
3009
- listen: { port: 8080 },
3010
- filters: [swcFilter]
3011
- });
3012
-
3013
- server.listen();
3014
- console.log('🚀 SSR Server running on http://localhost:8080');
3015
- ```
69
+ ### 3. Style Manipulation (`@applyStyle`)
3016
70
 
3017
- ### 2. Universal Bootfactory
71
+ Surgically update element styles with dynamic value support.
3018
72
 
3019
- The bootfactory runs on **both server and client** with different connection modes:
73
+ - **Convenience Methods**:
74
+ - `@setStyle(selector)`: Replaces existing inline styles.
75
+ - `@updateStyle(selector)`: Merges with existing styles.
76
+ - `@removeStyle(selector)`: Removes specific properties.
3020
77
 
3021
78
  ```typescript
3022
- // src/bootfactory.ts
3023
- import register from '@dooboostore/simple-web-component';
3024
- import { IndexRouter } from './pages/index.router';
3025
-
3026
- export default (window: Window, initialPath?: string) => {
3027
- // Register all components globally
3028
- register(window, [...componentFactories, ...pageFactories]);
3029
-
3030
- const app = window.document.querySelector('#app') as any;
3031
-
3032
- const config = {
3033
- rootRouter: IndexRouter.SYMBOL,
3034
- routeType: 'path', // Use browser path: /users, /pages/about
3035
- path: initialPath ?? '/',
3036
- window: window
79
+ @updateStyle('.box')
80
+ adjustStyle(highlight: boolean) {
81
+ return {
82
+ 'background-color': highlight ? 'yellow' : null, // null/undefined removes property
83
+ 'font-weight': (el) => highlight ? 'bold' : 'normal'
3037
84
  };
3038
-
3039
- // Detect client vs server environment
3040
- const isClientWindow = typeof window !== 'undefined' && window === globalThis.window;
3041
-
3042
- if (isClientWindow) {
3043
- // CLIENT: Use 'swap' mode for zero FOUC hydration
3044
- app.connect({
3045
- ...config,
3046
- connectMode: 'swap' // Clone → Connect → Replace original
3047
- });
3048
- } else {
3049
- // SERVER: Use 'direct' mode (no swap needed)
3050
- app.connect(config);
3051
- }
3052
-
3053
- return app;
3054
- };
3055
- ```
3056
-
3057
- ### 3. Isomorphic Router & Pages
3058
-
3059
- Define routers that work the same way on server and client:
3060
-
3061
- ```typescript
3062
- // src/pages/index.router.ts
3063
- import { Sim } from '@dooboostore/simple-boot';
3064
- import { Router } from '@dooboostore/simple-boot';
3065
- import { elementDefine, onConnectedInnerHtml, setAttribute } from '@dooboostore/simple-web-component';
3066
- import { RouterAction } from '@dooboostore/simple-boot';
3067
-
3068
- @Sim
3069
- @Router({
3070
- path: '',
3071
- route: {
3072
- '/': HomePage.SYMBOL,
3073
- '/user': UserPage.SYMBOL
3074
- }
3075
- })
3076
- @elementDefine('index-router')
3077
- class IndexRouter extends HTMLElement implements RouterAction.CanActivate {
3078
- constructor(private router: Router) {
3079
- super();
3080
- }
3081
-
3082
- // Lifecycle hook: Called BEFORE entering route
3083
- async canActivate(url: RoutingDataSet, data?: any) {
3084
- // Inject rendered page component
3085
- if (data instanceof Node) {
3086
- console.log(`✅ Rendering: \${data.nodeName}`);
3087
- this.replaceChildren(data);
3088
- }
3089
- }
3090
-
3091
- @onConnectedInnerHtml({ useShadow: true })
3092
- render() {
3093
- return `
3094
- <style>
3095
- :host { display: block; }
3096
- header { background: #222; color: white; padding: 20px; }
3097
- nav button { margin: 5px; padding: 10px 15px; cursor: pointer; }
3098
- section { padding: 20px; }
3099
- </style>
3100
- <header>
3101
- <h1>SWC + SSR App</h1>
3102
- <nav>
3103
- <button swc-on-click="$host.router.go('/')">🏠 Home</button>
3104
- <button swc-on-click="$host.router.go('/user')">👤 Users</button>
3105
- </nav>
3106
- </header>
3107
- <section><slot></slot></section>
3108
- `;
3109
- }
3110
- }
3111
- ```
3112
-
3113
- ### 4. Page Components
3114
-
3115
- ```typescript
3116
- // src/pages/home.page.ts - Leaf route (no children)
3117
- @Sim
3118
- @elementDefine('home-page')
3119
- class HomePage extends HTMLElement {
3120
- @onConnectedInnerHtml
3121
- render() {
3122
- return `
3123
- <h2>Welcome! 👋</h2>
3124
- <p>This page was rendered on the server and hydrated on the client.</p>
3125
- <template is="swc-loop" on-get-value="[1,2,3]">
3126
- <div style="padding: 10px; background: #f5f5f5; margin: 5px;">
3127
- Item \${item}
3128
- </div>
3129
- </template>
3130
- `;
3131
- }
3132
- }
3133
-
3134
- // src/pages/user.router.ts - Nested router with sub-routes
3135
- @Sim
3136
- @Router({
3137
- path: '/user',
3138
- route: {
3139
- '': UserListPage.SYMBOL,
3140
- '/:id': UserDetailPage.SYMBOL
3141
- }
3142
- })
3143
- @elementDefine('user-router')
3144
- class UserRouter extends HTMLElement implements RouterAction.CanActivate {
3145
- async canActivate(url: RoutingDataSet, data?: any) {
3146
- if (data instanceof Node) {
3147
- this.replaceChildren(data);
3148
- }
3149
- }
3150
-
3151
- @onConnectedInnerHtml({ useShadow: true })
3152
- render() {
3153
- return `
3154
- <div style="margin-left: 20px;">
3155
- <h2>User Management</h2>
3156
- <slot></slot>
3157
- </div>
3158
- `;
3159
- }
3160
85
  }
3161
86
  ```
3162
87
 
3163
- ### 5. Client Entry Point
3164
-
3165
- The client reuses the same bootfactory:
88
+ ### 4. Surgical DOM Manipulation (`@applyNode`)
3166
89
 
3167
- ```typescript
3168
- // front-end/index.ts
3169
- import bootfactory from '@swc-src/bootfactory';
3170
- import { UrlUtils } from '@dooboostore/core/url';
3171
-
3172
- console.log('🚀 Client-side app initializing...');
3173
-
3174
- // Get current URL path from browser
3175
- const currentPath = UrlUtils.getUrlPath(window.location);
3176
-
3177
- // Bootstrap with swap mode (zero FOUC)
3178
- bootfactory(window, currentPath);
90
+ Update specific DOM nodes without full re-renders.
3179
91
 
3180
- console.log('✅ Client app hydrated');
3181
- ```
3182
-
3183
- ### 6. Flow Diagram
92
+ - `@applyInnerHtmlNode(selector)`: Sets `innerHTML`.
93
+ - `@applyInnerTextNode(selector)`: Sets `innerText`.
94
+ - `@applyReplaceChildrenNode(selector)`: Replaces all children.
95
+ - `@applyAppendChildNode(selector)`: Appends nodes.
3184
96
 
3185
- ```
3186
- ┌─────────────────────────────────────────────────────────┐
3187
- │ User Request │
3188
- │ GET /user/123 │
3189
- └──────────────────────┬──────────────────────────────────┘
3190
- │
3191
- ┌──────────────▼──────────────┐
3192
- │ Backend SSR Renderer │
3193
- ├─────────────────────────────┤
3194
- │ 1. bootfactory(virtualWin) │
3195
- │ 2. IndexRouter routes │
3196
- │ 3. UserRouter renders │
3197
- │ 4. UserDetailPage → HTML │
3198
- └──────────────┬──────────────┘
3199
- │
3200
- ┌──────────────▼──────────────┐
3201
- │ Sent to Client as HTML │
3202
- │ <swc-app> │
3203
- │ <index-router> │
3204
- │ <user-router> │
3205
- │ <user-detail-page> │
3206
- │ </user-detail-page> │
3207
- │ </user-router> │
3208
- │ </index-router> │
3209
- │ </swc-app> │
3210
- └──────────────┬──────────────┘
3211
- │
3212
- ┌──────────────▼──────────────┐
3213
- │ Client Hydration Swap │
3214
- ├─────────────────────────────┤
3215
- │ 1. Clone SSR HTML │
3216
- │ 2. Connect listeners │
3217
- │ 3. Replace original DOM │
3218
- │ 4. Fully interactive │
3219
- └─────────────────────────────┘
3220
- ```
97
+ ### 5. Host Shorthands
3221
98
 
3222
- ### 7. Key Benefits
99
+ All decorators have `:host` equivalents for convenience:
100
+ `@updateClassHost()`, `@updateStyleHost()`, `@applyAttributeHost()`, etc.
3223
101
 
3224
- | Aspect | Benefit |
3225
- |--------|---------|
3226
- | **SEO** | Server renders initial HTML (searchable) |
3227
- | **Performance** | Faster First Contentful Paint (FCP) |
3228
- | **Tag Preservation** | Custom element tags survive SSR → client |
3229
- | **Zero FOUC** | Swap mode clones & replaces seamlessly |
3230
- | **Code Sharing** | Same router code on server & client |
3231
- | **Island Architecture** | Multiple independent `<swc-app>` instances |
3232
- | **Progressive Enhancement** | Works without JavaScript (graceful fallback) |
102
+ ---
3233
103
 
3234
- ### 8. Build & Run
104
+ ## 📦 Installation & Setup
3235
105
 
3236
106
  ```bash
3237
- # Build backend (Node.js server)
3238
- cd back-end && npm run build
3239
-
3240
- # Build frontend (browser bundle)
3241
- cd front-end && npm run build
3242
-
3243
- # Run SSR server on http://localhost:8080
3244
- npm start
3245
-
3246
- # Visit http://localhost:8080/user/123
3247
- # Server renders UserDetailPage → Client hydrates with swap mode
107
+ npm install @dooboostore/simple-web-component reflect-metadata
3248
108
  ```
3249
109
 
3250
- ---
3251
-
3252
- ## ⚡ Performance Features
3253
- - **Zero "Deep Scan"**: Listeners and queries are only bound to specific selectors.
3254
- - **Surgical Updates**: Manipulate DOM directly in `@changedAttribute` instead of full re-renders.
3255
- - **Zero Background Overhead**: No polling, no background observers.
3256
- - **Smart Delegation**: Host-level delegation with `__swc_handled` duplicate protection.
3257
- - **Index-Preserving Removal**: `SwcLoop` maintains stable indices during item removal for maximum UI consistency.
3258
-
3259
- ---
3260
-
3261
- ## 📜 License
3262
- MIT
110
+ ### Browser Support
111
+ For **Safari** support of the `is` attribute, add this to your `<head>`:
112
+ ```html
113
+ <script src="https://unpkg.com/@ungap/custom-elements"></script>
114
+ ```