@dooboostore/simple-web-component 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -3424
- package/dist/cjs/decorators/addEventListener.js +4 -0
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/attribute.js +32 -43
- package/dist/cjs/decorators/attribute.js.map +3 -3
- package/dist/cjs/decorators/elementDefine.js +45 -19
- package/dist/cjs/decorators/elementDefine.js.map +2 -2
- package/dist/cjs/decorators/index.js +1 -0
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/lifecycles.js +5 -5
- package/dist/cjs/decorators/lifecycles.js.map +2 -2
- package/dist/cjs/decorators/query.js +4 -4
- package/dist/cjs/decorators/query.js.map +2 -2
- package/dist/cjs/decorators/queryAll.js +4 -4
- package/dist/cjs/decorators/queryAll.js.map +2 -2
- package/dist/cjs/decorators/subscribeSwcAppRouteChange.js +59 -0
- package/dist/cjs/decorators/subscribeSwcAppRouteChange.js.map +7 -0
- package/dist/cjs/elements/SwcAppEngine.js +18 -63
- package/dist/cjs/elements/SwcAppEngine.js.map +3 -3
- package/dist/cjs/elements/SwcAppMixin.js +103 -24
- package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
- package/dist/cjs/elements/register.js +0 -1
- package/dist/cjs/elements/register.js.map +2 -2
- package/dist/cjs/utils/Utils.js +18 -0
- package/dist/cjs/utils/Utils.js.map +3 -3
- package/dist/esm/decorators/addEventListener.js +4 -0
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/attribute.js +32 -43
- package/dist/esm/decorators/attribute.js.map +3 -3
- package/dist/esm/decorators/elementDefine.js +46 -20
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/decorators/index.js +1 -0
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/lifecycles.js +5 -5
- package/dist/esm/decorators/lifecycles.js.map +2 -2
- package/dist/esm/decorators/query.js +4 -4
- package/dist/esm/decorators/query.js.map +2 -2
- package/dist/esm/decorators/queryAll.js +4 -4
- package/dist/esm/decorators/queryAll.js.map +2 -2
- package/dist/esm/decorators/subscribeSwcAppRouteChange.js +40 -0
- package/dist/esm/decorators/subscribeSwcAppRouteChange.js.map +7 -0
- package/dist/esm/elements/SwcAppEngine.js +18 -63
- package/dist/esm/elements/SwcAppEngine.js.map +2 -2
- package/dist/esm/elements/SwcAppMixin.js +103 -24
- package/dist/esm/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm/elements/register.js +0 -1
- package/dist/esm/elements/register.js.map +2 -2
- package/dist/esm/utils/Utils.js +18 -0
- package/dist/esm/utils/Utils.js.map +3 -3
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +177 -2477
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/decorators/addEventListener.d.ts +8 -1
- package/dist/types/decorators/addEventListener.d.ts.map +1 -1
- package/dist/types/decorators/attribute.d.ts +0 -1
- package/dist/types/decorators/attribute.d.ts.map +1 -1
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/decorators/index.d.ts +1 -0
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/lifecycles.d.ts +2 -2
- package/dist/types/decorators/lifecycles.d.ts.map +1 -1
- package/dist/types/decorators/query.d.ts +1 -1
- package/dist/types/decorators/query.d.ts.map +1 -1
- package/dist/types/decorators/queryAll.d.ts +1 -1
- package/dist/types/decorators/queryAll.d.ts.map +1 -1
- package/dist/types/decorators/subscribeSwcAppRouteChange.d.ts +18 -0
- package/dist/types/decorators/subscribeSwcAppRouteChange.d.ts.map +1 -0
- package/dist/types/elements/SwcAppEngine.d.ts +3 -5
- package/dist/types/elements/SwcAppEngine.d.ts.map +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts +9 -0
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/types/elements/register.d.ts +1 -1
- package/dist/types/elements/register.d.ts.map +1 -1
- package/dist/types/utils/Utils.d.ts +3 -0
- package/dist/types/utils/Utils.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +177 -2477
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/package.json +2 -2
- package/src/decorators/addEventListener.ts +11 -1
- package/src/decorators/attribute.ts +62 -53
- package/src/decorators/elementDefine.ts +93 -30
- package/src/decorators/index.ts +1 -0
- package/src/decorators/lifecycles.ts +2 -2
- package/src/decorators/query.ts +5 -5
- package/src/decorators/queryAll.ts +8 -7
- package/src/decorators/subscribeSwcAppRouteChange.ts +71 -0
- package/src/elements/SwcAppEngine.ts +25 -81
- package/src/elements/SwcAppMixin.ts +143 -28
- package/src/elements/register.ts +0 -2
- package/src/utils/Utils.ts +33 -0
package/README.md
CHANGED
|
@@ -3,3583 +3,375 @@
|
|
|
3
3
|
[](https://github.com/dooboostore-develop/packages/actions/workflows/main.yaml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
> **"Pure, Standards-Compliant, and Blazing Fast Web Components Framework."**
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
##
|
|
11
|
+
## 🎯 Core Features
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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>`;
|
|
19
|
+
@onInitialize
|
|
20
|
+
constructor(private service: MyService) {
|
|
21
|
+
super();
|
|
92
22
|
}
|
|
93
23
|
}
|
|
94
24
|
```
|
|
95
25
|
|
|
96
|
-
**
|
|
97
|
-
|
|
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.
|
|
26
|
+
### 2. **Dependency Injection (@onInitialize)**
|
|
27
|
+
Inject services into Web Components using the `@onInitialize` decorator.
|
|
115
28
|
|
|
116
29
|
```typescript
|
|
117
|
-
@elementDefine('
|
|
118
|
-
class
|
|
119
|
-
@
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const rect = this.getBoundingClientRect();
|
|
126
|
-
console.log('Size:', rect.width, rect.height);
|
|
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;
|
|
127
38
|
}
|
|
128
39
|
}
|
|
129
40
|
```
|
|
130
41
|
|
|
131
|
-
**
|
|
42
|
+
### 3. **Declarative DOM Updates**
|
|
132
43
|
|
|
133
|
-
####
|
|
134
|
-
|
|
44
|
+
#### @onConnectedInnerHtml
|
|
45
|
+
Render HTML automatically when component connects to DOM.
|
|
135
46
|
|
|
136
47
|
```typescript
|
|
137
|
-
@
|
|
138
|
-
|
|
139
|
-
|
|
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');
|
|
156
|
-
}
|
|
48
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
49
|
+
render() {
|
|
50
|
+
return `<div>Hello, <span>${this.name}</span>!</div>`;
|
|
157
51
|
}
|
|
158
52
|
```
|
|
159
53
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
#### 5. `@onAfterDisconnected()` / `@onDisconnected()`
|
|
163
|
-
Called after the component disconnects from the DOM. Final cleanup.
|
|
54
|
+
#### @applyInnerHtmlNodeHost
|
|
55
|
+
Apply HTML to specific host element.
|
|
164
56
|
|
|
165
57
|
```typescript
|
|
166
|
-
@
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
58
|
+
@applyInnerHtmlNodeHost({ root: 'light' })
|
|
59
|
+
updateContent() {
|
|
60
|
+
return `<p>Updated content</p>`;
|
|
181
61
|
}
|
|
182
62
|
```
|
|
183
63
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
#### 6. `@onBeforeAdopted()`
|
|
187
|
-
Called before the element is adopted by a new document (useful for SSR/iframe scenarios).
|
|
64
|
+
#### @applyReplaceChildrenNodeHost
|
|
65
|
+
Replace child nodes with new content.
|
|
188
66
|
|
|
189
67
|
```typescript
|
|
190
|
-
@
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
prepareForAdoption() {
|
|
194
|
-
console.log('Adopting to new document...');
|
|
195
|
-
// Prepare for document change
|
|
196
|
-
}
|
|
68
|
+
@applyReplaceChildrenNodeHost({ root: 'light' })
|
|
69
|
+
replaceChildren(node: Node) {
|
|
70
|
+
return node;
|
|
197
71
|
}
|
|
198
72
|
```
|
|
199
73
|
|
|
200
|
-
|
|
201
|
-
Called after the element is adopted by a new document.
|
|
202
|
-
|
|
203
|
-
```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
|
-
}
|
|
216
|
-
}
|
|
217
|
-
```
|
|
74
|
+
### 4. **Event Handling**
|
|
218
75
|
|
|
219
|
-
####
|
|
220
|
-
|
|
76
|
+
#### @addEventListener
|
|
77
|
+
Attach event listeners to elements with optional filter support.
|
|
221
78
|
|
|
222
79
|
```typescript
|
|
223
|
-
@
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
onListenerAdded(type: string, listener: EventListener, options?: AddEventListenerOptions) {
|
|
227
|
-
console.log(`Event listener added: ${type}`);
|
|
228
|
-
// Track event listeners for debugging
|
|
229
|
-
}
|
|
80
|
+
@addEventListener('#submit-btn', 'click')
|
|
81
|
+
onSubmit(event: Event) {
|
|
82
|
+
console.log('Submitted');
|
|
230
83
|
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
#### 9. `@onConnectedInnerHtml(options?)`
|
|
234
|
-
Defines the component's template. Renders to Shadow DOM or Light DOM based on `useShadow` option.
|
|
235
84
|
|
|
236
|
-
|
|
237
|
-
@
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
`;
|
|
85
|
+
// Filter events - only process matching events
|
|
86
|
+
@addEventListener('button', 'click', {
|
|
87
|
+
filter: (event, helper) => {
|
|
88
|
+
return event.target?.id === 'critical-button';
|
|
254
89
|
}
|
|
90
|
+
})
|
|
91
|
+
onCriticalClick(event: Event) {
|
|
92
|
+
console.log('Critical action');
|
|
255
93
|
}
|
|
256
94
|
```
|
|
257
95
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
96
|
+
#### @emitCustomEvent
|
|
97
|
+
Emit custom events with data.
|
|
275
98
|
|
|
276
99
|
```typescript
|
|
277
|
-
@
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
}
|
|
100
|
+
@emitCustomEvent(':appHost', 'user-login')
|
|
101
|
+
async onLogin() {
|
|
102
|
+
const user = await this.authService.login();
|
|
103
|
+
return { user }; // Sent as event detail
|
|
295
104
|
}
|
|
296
105
|
```
|
|
297
106
|
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
107
|
+
#### @emitCustomEventHost
|
|
108
|
+
Emit events from a method with custom event name mapping.
|
|
305
109
|
|
|
306
110
|
```typescript
|
|
307
|
-
@
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
}
|
|
111
|
+
@emitCustomEventHost('navigate', { attributeName: 'on-navigate' })
|
|
112
|
+
onNavClick(e: any) {
|
|
113
|
+
return { path: e.target.dataset.path };
|
|
322
114
|
}
|
|
323
115
|
```
|
|
324
116
|
|
|
325
|
-
####
|
|
326
|
-
|
|
327
|
-
Universal decorator for handling attributes with flexible strategies.
|
|
117
|
+
#### @addEventListenerHost
|
|
118
|
+
Listen to events on the component element itself (`:host` selector).
|
|
328
119
|
|
|
329
120
|
```typescript
|
|
330
|
-
@
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
}
|
|
352
|
-
|
|
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
|
-
}
|
|
121
|
+
@addEventListenerHost('click')
|
|
122
|
+
onHostClick(event: Event) {
|
|
123
|
+
console.log('Host element clicked');
|
|
361
124
|
}
|
|
362
125
|
```
|
|
363
126
|
|
|
364
|
-
|
|
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).
|
|
127
|
+
#### @addEventListenerAppHost
|
|
128
|
+
Listen to events on the app root host element (`:appHost` selector). Enables selective event handling with filters.
|
|
368
129
|
|
|
369
130
|
```typescript
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
}
|
|
402
|
-
|
|
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
|
-
}
|
|
131
|
+
// Listen to all user-action events from :appHost
|
|
132
|
+
@addEventListenerAppHost('user-action')
|
|
133
|
+
onUserAction(e: CustomEvent) {
|
|
134
|
+
console.log('User action:', e.detail);
|
|
410
135
|
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
**Signature:**
|
|
414
|
-
- `@setAttribute(selector, attributeName, options?)`
|
|
415
|
-
- Returns method's value → reflects to target's attribute
|
|
416
136
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
updateModifiedTime() {
|
|
424
|
-
return new Date().toISOString();
|
|
137
|
+
// Filter specific events - loose coupling pattern
|
|
138
|
+
@addEventListenerAppHost('user-action', {
|
|
139
|
+
filter: (event, helper) => event.detail?.type === 'login'
|
|
140
|
+
})
|
|
141
|
+
onUserLogin(e: CustomEvent) {
|
|
142
|
+
console.log('User logged in:', e.detail.userName);
|
|
425
143
|
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
#### 12d. `@removeAttribute(selector, name, options?)` - Remove Attribute
|
|
429
144
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
145
|
+
// Different component filtering same event differently
|
|
146
|
+
@addEventListenerAppHost('user-action', {
|
|
147
|
+
filter: (event, helper) => event.detail?.type === 'logout'
|
|
148
|
+
})
|
|
149
|
+
onUserLogout(e: CustomEvent) {
|
|
150
|
+
console.log('User logged out');
|
|
436
151
|
}
|
|
437
152
|
```
|
|
438
153
|
|
|
439
|
-
**
|
|
440
|
-
- `':host'` - The component itself
|
|
441
|
-
- `'#element-id'` - Element by ID
|
|
442
|
-
- `'.class-name'` - Element by class
|
|
443
|
-
- `selector` - Any CSS selector
|
|
154
|
+
### 5. **DOM Querying**
|
|
444
155
|
|
|
445
|
-
####
|
|
446
|
-
|
|
156
|
+
#### @query
|
|
157
|
+
Query single element (Light DOM by default).
|
|
447
158
|
|
|
448
159
|
```typescript
|
|
449
|
-
@
|
|
450
|
-
|
|
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
|
-
}
|
|
160
|
+
@query('#form-input')
|
|
161
|
+
formInput?: HTMLInputElement;
|
|
463
162
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return (this.count >= 10).toString();
|
|
467
|
-
}
|
|
468
|
-
|
|
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
|
-
}
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**Usage:**
|
|
489
|
-
```typescript
|
|
490
|
-
// Full form
|
|
491
|
-
@setAttribute(':host', 'data-status')
|
|
492
|
-
// vs
|
|
493
|
-
// Shorthand for :host
|
|
494
|
-
@setAttributeHost('data-status')
|
|
163
|
+
@query('.card', { root: 'shadow' })
|
|
164
|
+
shadowCard?: HTMLElement;
|
|
495
165
|
```
|
|
496
166
|
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
511
|
-
|
|
512
|
-
#### 13. `@query(selector, options?)`
|
|
513
|
-
Inject a single element into a property.
|
|
167
|
+
#### @queryAll
|
|
168
|
+
Query multiple elements.
|
|
514
169
|
|
|
515
170
|
```typescript
|
|
516
|
-
@
|
|
517
|
-
|
|
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]`
|
|
171
|
+
@queryAll('input[type="text"]')
|
|
172
|
+
textInputs?: HTMLInputElement[];
|
|
539
173
|
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
174
|
+
@queryAll('li', { root: 'shadow' })
|
|
175
|
+
listItems?: HTMLLIElement[];
|
|
547
176
|
```
|
|
548
177
|
|
|
549
|
-
####
|
|
550
|
-
|
|
178
|
+
#### @queryHost
|
|
179
|
+
Query the component element itself.
|
|
551
180
|
|
|
552
181
|
```typescript
|
|
553
|
-
@
|
|
554
|
-
|
|
555
|
-
@queryAll('.item') items?: HTMLElement[];
|
|
556
|
-
@queryAll('button', { root: 'shadow' }) buttons?: HTMLButtonElement[];
|
|
182
|
+
@queryHost
|
|
183
|
+
self?: HTMLElement;
|
|
557
184
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
185
|
+
printTag() {
|
|
186
|
+
console.log(this.self?.tagName); // CUSTOM-CARD
|
|
561
187
|
}
|
|
562
188
|
```
|
|
563
189
|
|
|
564
|
-
####
|
|
565
|
-
|
|
190
|
+
#### @queryAllHost
|
|
191
|
+
Query all direct children of the component.
|
|
566
192
|
|
|
567
193
|
```typescript
|
|
568
|
-
@
|
|
569
|
-
|
|
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
|
-
}
|
|
194
|
+
@queryAllHost
|
|
195
|
+
children?: HTMLElement[];
|
|
586
196
|
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
}
|
|
197
|
+
getChildCount() {
|
|
198
|
+
return this.children?.length ?? 0;
|
|
605
199
|
}
|
|
606
200
|
```
|
|
607
201
|
|
|
608
|
-
**
|
|
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
|
|
202
|
+
### 6. **Attribute Binding**
|
|
619
203
|
|
|
620
|
-
####
|
|
621
|
-
|
|
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;
|
|
204
|
+
#### @attribute
|
|
205
|
+
Bind HTML attributes to properties (automatic sync).
|
|
627
206
|
|
|
628
207
|
```typescript
|
|
629
|
-
@
|
|
630
|
-
|
|
631
|
-
@queryHost
|
|
632
|
-
self?: HTMLElement;
|
|
633
|
-
|
|
634
|
-
@queryHost()
|
|
635
|
-
selfExplicit?: HTMLElement;
|
|
208
|
+
@attribute
|
|
209
|
+
width: string = '100px';
|
|
636
210
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
}
|
|
211
|
+
@attribute('data-id')
|
|
212
|
+
resourceId: string = '';
|
|
641
213
|
```
|
|
642
214
|
|
|
643
|
-
####
|
|
644
|
-
|
|
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[];
|
|
215
|
+
#### @attributeHost
|
|
216
|
+
Bind attributes on host element itself.
|
|
650
217
|
|
|
651
218
|
```typescript
|
|
652
|
-
@
|
|
653
|
-
|
|
654
|
-
@queryAllHost
|
|
655
|
-
allChildren?: HTMLElement[];
|
|
219
|
+
@attributeHost('product-id')
|
|
220
|
+
productId: string = '';
|
|
656
221
|
|
|
657
|
-
|
|
658
|
-
|
|
222
|
+
@onInitialize
|
|
223
|
+
constructor() {
|
|
224
|
+
super();
|
|
225
|
+
if (this.productId) {
|
|
226
|
+
this.loadProduct(this.productId);
|
|
659
227
|
}
|
|
660
228
|
}
|
|
661
229
|
```
|
|
662
230
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
### Event & Messaging Decorators
|
|
666
|
-
|
|
667
|
-
#### 17. `@addEventListener(selector, event, options?)`
|
|
668
|
-
Declarative event binding with automatic delegation.
|
|
231
|
+
### 7. **Lifecycle Hooks**
|
|
669
232
|
|
|
670
233
|
```typescript
|
|
671
|
-
@
|
|
672
|
-
|
|
673
|
-
|
|
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);
|
|
698
|
-
}
|
|
699
|
-
|
|
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
|
-
`;
|
|
713
|
-
}
|
|
234
|
+
@onConnected
|
|
235
|
+
onConnected() {
|
|
236
|
+
// Called when element enters DOM
|
|
714
237
|
}
|
|
715
|
-
```
|
|
716
|
-
|
|
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.
|
|
726
238
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
}
|
|
751
|
-
|
|
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
|
-
`;
|
|
760
|
-
}
|
|
239
|
+
@onDisconnected
|
|
240
|
+
onDisconnected() {
|
|
241
|
+
// Called when element leaves DOM
|
|
761
242
|
}
|
|
762
243
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
private user: any = null;
|
|
767
|
-
|
|
768
|
-
@addEventListener(':appHost', 'user-logged-in')
|
|
769
|
-
onUserLoggedIn(event: CustomEvent) {
|
|
770
|
-
this.user = event.detail.user;
|
|
771
|
-
this.render();
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
@onConnectedInnerHtml
|
|
775
|
-
render() {
|
|
776
|
-
return `<p>User: ${this.user?.name ?? 'Anonymous'}</p>`;
|
|
777
|
-
}
|
|
244
|
+
@onAttributeChanged
|
|
245
|
+
onAttributeChanged(name: string, oldValue: string, newValue: string) {
|
|
246
|
+
// Called when attribute changes
|
|
778
247
|
}
|
|
779
248
|
```
|
|
780
249
|
|
|
781
|
-
**
|
|
782
|
-
|
|
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
|
-
---
|
|
799
|
-
|
|
800
|
-
### DOM Manipulation Decorators
|
|
801
|
-
|
|
802
|
-
#### 19. `@applyNode(selector, options?)`
|
|
803
|
-
Surgically add, update, or replace DOM nodes at specific positions.
|
|
250
|
+
### 8. **Routing with @subscribeSwcAppRouteChange**
|
|
251
|
+
Declarative route pattern matching with path parameters.
|
|
804
252
|
|
|
805
253
|
```typescript
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
@
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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();
|
|
254
|
+
@elementDefine('root-router')
|
|
255
|
+
class RootRouter extends HTMLElement {
|
|
256
|
+
@subscribeSwcAppRouteChange('/')
|
|
257
|
+
@applyInnerHtmlNodeHost({ root: 'light' })
|
|
258
|
+
homeRoute(router: RouterEventType) {
|
|
259
|
+
return `<home-page/>`;
|
|
835
260
|
}
|
|
836
261
|
|
|
837
|
-
|
|
838
|
-
@
|
|
839
|
-
|
|
840
|
-
return
|
|
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
|
-
`;
|
|
262
|
+
@subscribeSwcAppRouteChange('/product/{id}')
|
|
263
|
+
@applyInnerHtmlNodeHost({ root: 'light' })
|
|
264
|
+
productRoute(router: RouterEventType, pathData: any) {
|
|
265
|
+
return `<product-page product-id="${pathData.id}"/>`;
|
|
853
266
|
}
|
|
854
267
|
}
|
|
855
268
|
```
|
|
856
269
|
|
|
857
|
-
**
|
|
858
|
-
-
|
|
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:
|
|
270
|
+
### 9. **Accommodation Pattern**
|
|
271
|
+
Factory-based component registration with explicit DI.
|
|
882
272
|
|
|
883
273
|
```typescript
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
}
|
|
911
|
-
|
|
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
|
-
}
|
|
274
|
+
// Component factory
|
|
275
|
+
export default (w: Window) => {
|
|
276
|
+
const tagName = 'my-component';
|
|
277
|
+
const existing = w.customElements.get(tagName);
|
|
278
|
+
if (existing) return tagName;
|
|
924
279
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
280
|
+
@elementDefine(tagName, { window: w })
|
|
281
|
+
class MyComponent extends w.HTMLElement {
|
|
282
|
+
@onInitialize
|
|
283
|
+
constructor(service: MyService) {
|
|
284
|
+
super();
|
|
285
|
+
this.service = service;
|
|
286
|
+
}
|
|
928
287
|
}
|
|
288
|
+
|
|
289
|
+
return tagName;
|
|
290
|
+
};
|
|
929
291
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
292
|
+
// Boot factory
|
|
293
|
+
export default (w: Window, container: symbol) => {
|
|
294
|
+
serviceFactories.forEach(s => s(container));
|
|
295
|
+
register(w, [...pageFactories, ...componentFactories]);
|
|
296
|
+
};
|
|
934
297
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
298
|
+
// Entry point
|
|
299
|
+
const container = Symbol('app');
|
|
300
|
+
bootFactory(w, container);
|
|
301
|
+
appElement.connect({
|
|
302
|
+
container,
|
|
303
|
+
window: w,
|
|
304
|
+
onEngineStarted: () => {
|
|
305
|
+
appElement.innerHTML = '<root-router></root-router>';
|
|
938
306
|
}
|
|
939
|
-
}
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
**SSR Support (with custom Document):**
|
|
943
|
-
|
|
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
|
|
307
|
+
});
|
|
951
308
|
```
|
|
952
309
|
|
|
953
|
-
|
|
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
|
-
|
|
960
|
-
```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
|
-
}
|
|
310
|
+
---
|
|
1000
311
|
|
|
1001
|
-
|
|
1002
|
-
return this.isRemoved;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
```
|
|
312
|
+
## ⚠️ Critical Rules
|
|
1006
313
|
|
|
1007
|
-
|
|
1008
|
-
|
|
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.
|
|
314
|
+
### DO NOT Use @Sim on Web Components
|
|
315
|
+
`@Sim` is for **Services only**. Web Components should use `@elementDefine`.
|
|
1036
316
|
|
|
1037
317
|
```typescript
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
}
|
|
318
|
+
// ✅ CORRECT
|
|
319
|
+
@Sim()
|
|
320
|
+
export class UserService { }
|
|
1053
321
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
addBtnStyle() {
|
|
1057
|
-
return { padding: '10px', cursor: 'pointer' };
|
|
1058
|
-
}
|
|
322
|
+
@elementDefine(tagName, { window: w })
|
|
323
|
+
class UserWidget extends w.HTMLElement { }
|
|
1059
324
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
325
|
+
// ❌ WRONG
|
|
326
|
+
@Sim()
|
|
327
|
+
@elementDefine(tagName, { window: w })
|
|
328
|
+
class UserWidget extends w.HTMLElement { }
|
|
1066
329
|
```
|
|
1067
330
|
|
|
1068
|
-
|
|
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
|
-
|
|
331
|
+
### Factory Always Returns tagName (String)
|
|
1081
332
|
```typescript
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
}
|
|
333
|
+
export default (w: Window) => {
|
|
334
|
+
const tagName = 'my-element';
|
|
335
|
+
const existing = w.customElements.get(tagName);
|
|
336
|
+
if (existing) return tagName; // ← Return string!
|
|
1104
337
|
|
|
1105
|
-
@
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
}
|
|
338
|
+
@elementDefine(tagName, { window: w })
|
|
339
|
+
class MyElement extends w.HTMLElement { }
|
|
340
|
+
|
|
341
|
+
return tagName; // ← Always return tagName, never class
|
|
342
|
+
};
|
|
1120
343
|
```
|
|
1121
344
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
Note: `:host` selector targets the component itself (boundary), so `root` option is ignored.
|
|
1125
|
-
|
|
1126
|
-
All Host variants support three usage patterns:
|
|
1127
|
-
- Without parentheses: `@setStyleHost` setHostStyle() { ... }
|
|
1128
|
-
- With empty parentheses: `@setStyleHost()` setHostStyle() { ... }
|
|
1129
|
-
- With options: `@setStyleHost({ ... })` setHostStyle() { ... }
|
|
1130
|
-
|
|
345
|
+
### @onInitialize for DI (Not Constructor Parameters)
|
|
1131
346
|
```typescript
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
@updateStyleHost()
|
|
1138
|
-
updateHostStyle() {
|
|
1139
|
-
return { backgroundColor: '#f0f0f0' };
|
|
347
|
+
// ✅ CORRECT
|
|
348
|
+
@onInitialize
|
|
349
|
+
constructor(@Inject({ symbol: Service.SYMBOL }) service: Service) {
|
|
350
|
+
super();
|
|
351
|
+
this.service = service;
|
|
1140
352
|
}
|
|
1141
353
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
return ['backgroundColor'];
|
|
1145
|
-
}
|
|
354
|
+
// ❌ WRONG
|
|
355
|
+
constructor(private service: Service) { super(); } // Web Components can't have constructor parameters!
|
|
1146
356
|
```
|
|
1147
357
|
|
|
1148
358
|
---
|
|
1149
359
|
|
|
1150
|
-
|
|
360
|
+
## 📚 Examples
|
|
1151
361
|
|
|
1152
|
-
|
|
362
|
+
- **[Commerce (E-Commerce SPA)](./examples/commerce)** - Full shopping cart example
|
|
363
|
+
- **[Stock (Market Dashboard)](./examples/stock)** - Real-time data with routing
|
|
364
|
+
- **[Accommodation (Reference Pattern)](./examples/accommodation)** - Standard setup pattern
|
|
1153
365
|
|
|
1154
|
-
|
|
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
|
-
}
|
|
366
|
+
---
|
|
1169
367
|
|
|
1170
|
-
|
|
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
|
-
---
|
|
368
|
+
## 🔗 Related Packages
|
|
3574
369
|
|
|
3575
|
-
|
|
3576
|
-
- **
|
|
3577
|
-
- **
|
|
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.
|
|
370
|
+
- **[@dooboostore/simple-boot](../simple-boot)** - DI & AOP Container
|
|
371
|
+
- **[@dooboostore/core-web](../core-web)** - Web Utilities & Router
|
|
372
|
+
- **[@dooboostore/dom-parser](../dom-parser)** - HTML Parsing & AST
|
|
3581
373
|
|
|
3582
374
|
---
|
|
3583
375
|
|
|
3584
|
-
##
|
|
3585
|
-
MIT
|
|
376
|
+
## 📄 License
|
|
377
|
+
MIT © 2024 dooboostore-develop
|