@dooboostore/simple-web-component 1.0.26 → 1.0.27

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