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