@dooboostore/simple-web-component 1.0.19 → 1.0.21
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 +2028 -146
- package/dist/cjs/decorators/addEventListener.js +4 -4
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/appendChild.js +4 -4
- package/dist/cjs/decorators/appendChild.js.map +2 -2
- package/dist/cjs/decorators/attribute.js +141 -0
- package/dist/cjs/decorators/attribute.js.map +7 -0
- package/dist/cjs/decorators/changedAttribute.js +5 -5
- package/dist/cjs/decorators/changedAttribute.js.map +2 -2
- package/dist/cjs/decorators/classList.js +4 -4
- package/dist/cjs/decorators/classList.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +36 -9
- package/dist/cjs/decorators/elementDefine.js.map +3 -3
- package/dist/cjs/decorators/emitCustomEvent.js +4 -4
- package/dist/cjs/decorators/emitCustomEvent.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 +4 -4
- package/dist/cjs/decorators/lifecycles.js.map +2 -2
- package/dist/cjs/decorators/onConnectedInnerHtml.js +4 -4
- package/dist/cjs/decorators/onConnectedInnerHtml.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/replaceChildren.js +5 -5
- package/dist/cjs/decorators/replaceChildren.js.map +2 -2
- package/dist/cjs/decorators/setAttribute.js +46 -20
- package/dist/cjs/decorators/setAttribute.js.map +2 -2
- package/dist/cjs/decorators/style.js +4 -4
- package/dist/cjs/decorators/style.js.map +2 -2
- package/dist/cjs/elements/SwcAppEngine.js +8 -8
- package/dist/cjs/elements/SwcAppEngine.js.map +3 -3
- package/dist/cjs/elements/SwcAppMixin.js +28 -1
- package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
- package/dist/cjs/elements/SwcAsync.js +4 -4
- package/dist/cjs/elements/SwcAsync.js.map +2 -2
- package/dist/cjs/elements/SwcChoose.js +4 -4
- package/dist/cjs/elements/SwcChoose.js.map +2 -2
- package/dist/cjs/elements/SwcIf.js +4 -4
- package/dist/cjs/elements/SwcIf.js.map +2 -2
- package/dist/cjs/elements/SwcLoop.js +4 -4
- package/dist/cjs/elements/SwcLoop.js.map +2 -2
- package/dist/cjs/elements/register.js +13 -13
- package/dist/cjs/elements/register.js.map +2 -2
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/router/ElementRouter.js +7 -7
- package/dist/cjs/router/ElementRouter.js.map +2 -2
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/decorators/addEventListener.js +1 -1
- package/dist/esm/decorators/addEventListener.js.map +1 -1
- package/dist/esm/decorators/appendChild.js +1 -1
- package/dist/esm/decorators/appendChild.js.map +1 -1
- package/dist/esm/decorators/attribute.js +122 -0
- package/dist/esm/decorators/attribute.js.map +7 -0
- package/dist/esm/decorators/changedAttribute.js +1 -1
- package/dist/esm/decorators/changedAttribute.js.map +1 -1
- package/dist/esm/decorators/classList.js +1 -1
- package/dist/esm/decorators/classList.js.map +1 -1
- package/dist/esm/decorators/elementDefine.js +31 -4
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/decorators/emitCustomEvent.js +1 -1
- package/dist/esm/decorators/emitCustomEvent.js.map +1 -1
- package/dist/esm/decorators/index.js +1 -0
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/lifecycles.js +1 -1
- package/dist/esm/decorators/lifecycles.js.map +1 -1
- package/dist/esm/decorators/onConnectedInnerHtml.js +1 -1
- package/dist/esm/decorators/onConnectedInnerHtml.js.map +1 -1
- package/dist/esm/decorators/query.js +1 -1
- package/dist/esm/decorators/query.js.map +1 -1
- package/dist/esm/decorators/queryAll.js +1 -1
- package/dist/esm/decorators/queryAll.js.map +1 -1
- package/dist/esm/decorators/replaceChildren.js +1 -1
- package/dist/esm/decorators/replaceChildren.js.map +1 -1
- package/dist/esm/decorators/setAttribute.js +42 -16
- package/dist/esm/decorators/setAttribute.js.map +2 -2
- package/dist/esm/decorators/style.js +1 -1
- package/dist/esm/decorators/style.js.map +1 -1
- package/dist/esm/elements/SwcAppEngine.js +4 -4
- package/dist/esm/elements/SwcAppEngine.js.map +2 -2
- package/dist/esm/elements/SwcAppMixin.js +28 -1
- package/dist/esm/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm/elements/SwcAsync.js +1 -1
- package/dist/esm/elements/SwcAsync.js.map +1 -1
- package/dist/esm/elements/SwcChoose.js +1 -1
- package/dist/esm/elements/SwcChoose.js.map +1 -1
- package/dist/esm/elements/SwcIf.js +1 -1
- package/dist/esm/elements/SwcIf.js.map +1 -1
- package/dist/esm/elements/SwcLoop.js +1 -1
- package/dist/esm/elements/SwcLoop.js.map +1 -1
- package/dist/esm/elements/register.js +1 -1
- package/dist/esm/elements/register.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/router/ElementRouter.js +2 -2
- package/dist/esm/router/ElementRouter.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +5910 -3839
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/decorators/attribute.d.ts +23 -0
- package/dist/types/decorators/attribute.d.ts.map +1 -0
- 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/setAttribute.d.ts +3 -0
- package/dist/types/decorators/setAttribute.d.ts.map +1 -1
- package/dist/types/elements/SwcAppEngine.d.ts +5 -4
- package/dist/types/elements/SwcAppEngine.d.ts.map +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/router/ElementRouter.d.ts +1 -1
- package/dist/types/router/ElementRouter.d.ts.map +1 -1
- package/dist/types/types.d.ts +2 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +5910 -3839
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/package.json +10 -5
- package/src/decorators/addEventListener.ts +1 -1
- package/src/decorators/appendChild.ts +1 -1
- package/src/decorators/attribute.ts +158 -0
- package/src/decorators/changedAttribute.ts +1 -1
- package/src/decorators/classList.ts +1 -1
- package/src/decorators/elementDefine.ts +38 -5
- package/src/decorators/emitCustomEvent.ts +1 -1
- package/src/decorators/index.ts +1 -0
- package/src/decorators/lifecycles.ts +1 -1
- package/src/decorators/onConnectedInnerHtml.ts +1 -1
- package/src/decorators/query.ts +1 -1
- package/src/decorators/queryAll.ts +1 -1
- package/src/decorators/replaceChildren.ts +1 -1
- package/src/decorators/setAttribute.ts +49 -17
- package/src/decorators/style.ts +1 -1
- package/src/elements/SwcAppEngine.ts +9 -8
- package/src/elements/SwcAppMixin.ts +32 -1
- package/src/elements/SwcAsync.ts +1 -1
- package/src/elements/SwcChoose.ts +1 -1
- package/src/elements/SwcIf.ts +1 -1
- package/src/elements/SwcLoop.ts +1 -1
- package/src/elements/register.ts +1 -1
- package/src/index.ts +3 -2
- package/src/router/ElementRouter.ts +2 -2
- package/src/types.ts +2 -1
package/README.md
CHANGED
|
@@ -58,7 +58,13 @@ class MyEl extends HTMLElement {
|
|
|
58
58
|
### 3. Modern Attribute Management
|
|
59
59
|
SWC uses a **Method-Based Reflection** approach. You control exactly how data flows.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
**Attribute sync rules**
|
|
62
|
+
- Initial sync honors DOM-first semantics: if a markup attribute already exists (e.g., `<my-el count="11">`), the first write keeps that DOM value even when the class defines `count = 0`.
|
|
63
|
+
- `connectedInitialize: true` flips the direction for that property so the in-memory value is pushed to the DOM during `connectedCallback`.
|
|
64
|
+
- Type conversion is automatic through `convertValue` so `Boolean`, `Number`, and `String` fields always expose typed values while still storing the serialized attribute on the element.
|
|
65
|
+
- Getters return `null` when the resolved target exists but the attribute is absent, giving you an explicit signal that the DOM is missing that state.
|
|
66
|
+
|
|
67
|
+
- **`@setAttribute(name?, options?)`**: Reflects the method's **return value** to the DOM attribute. Supports `SwcQueryOptions` for root-aware target resolution (light DOM, shadow DOM, or auto-detect).
|
|
62
68
|
- **`@changedAttribute(name?)`**: A handler triggered when the attribute changes.
|
|
63
69
|
|
|
64
70
|
```typescript
|
|
@@ -66,10 +72,14 @@ SWC uses a **Method-Based Reflection** approach. You control exactly how data fl
|
|
|
66
72
|
class MyCounter extends HTMLElement {
|
|
67
73
|
private count = 0;
|
|
68
74
|
@query('#display') displayEl?: HTMLElement;
|
|
75
|
+
@query('#shadow-input', { root: 'shadow' }) shadowInput?: HTMLInputElement;
|
|
69
76
|
|
|
70
77
|
@setAttribute('count')
|
|
71
78
|
updateCount(val: number) { return val; } // Sets 'count' attribute in DOM
|
|
72
79
|
|
|
80
|
+
@setAttribute('shadow-value', { root: 'shadow' })
|
|
81
|
+
updateShadowValue(val: string) { return val; } // Sets attribute on shadow DOM element
|
|
82
|
+
|
|
73
83
|
@changedAttribute('count')
|
|
74
84
|
onCountChange(newVal: string, oldVal: string, name: string) {
|
|
75
85
|
this.count = Number(newVal);
|
|
@@ -82,6 +92,13 @@ class MyCounter extends HTMLElement {
|
|
|
82
92
|
### 4. Powerful Queries (`@query`, `@queryAll`)
|
|
83
93
|
Inject internal elements or host instances directly into properties. Assigning `null` or `[]` to these properties will surgically remove the target elements from the DOM.
|
|
84
94
|
|
|
95
|
+
#### Root Options
|
|
96
|
+
The `root` option controls which DOM tree is searched:
|
|
97
|
+
- **`'auto'`** (default): Searches shadow DOM first, then light DOM
|
|
98
|
+
- **`'light'`**: Searches only light DOM
|
|
99
|
+
- **`'shadow'`**: Searches only shadow DOM
|
|
100
|
+
- **`'all'`**: Searches all ancestors, returning `[lightNodes, shadowNodes]` array
|
|
101
|
+
|
|
85
102
|
#### Usage with Special Targets:
|
|
86
103
|
Special targets (starting with `:`) navigate the hierarchy and environment.
|
|
87
104
|
```typescript
|
|
@@ -101,250 +118,2115 @@ Standard CSS selectors support precise root control.
|
|
|
101
118
|
```
|
|
102
119
|
|
|
103
120
|
### 5. Smart Event Binding (`@addEventListener`)
|
|
104
|
-
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
#### Core Features:
|
|
125
|
+
- **Direct Binding**: Bind to static selectors present at initialization
|
|
126
|
+
- **Event Delegation**: Automatically handle dynamically added elements
|
|
127
|
+
- **Auto-Cleanup**: Automatically remove listeners on `disconnectedCallback`
|
|
128
|
+
- **Context Injection**: Receive `$host`, `$parentHost`, `$appHost` automatically
|
|
129
|
+
- **Special Targets**: `:window`, `:document`, `:parentHost`, `:appHost`
|
|
130
|
+
|
|
131
|
+
#### Pattern 1: Direct Binding & Static Selectors
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
@Sim
|
|
135
|
+
@elementDefine('form-handler')
|
|
136
|
+
class FormHandler extends HTMLElement {
|
|
137
|
+
@query('#submit-btn') submitBtn?: HTMLButtonElement;
|
|
138
|
+
@query('#form') form?: HTMLFormElement;
|
|
139
|
+
|
|
140
|
+
// Bind to a specific static element
|
|
141
|
+
@addEventListener('#submit-btn', 'click')
|
|
142
|
+
onSubmit(event: MouseEvent) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
console.log('Form submitted');
|
|
145
|
+
this.form?.submit();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Bind to multiple elements with selector
|
|
149
|
+
@addEventListener('input[type="text"]', 'input')
|
|
150
|
+
onTextInput(event: Event) {
|
|
151
|
+
console.log('Input value:', (event.target as HTMLInputElement).value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@onConnectedInnerHtml
|
|
155
|
+
render() {
|
|
156
|
+
return `
|
|
157
|
+
<form id="form">
|
|
158
|
+
<input type="text" placeholder="Enter name">
|
|
159
|
+
<input type="email" placeholder="Enter email">
|
|
160
|
+
<button id="submit-btn">Submit</button>
|
|
161
|
+
</form>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Pattern 2: Event Delegation (Dynamic Content)
|
|
168
|
+
|
|
169
|
+
Automatically handle elements added **after component initialization**.
|
|
105
170
|
|
|
106
171
|
```typescript
|
|
107
|
-
@
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
172
|
+
@Sim
|
|
173
|
+
@elementDefine('todo-app')
|
|
174
|
+
class TodoApp extends HTMLElement {
|
|
175
|
+
private todos: any[] = [];
|
|
176
|
+
|
|
177
|
+
constructor(private todoService: TodoService) { super(); }
|
|
178
|
+
|
|
179
|
+
// Delegate: Handles all current AND future .todo-item elements
|
|
180
|
+
@addEventListener('.todo-item', 'click', { delegate: true })
|
|
181
|
+
onTodoClick(event: MouseEvent, hostSet: any, target: HTMLElement) {
|
|
182
|
+
const todoId = target.getAttribute('data-id');
|
|
183
|
+
console.log('Clicked todo:', todoId);
|
|
184
|
+
this.selectTodo(todoId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Event delegation with checkbox - even newly added items react
|
|
188
|
+
@addEventListener('.todo-checkbox', 'change', { delegate: true })
|
|
189
|
+
onTodoToggle(event: Event, hostSet: any, target: HTMLInputElement) {
|
|
190
|
+
const todoId = target.closest('.todo-item')?.getAttribute('data-id');
|
|
191
|
+
const isChecked = target.checked;
|
|
192
|
+
console.log(`Todo ${todoId} ${isChecked ? 'completed' : 'uncompleted'}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async addTodo(text: string) {
|
|
196
|
+
const newTodo = await this.todoService.create(text);
|
|
197
|
+
this.todos.push(newTodo);
|
|
198
|
+
this.render();
|
|
199
|
+
// New .todo-item elements automatically get event listeners!
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
selectTodo(id: string) {
|
|
203
|
+
const todo = this.todos.find(t => t.id === id);
|
|
204
|
+
console.log('Selected:', todo);
|
|
205
|
+
}
|
|
112
206
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
207
|
+
@onConnectedInnerHtml
|
|
208
|
+
render() {
|
|
209
|
+
return `
|
|
210
|
+
<div>
|
|
211
|
+
<input type="text" placeholder="Add todo">
|
|
212
|
+
<button swc-on-click="$host.addTodo(this.previousElementSibling.value)">Add</button>
|
|
213
|
+
<ul>
|
|
214
|
+
${this.todos.map(t => `
|
|
215
|
+
<li class="todo-item" data-id="${t.id}">
|
|
216
|
+
<input type="checkbox" class="todo-checkbox" ${t.done ? 'checked' : ''}>
|
|
217
|
+
<span>${t.text}</span>
|
|
218
|
+
</li>
|
|
219
|
+
`).join('')}
|
|
220
|
+
</ul>
|
|
221
|
+
</div>
|
|
222
|
+
`;
|
|
117
223
|
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Pattern 3: Global Events with Context Access
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
@Sim
|
|
231
|
+
@elementDefine('responsive-layout')
|
|
232
|
+
class ResponsiveLayout extends HTMLElement {
|
|
233
|
+
private isMobile = false;
|
|
118
234
|
|
|
119
|
-
//
|
|
235
|
+
// Listen to window resize with auto-cleanup on disconnect
|
|
120
236
|
@addEventListener(':window', 'resize', { removeOnDisconnected: true })
|
|
121
|
-
|
|
237
|
+
onWindowResize() {
|
|
238
|
+
this.isMobile = window.innerWidth < 768;
|
|
239
|
+
this.updateLayout();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Listen to global scroll with host context
|
|
243
|
+
@addEventListener(':window', 'scroll', { removeOnDisconnected: true })
|
|
244
|
+
onScroll(event: Event, { $appHost }: any) {
|
|
245
|
+
const scrollPos = window.scrollY;
|
|
246
|
+
console.log('Scrolled in app:', $appHost?.tagName, 'Position:', scrollPos);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
updateLayout() {
|
|
250
|
+
this.className = this.isMobile ? 'mobile-layout' : 'desktop-layout';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@onConnectedInnerHtml
|
|
254
|
+
render() {
|
|
255
|
+
return `<div class="${this.isMobile ? 'mobile' : 'desktop'}"><slot></slot></div>`;
|
|
256
|
+
}
|
|
122
257
|
}
|
|
123
258
|
```
|
|
124
259
|
|
|
125
260
|
### 6. `@replaceChildren(selector)`
|
|
126
|
-
|
|
261
|
+
|
|
262
|
+
Surgically replaces the children of a target element. Perfect for dynamic content updates without full-page re-renders.
|
|
263
|
+
|
|
264
|
+
**Use Cases:**
|
|
265
|
+
- Search results updating without page reload
|
|
266
|
+
- Filtering product lists
|
|
267
|
+
- Content swaps in tabbed interfaces
|
|
268
|
+
- Live data refreshes
|
|
269
|
+
|
|
270
|
+
#### Pattern 1: Simple Content Replacement
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
@Sim
|
|
274
|
+
@elementDefine('search-results')
|
|
275
|
+
class SearchResults extends HTMLElement {
|
|
276
|
+
private results: any[] = [];
|
|
277
|
+
private isLoading = false;
|
|
278
|
+
|
|
279
|
+
// Replace #results-container with new search results
|
|
280
|
+
@replaceChildren('#results-container')
|
|
281
|
+
async displayResults(query: string) {
|
|
282
|
+
this.isLoading = true;
|
|
283
|
+
const startTime = Date.now();
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
this.results = await fetch(`/api/search?q=${query}`)
|
|
287
|
+
.then(r => r.json());
|
|
288
|
+
|
|
289
|
+
return htmlFragment`
|
|
290
|
+
<div class="results-header">
|
|
291
|
+
<p>Found ${this.results.length} results in ${Date.now() - startTime}ms</p>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="results-list">
|
|
294
|
+
${this.results.map(item => `
|
|
295
|
+
<div class="result-item" data-id="${item.id}">
|
|
296
|
+
<h3>${item.title}</h3>
|
|
297
|
+
<p>${item.description}</p>
|
|
298
|
+
<a href="${item.url}">View</a>
|
|
299
|
+
</div>
|
|
300
|
+
`).join('')}
|
|
301
|
+
</div>
|
|
302
|
+
`;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return htmlFragment`<p class="error">Search failed: ${error.message}</p>`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@onConnectedInnerHtml
|
|
309
|
+
render() {
|
|
310
|
+
return `
|
|
311
|
+
<div>
|
|
312
|
+
<input type="text" class="search-input" placeholder="Search...">
|
|
313
|
+
<button swc-on-click="$host.search($el.previousElementSibling.value)">Search</button>
|
|
314
|
+
<div id="results-container">
|
|
315
|
+
<p>Enter search term...</p>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
search(query: string) {
|
|
322
|
+
if (query.trim()) {
|
|
323
|
+
this.displayResults(query);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### Pattern 2: Tab Content Switching
|
|
127
330
|
|
|
128
331
|
```typescript
|
|
129
|
-
@
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
332
|
+
@Sim
|
|
333
|
+
@elementDefine('tabbed-panel')
|
|
334
|
+
class TabbedPanel extends HTMLElement {
|
|
335
|
+
private activeTab = 'overview';
|
|
336
|
+
private tabData: any = {
|
|
337
|
+
overview: '<h3>Overview</h3><p>General information...</p>',
|
|
338
|
+
details: '<h3>Details</h3><p>Detailed specifications...</p>',
|
|
339
|
+
reviews: '<h3>Reviews</h3><p>Customer reviews...</p>'
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Click tab and automatically replace content
|
|
343
|
+
@replaceChildren('.tab-content')
|
|
344
|
+
switchTab(tabName: string) {
|
|
345
|
+
this.activeTab = tabName;
|
|
346
|
+
|
|
347
|
+
// Update tab buttons visual state
|
|
348
|
+
Array.from(this.querySelectorAll('.tab-btn')).forEach(btn => {
|
|
349
|
+
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Return new content
|
|
353
|
+
return htmlFragment`${this.tabData[tabName]}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@onConnectedInnerHtml
|
|
357
|
+
render() {
|
|
358
|
+
return `
|
|
359
|
+
<div class="tabs">
|
|
360
|
+
<button class="tab-btn active" data-tab="overview"
|
|
361
|
+
swc-on-click="$host.switchTab('overview')">Overview</button>
|
|
362
|
+
<button class="tab-btn" data-tab="details"
|
|
363
|
+
swc-on-click="$host.switchTab('details')">Details</button>
|
|
364
|
+
<button class="tab-btn" data-tab="reviews"
|
|
365
|
+
swc-on-click="$host.switchTab('reviews')">Reviews</button>
|
|
366
|
+
</div>
|
|
367
|
+
<div class="tab-content">${this.tabData.overview}</div>
|
|
368
|
+
`;
|
|
135
369
|
}
|
|
136
370
|
}
|
|
137
371
|
```
|
|
138
372
|
|
|
139
373
|
### 7. `@appendChild(selector, options?)`
|
|
140
|
-
|
|
141
|
-
|
|
374
|
+
|
|
375
|
+
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.
|
|
376
|
+
|
|
377
|
+
**Position Options:**
|
|
378
|
+
- **`'lastChild'`** (default): Append to end
|
|
379
|
+
- **`'firstChild'`**: Prepend to beginning
|
|
380
|
+
- **`'nextSibling'`**: Insert after element
|
|
381
|
+
- **`'previousSibling'`**: Insert before element
|
|
382
|
+
|
|
383
|
+
#### Pattern 1: Dynamic List Building
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
@Sim
|
|
387
|
+
@elementDefine('task-list')
|
|
388
|
+
class TaskList extends HTMLElement {
|
|
389
|
+
private tasks: any[] = [];
|
|
390
|
+
private taskIdCounter = 0;
|
|
391
|
+
|
|
392
|
+
// Append new task to bottom of list
|
|
393
|
+
@appendChild('#task-list', { position: 'lastChild' })
|
|
394
|
+
addTask(title: string, priority: 'low' | 'medium' | 'high' = 'medium') {
|
|
395
|
+
const id = ++this.taskIdCounter;
|
|
396
|
+
const bgColor = { low: '#e8f5e9', medium: '#fff3e0', high: '#ffebee' }[priority];
|
|
397
|
+
|
|
398
|
+
return htmlFragment`
|
|
399
|
+
<div class="task-item" data-id="${id}" style="background: ${bgColor}; padding: 12px; margin: 8px 0; border-radius: 4px;">
|
|
400
|
+
<div class="task-header">
|
|
401
|
+
<input type="checkbox" class="task-checkbox" data-id="${id}">
|
|
402
|
+
<span class="task-title">${title}</span>
|
|
403
|
+
<span class="task-priority">${priority.toUpperCase()}</span>
|
|
404
|
+
</div>
|
|
405
|
+
<button class="task-delete" swc-on-click="$host.deleteTask(${id})">🗑️ Delete</button>
|
|
406
|
+
</div>
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Prepend urgent task to top of list
|
|
411
|
+
@appendChild('#task-list', { position: 'firstChild' })
|
|
412
|
+
addUrgentTask(title: string) {
|
|
413
|
+
return htmlFragment`
|
|
414
|
+
<div class="task-item urgent" style="background: #ffcdd2; padding: 12px; border: 2px solid #f44336;">
|
|
415
|
+
<strong>🚨 URGENT: ${title}</strong>
|
|
416
|
+
<button swc-on-click="$host.deleteTask()">Close</button>
|
|
417
|
+
</div>
|
|
418
|
+
`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
deleteTask(id?: number) {
|
|
422
|
+
console.log('Delete task:', id);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@onConnectedInnerHtml
|
|
426
|
+
render() {
|
|
427
|
+
return `
|
|
428
|
+
<div>
|
|
429
|
+
<div class="task-input">
|
|
430
|
+
<input type="text" id="new-task" placeholder="Enter task...">
|
|
431
|
+
<select id="priority">
|
|
432
|
+
<option value="low">Low</option>
|
|
433
|
+
<option value="medium" selected>Medium</option>
|
|
434
|
+
<option value="high">High</option>
|
|
435
|
+
</select>
|
|
436
|
+
<button swc-on-click="
|
|
437
|
+
const input = document.getElementById('new-task');
|
|
438
|
+
const priority = document.getElementById('priority').value;
|
|
439
|
+
if (priority === 'high') {
|
|
440
|
+
$host.addUrgentTask(input.value);
|
|
441
|
+
} else {
|
|
442
|
+
$host.addTask(input.value, priority);
|
|
443
|
+
}
|
|
444
|
+
input.value = '';
|
|
445
|
+
input.focus();
|
|
446
|
+
">Add Task</button>
|
|
447
|
+
</div>
|
|
448
|
+
<div id="task-list"></div>
|
|
449
|
+
</div>
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Pattern 2: Notification Stack
|
|
142
456
|
|
|
143
457
|
```typescript
|
|
144
|
-
@
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
458
|
+
@Sim
|
|
459
|
+
@elementDefine('notification-stack')
|
|
460
|
+
class NotificationStack extends HTMLElement {
|
|
461
|
+
private notificationId = 0;
|
|
462
|
+
|
|
463
|
+
// Append notifications to stack (they auto-dismiss)
|
|
464
|
+
@appendChild('.notifications', { position: 'lastChild' })
|
|
465
|
+
notify(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info', duration = 5000) {
|
|
466
|
+
const id = ++this.notificationId;
|
|
467
|
+
const bgColor = { info: '#e3f2fd', success: '#e8f5e9', warning: '#fff3e0', error: '#ffebee' }[type];
|
|
468
|
+
const borderColor = { info: '#2196f3', success: '#4caf50', warning: '#ff9800', error: '#f44336' }[type];
|
|
469
|
+
|
|
470
|
+
// Auto-remove after duration
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
const el = document.querySelector(`[data-notify-id="${id}"]`);
|
|
473
|
+
if (el) el.remove();
|
|
474
|
+
}, duration);
|
|
475
|
+
|
|
476
|
+
return htmlFragment`
|
|
477
|
+
<div class="notification" data-notify-id="${id}"
|
|
478
|
+
style="background: ${bgColor}; border-left: 4px solid ${borderColor}; padding: 16px; margin: 8px 0; border-radius: 4px; display: flex; justify-content: space-between;">
|
|
479
|
+
<span>${message}</span>
|
|
480
|
+
<button swc-on-click="this.closest('[data-notify-id]').remove()"
|
|
481
|
+
style="background: none; border: none; cursor: pointer; font-size: 18px;">×</button>
|
|
482
|
+
</div>
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@onConnectedInnerHtml
|
|
487
|
+
render() {
|
|
488
|
+
return `
|
|
489
|
+
<div class="notifications" style="position: fixed; top: 20px; right: 20px; width: 300px; z-index: 1000;"></div>
|
|
490
|
+
`;
|
|
150
491
|
}
|
|
151
492
|
}
|
|
493
|
+
|
|
494
|
+
// Usage
|
|
495
|
+
const notifier = document.querySelector('notification-stack');
|
|
496
|
+
notifier.notify('Operation successful!', 'success');
|
|
497
|
+
notifier.notify('Something went wrong', 'error');
|
|
152
498
|
```
|
|
153
499
|
|
|
154
500
|
---
|
|
155
501
|
|
|
156
502
|
## 🛠 Template Utilities
|
|
157
503
|
|
|
158
|
-
SWC provides powerful utilities to create DOM nodes using Tagged Template Literals
|
|
504
|
+
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`.
|
|
505
|
+
|
|
506
|
+
### Complete Factory List
|
|
507
|
+
|
|
508
|
+
SWC provides element factories for **all common HTML tags**:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import {
|
|
512
|
+
htmlFragment, textNode,
|
|
513
|
+
htmlDivElement, htmlSpanElement, htmlButtonElement,
|
|
514
|
+
htmlInputElement, htmlFormElement,
|
|
515
|
+
htmlTableElement, htmlTrElement, htmlTdElement,
|
|
516
|
+
htmlUlElement, htmlLiElement,
|
|
517
|
+
htmlHeadingElement, htmlImageElement,
|
|
518
|
+
// ... 50+ more factories
|
|
519
|
+
} from '@dooboostore/simple-web-component';
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### 1. `htmlFragment` - DOM Fragments
|
|
523
|
+
|
|
524
|
+
Returns a `DocumentFragment` from an HTML string. Perfect for multi-element insertion.
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
@Sim
|
|
528
|
+
@elementDefine('fragment-demo')
|
|
529
|
+
class FragmentDemo extends HTMLElement {
|
|
530
|
+
@replaceChildren('#container')
|
|
531
|
+
renderMultipleElements() {
|
|
532
|
+
// All elements wrapped in one fragment for efficient insertion
|
|
533
|
+
return htmlFragment`
|
|
534
|
+
<h2>Title</h2>
|
|
535
|
+
<p>First paragraph</p>
|
|
536
|
+
<p>Second paragraph</p>
|
|
537
|
+
<ul>
|
|
538
|
+
<li>Item 1</li>
|
|
539
|
+
<li>Item 2</li>
|
|
540
|
+
</ul>
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@onConnectedInnerHtml
|
|
545
|
+
render() {
|
|
546
|
+
return `<div id="container"></div>`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 2. `textNode` - Pure Text Nodes
|
|
552
|
+
|
|
553
|
+
Create raw text nodes (useful for escaping HTML or building text content).
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
@Sim
|
|
557
|
+
@elementDefine('text-renderer')
|
|
558
|
+
class TextRenderer extends HTMLElement {
|
|
559
|
+
@appendChild('.text-list', { position: 'lastChild' })
|
|
560
|
+
addPlainText(userInput: string) {
|
|
561
|
+
// Create pure text node - prevents XSS
|
|
562
|
+
return textNode`User wrote: ${userInput}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@onConnectedInnerHtml
|
|
566
|
+
render() {
|
|
567
|
+
return `
|
|
568
|
+
<input type="text" id="user-input" placeholder="Enter text...">
|
|
569
|
+
<button swc-on-click="
|
|
570
|
+
const input = document.getElementById('user-input');
|
|
571
|
+
$host.addPlainText(input.value);
|
|
572
|
+
input.value = '';
|
|
573
|
+
">Add Text</button>
|
|
574
|
+
<div class="text-list"></div>
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
159
579
|
|
|
160
|
-
###
|
|
161
|
-
- **`htmlFragment\`...\``**: Returns a `DocumentFragment` from an HTML string.
|
|
162
|
-
- **`textNode\`...\``**: Returns a pure `Text` node.
|
|
580
|
+
### 3. Element Factories (`html***Element`)
|
|
163
581
|
|
|
164
|
-
|
|
165
|
-
Specific factories for common tags like `htmlDivElement`, `htmlSpanElement`, `htmlButtonElement`, etc.
|
|
582
|
+
Specific factories for common tags. Each returns a **typed element instance**.
|
|
166
583
|
|
|
167
584
|
```typescript
|
|
168
|
-
|
|
585
|
+
@Sim
|
|
586
|
+
@elementDefine('element-factory-demo')
|
|
587
|
+
class ElementFactoryDemo extends HTMLElement {
|
|
588
|
+
// htmlDivElement returns HTMLDivElement (typed)
|
|
589
|
+
@appendChild('#container')
|
|
590
|
+
addCard(title: string, description: string) {
|
|
591
|
+
return htmlDivElement`
|
|
592
|
+
<div class="card">
|
|
593
|
+
<h3>${title}</h3>
|
|
594
|
+
<p>${description}</p>
|
|
595
|
+
<button class="card-btn">Learn More</button>
|
|
596
|
+
</div>
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// htmlTableElement for building tables
|
|
601
|
+
@replaceChildren('#table-container')
|
|
602
|
+
buildTable(data: Array<{ name: string; age: number; role: string }>) {
|
|
603
|
+
const headerRow = htmlTrElement`
|
|
604
|
+
<tr style="background: #f0f0f0; font-weight: bold;">
|
|
605
|
+
<td>Name</td>
|
|
606
|
+
<td>Age</td>
|
|
607
|
+
<td>Role</td>
|
|
608
|
+
</tr>
|
|
609
|
+
`;
|
|
610
|
+
|
|
611
|
+
const rows = data.map(item =>
|
|
612
|
+
htmlTrElement`
|
|
613
|
+
<tr>
|
|
614
|
+
<td>${item.name}</td>
|
|
615
|
+
<td>${item.age}</td>
|
|
616
|
+
<td>${item.role}</td>
|
|
617
|
+
</tr>
|
|
618
|
+
`
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return htmlTableElement`
|
|
622
|
+
${headerRow}
|
|
623
|
+
${rows}
|
|
624
|
+
`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// htmlUlElement for lists
|
|
628
|
+
@appendChild('.sidebar')
|
|
629
|
+
addMenuList(items: string[]) {
|
|
630
|
+
const listItems = items.map(item =>
|
|
631
|
+
htmlLiElement`<li><a href="#">${item}</a></li>`
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
return htmlUlElement`
|
|
635
|
+
<ul class="menu">
|
|
636
|
+
${listItems}
|
|
637
|
+
</ul>
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@onConnectedInnerHtml
|
|
642
|
+
render() {
|
|
643
|
+
return `
|
|
644
|
+
<div id="container"></div>
|
|
645
|
+
<div id="table-container"></div>
|
|
646
|
+
<div class="sidebar"></div>
|
|
647
|
+
`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## ⚡ Quick Start
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
import 'reflect-metadata';
|
|
658
|
+
import swcRegister, { elementDefine, onConnectedInnerHtml } from '@dooboostore/simple-web-component';
|
|
659
|
+
|
|
660
|
+
swcRegister(window);
|
|
661
|
+
|
|
662
|
+
@elementDefine('hello-swc')
|
|
663
|
+
class HelloSwc extends HTMLElement {
|
|
664
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
665
|
+
render() {
|
|
666
|
+
return `<style>:host{display:block;padding:12px;background:#111;color:#fefefe}</style><slot>Hello SWC</slot>`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
169
669
|
|
|
170
|
-
|
|
670
|
+
// <hello-swc> contents render automatically once the element is connected.
|
|
171
671
|
```
|
|
172
672
|
|
|
173
|
-
|
|
174
|
-
|
|
673
|
+
- Call `swcRegister(window)` once per window to register structural elements (`swc-if`, `swc-loop`, etc.) globally.
|
|
674
|
+
- Decorators rely on `reflect-metadata`, so make sure it is imported before defining elements.
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 🧪 Interactive Test Cases
|
|
679
|
+
|
|
680
|
+
Every decorator and directive ships with runnable playgrounds under [test/case/src](test/case/src). They double as living documentation.
|
|
681
|
+
|
|
682
|
+
1. Install dependencies at the repo root: `pnpm install`.
|
|
683
|
+
2. Start the playground server:
|
|
684
|
+
```bash
|
|
685
|
+
cd packages/@dooboostore/simple-web-component/test/case
|
|
686
|
+
pnpm run dev
|
|
687
|
+
```
|
|
688
|
+
3. Open `http://localhost:3005` and pick a scenario from the menu.
|
|
689
|
+
|
|
690
|
+
| Scenario | Folder | Highlights |
|
|
691
|
+
| --- | --- | --- |
|
|
692
|
+
| Lifecycle hooks | `lifecycle/` | All pre/post connect, disconnect, adopt hooks with parent-child ordering. |
|
|
693
|
+
| Attribute flow | `attr/` and `attribute-test/` | DOM-first attribute sync, `@setAttribute`, `@changedAttribute`, `connectedInitialize` edge cases. |
|
|
694
|
+
| Events | `event/` and `event-attribute/` | Direct binding, delegation, special selectors, `swc-on-*`, custom event emission. |
|
|
695
|
+
| Queries | `query/` | `@query`, `@queryAll`, special selectors like `:appHost`, element injection safety. |
|
|
696
|
+
| Structural directives | `swc-loop/`, `swc-if/`, `swc-choose/`, `swc-async/` | Surgical list handling, portal rendering, async template states. |
|
|
697
|
+
| SPA & DI | `spa/` | Multiple `<swc-app>` instances, DI containers, nested routers. |
|
|
698
|
+
| Performance | `load-test/` | 1,000+ node stress tests showcasing zero MutationObserver overhead. |
|
|
699
|
+
|
|
700
|
+
Each folder contains an `index.ts` that demonstrates the decorator usage and an `index.html` you can load standalone if needed.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## 🧱 Project Layout
|
|
705
|
+
|
|
706
|
+
- [src/](src) holds the production TypeScript sources (decorators, structural elements, router, utilities).
|
|
707
|
+
- [test/case/src](test/case/src) aggregates example applications used in documentation and regression checks.
|
|
708
|
+
- [test/case/src/README.md](test/case/src/README.md) provides an overview of every scenario along with the architectural ideas they validate.
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 🛠 Developer Scripts
|
|
713
|
+
|
|
714
|
+
Inside [package.json](package.json) you will find task shortcuts:
|
|
715
|
+
|
|
716
|
+
- `pnpm run build` – produce ESM, CJS, and bundle artifacts (`dist/`).
|
|
717
|
+
- `pnpm run watch` – rebuild all targets on file changes.
|
|
718
|
+
- `pnpm run build:esm` / `build:cjs` – individual module formats when you only care about one output.
|
|
719
|
+
- `pnpm run test:bundle` – launches the bundle regression playground (`@dooboostore/simple-web-component-bundle-test`).
|
|
720
|
+
- `pnpm run typecheck` – validate TypeScript types without emitting output.
|
|
721
|
+
|
|
722
|
+
Publishing uses `publish:npm`, which runs a clean build, packs artifacts, and pushes them with the correct metadata.
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## 🌐 Server-Side Rendering (SSR) & Hydration
|
|
727
|
+
|
|
728
|
+
SWC provides seamless SSR support with automatic tag-name preservation and client-side hydration strategies.
|
|
729
|
+
|
|
730
|
+
### SSR Features
|
|
731
|
+
- **Tag Name Preservation**: Custom element tags (e.g., `<user-route>`, `<index-router>`) are preserved through the SSR virtual DOM layer.
|
|
732
|
+
- **Metadata-Based Registration**: The `CustomElementRegistry` stores tagname metadata during `define()`, allowing `HTMLElementBase` to auto-infer correct tag names during construction.
|
|
733
|
+
- **Virtual Window Support**: Pass a custom window object to `@elementDefine()` for SSR rendering environments.
|
|
734
|
+
|
|
735
|
+
### Hydration Modes
|
|
736
|
+
When connecting a server-rendered app to the client, choose a connection strategy via the `connectMode` config:
|
|
737
|
+
|
|
175
738
|
```typescript
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
739
|
+
const config = {
|
|
740
|
+
rootRouter: RootRouter,
|
|
741
|
+
routeType: 'element',
|
|
742
|
+
connectMode: 'swap' // or 'direct' (default)
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
app.connect(config);
|
|
180
746
|
```
|
|
181
747
|
|
|
748
|
+
#### Mode Behaviors:
|
|
749
|
+
- **`'direct'`** (default): Directly connects the existing rendered DOM tree.
|
|
750
|
+
- **`'swap'`**: Creates a clone of the rendered app, connects it, and smoothly replaces the original DOM (Zero FOUC - Flash of Unstyled Content).
|
|
751
|
+
|
|
752
|
+
The swap mode is ideal for SSR workflows where you want to:
|
|
753
|
+
1. Render components on the server with proper tag names
|
|
754
|
+
2. Send minimal HTML to the client
|
|
755
|
+
3. Clone and connect on the client side without visible DOM flashing
|
|
756
|
+
|
|
182
757
|
---
|
|
183
758
|
|
|
184
|
-
## 🌐
|
|
759
|
+
## 🌐 Revolutionary SPA Architecture
|
|
760
|
+
|
|
761
|
+
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.
|
|
762
|
+
|
|
763
|
+
### Core Philosophy: Multi-App Composition
|
|
764
|
+
Unlike traditional SPAs that force a global router and monolithic state tree, SWC allows:
|
|
765
|
+
- **Nested Independent Apps**: Multiple `<swc-app>` instances on the same page, each with zero cross-contamination
|
|
766
|
+
- **Autonomous Routing**: Each app handles its own navigation, history, and component lifecycle independently
|
|
767
|
+
- **Decentralized DI**: Service dependencies are resolved within app scope, enabling natural code splitting and lazy loading
|
|
768
|
+
- **Island Architecture**: Apps boot only when needed, reducing initial JavaScript payload
|
|
769
|
+
|
|
770
|
+
---
|
|
185
771
|
|
|
186
|
-
|
|
772
|
+
### 1. Isolated App Scopes & Micro-Frontend Pattern
|
|
187
773
|
|
|
188
|
-
|
|
189
|
-
Use the `<swc-app>` tag to create an isolated universe. Each instance has its own **DI Container** and **Router**.
|
|
774
|
+
Create a self-contained universe with `<swc-app>`. The tag acts as a **boundary** where routing, DI, and state are completely isolated from sibling apps.
|
|
190
775
|
|
|
191
776
|
```html
|
|
192
|
-
<!--
|
|
193
|
-
<
|
|
777
|
+
<!-- Example: Multi-tenant dashboard with independent feature modules -->
|
|
778
|
+
<div class="dashboard">
|
|
779
|
+
<!-- User Management Module -->
|
|
780
|
+
<swc-app swc-get-application-config="createUserModule()"></swc-app>
|
|
781
|
+
|
|
782
|
+
<!-- Reports Module -->
|
|
783
|
+
<swc-app swc-get-application-config="createReportsModule()"></swc-app>
|
|
784
|
+
|
|
785
|
+
<!-- Settings Module -->
|
|
786
|
+
<swc-app swc-get-application-config="createSettingsModule()"></swc-app>
|
|
787
|
+
</div>
|
|
194
788
|
|
|
195
789
|
<script>
|
|
196
|
-
//
|
|
197
|
-
window.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
790
|
+
// Each module is bootstrapped independently
|
|
791
|
+
window.createUserModule = (host) => ({
|
|
792
|
+
rootRouter: UserManagementRouter, // Entry point router
|
|
793
|
+
routeType: 'element', // 'element' (in-memory), 'hash', or 'path'
|
|
794
|
+
path: '/dashboard', // Initial route
|
|
795
|
+
window: window // Optional: SSR virtual window
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
window.createReportsModule = (host) => ({
|
|
799
|
+
rootRouter: ReportsRouter,
|
|
800
|
+
routeType: 'element',
|
|
801
|
+
path: '/overview',
|
|
802
|
+
window: window
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// External programmatic navigation (from parent scope)
|
|
806
|
+
function switchUserModule(userId) {
|
|
807
|
+
const userApp = document.querySelectorAll('swc-app')[0];
|
|
808
|
+
userApp.routing(`/user/${userId}`); // Navigate within the isolated app
|
|
809
|
+
}
|
|
205
810
|
|
|
206
|
-
//
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
app.
|
|
811
|
+
// Inter-app communication (if needed)
|
|
812
|
+
function notifyModules(eventName, data) {
|
|
813
|
+
document.querySelectorAll('swc-app').forEach(app => {
|
|
814
|
+
app.dispatchEvent(new CustomEvent(eventName, { detail: data }));
|
|
815
|
+
});
|
|
210
816
|
}
|
|
211
817
|
</script>
|
|
212
818
|
```
|
|
213
819
|
|
|
214
|
-
|
|
820
|
+
**Key Insights:**
|
|
821
|
+
- Each `<swc-app>` is **completely isolated**: one app's router state doesn't affect another
|
|
822
|
+
- No global namespace pollution: services, routes, and components live within app scope
|
|
823
|
+
- Perfect for **dashboard layouts, plugin architectures, and federated frontends**
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
### 2. Advanced DI-Powered Routing with Lifecycle Hooks
|
|
828
|
+
|
|
829
|
+
SWC routing leverages **Dependency Injection** to inject services directly into components, enabling dynamic route guards, permission checks, and data preloading.
|
|
830
|
+
|
|
215
831
|
```typescript
|
|
832
|
+
// ============================================
|
|
833
|
+
// 1. SERVICE LAYER (Shared by all routes)
|
|
834
|
+
// ============================================
|
|
216
835
|
@Sim
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
render() {
|
|
225
|
-
return `<header>User System</header><main><slot></slot></main>`;
|
|
836
|
+
class AuthService {
|
|
837
|
+
private user: { id: string; role: 'admin' | 'user' } | null = null;
|
|
838
|
+
|
|
839
|
+
async login(credentials: { username: string; password: string }) {
|
|
840
|
+
// Simulate API call
|
|
841
|
+
this.user = { id: 'user123', role: 'admin' };
|
|
842
|
+
return this.user;
|
|
226
843
|
}
|
|
227
844
|
|
|
228
|
-
async
|
|
229
|
-
|
|
845
|
+
async logout() {
|
|
846
|
+
this.user = null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
getCurrentUser() {
|
|
850
|
+
return this.user;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
hasRole(role: string) {
|
|
854
|
+
return this.user?.role === role;
|
|
230
855
|
}
|
|
231
856
|
}
|
|
232
857
|
|
|
233
858
|
@Sim
|
|
234
859
|
class UserService {
|
|
235
|
-
|
|
860
|
+
constructor(private auth: AuthService) {}
|
|
861
|
+
|
|
862
|
+
async getUsers() {
|
|
863
|
+
if (!this.auth.hasRole('admin')) {
|
|
864
|
+
throw new Error('Unauthorized');
|
|
865
|
+
}
|
|
866
|
+
return [
|
|
867
|
+
{ id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
868
|
+
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
|
|
869
|
+
];
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async getUserById(id: string) {
|
|
873
|
+
return { id, name: 'User ' + id, email: `user${id}@example.com` };
|
|
874
|
+
}
|
|
236
875
|
}
|
|
237
876
|
|
|
877
|
+
// ============================================
|
|
878
|
+
// 2. ROUTE COMPONENTS (Nested routing)
|
|
879
|
+
// ============================================
|
|
238
880
|
@Sim
|
|
239
|
-
@
|
|
240
|
-
|
|
241
|
-
|
|
881
|
+
@Router({
|
|
882
|
+
path: '/admin',
|
|
883
|
+
route: {
|
|
884
|
+
'/users': UserListPage,
|
|
885
|
+
'/users/:id': UserDetailPage,
|
|
886
|
+
'/settings': SettingsPage
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
@elementDefine('admin-router')
|
|
890
|
+
class AdminRouter extends HTMLElement implements RouterAction.CanActivate {
|
|
891
|
+
constructor(private auth: AuthService) { super(); }
|
|
242
892
|
|
|
243
|
-
@onConnectedInnerHtml
|
|
244
|
-
render() {
|
|
893
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
894
|
+
render() {
|
|
245
895
|
return `
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
896
|
+
<style>
|
|
897
|
+
:host { display: flex; height: 100vh; }
|
|
898
|
+
.sidebar { width: 200px; background: #333; color: white; padding: 20px; }
|
|
899
|
+
.sidebar nav a { display: block; margin: 10px 0; color: #0066cc; cursor: pointer; }
|
|
900
|
+
.content { flex: 1; padding: 20px; }
|
|
901
|
+
</style>
|
|
902
|
+
<div class="sidebar">
|
|
903
|
+
<h3>Admin Panel</h3>
|
|
904
|
+
<nav>
|
|
905
|
+
<a swc-on-click="$host.router.go('/admin/users')">Users</a>
|
|
906
|
+
<a swc-on-click="$host.router.go('/admin/settings')">Settings</a>
|
|
907
|
+
</nav>
|
|
908
|
+
</div>
|
|
909
|
+
<div class="content">
|
|
910
|
+
<slot></slot>
|
|
911
|
+
</div>
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Lifecycle hook: Called BEFORE route transition
|
|
916
|
+
// Used for permission checks, data preloading, etc.
|
|
917
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
918
|
+
// Only admin can enter this route
|
|
919
|
+
if (!this.auth.hasRole('admin')) {
|
|
920
|
+
console.warn('Access Denied: Admin role required');
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// If data is a rendered component (Node), inject it into DOM
|
|
925
|
+
if (data instanceof Node) {
|
|
926
|
+
this.replaceChildren(data);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return true;
|
|
249
930
|
}
|
|
250
931
|
}
|
|
251
932
|
|
|
933
|
+
// ============================================
|
|
934
|
+
// 3. PAGE COMPONENTS (Leaf routes)
|
|
935
|
+
// ============================================
|
|
252
936
|
@Sim
|
|
253
|
-
@elementDefine('user-
|
|
254
|
-
class
|
|
255
|
-
|
|
937
|
+
@elementDefine('user-list-page')
|
|
938
|
+
class UserListPage extends HTMLElement {
|
|
939
|
+
private users: any[] = [];
|
|
940
|
+
|
|
941
|
+
constructor(
|
|
942
|
+
private userService: UserService,
|
|
943
|
+
private router: Router,
|
|
944
|
+
private auth: AuthService
|
|
945
|
+
) {
|
|
946
|
+
super();
|
|
947
|
+
}
|
|
256
948
|
|
|
257
949
|
@onConnected
|
|
258
|
-
async
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
950
|
+
async loadUsers() {
|
|
951
|
+
try {
|
|
952
|
+
this.users = await this.userService.getUsers();
|
|
953
|
+
this.render();
|
|
954
|
+
} catch (error) {
|
|
955
|
+
console.error('Failed to load users:', error);
|
|
956
|
+
this.innerHTML = '<p style="color: red;">Failed to load users</p>';
|
|
957
|
+
}
|
|
262
958
|
}
|
|
263
959
|
|
|
960
|
+
@classList('active')
|
|
961
|
+
isActive = false;
|
|
962
|
+
|
|
264
963
|
@onConnectedInnerHtml
|
|
265
|
-
render() {
|
|
266
|
-
|
|
267
|
-
|
|
964
|
+
render() {
|
|
965
|
+
return `
|
|
966
|
+
<style>
|
|
967
|
+
.user-table { width: 100%; border-collapse: collapse; }
|
|
968
|
+
.user-table th, .user-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
969
|
+
.user-table tr:hover { background: #f5f5f5; }
|
|
970
|
+
</style>
|
|
971
|
+
<h2>Users</h2>
|
|
972
|
+
<table class="user-table">
|
|
973
|
+
<thead>
|
|
974
|
+
<tr><th>Name</th><th>Email</th><th>Actions</th></tr>
|
|
975
|
+
</thead>
|
|
976
|
+
<tbody>
|
|
977
|
+
${this.users.map(u => `
|
|
978
|
+
<tr>
|
|
979
|
+
<td>${u.name}</td>
|
|
980
|
+
<td>${u.email}</td>
|
|
981
|
+
<td>
|
|
982
|
+
<button swc-on-click="$host.router.go('/admin/users/${u.id}')">View</button>
|
|
983
|
+
<button swc-on-click="$host.deleteUser('${u.id}')">Delete</button>
|
|
984
|
+
</td>
|
|
985
|
+
</tr>
|
|
986
|
+
`).join('')}
|
|
987
|
+
</tbody>
|
|
988
|
+
</table>
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
268
991
|
|
|
269
|
-
|
|
992
|
+
deleteUser(id: string) {
|
|
993
|
+
if (confirm('Delete this user?')) {
|
|
994
|
+
this.users = this.users.filter(u => u.id !== id);
|
|
995
|
+
this.render();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
270
999
|
|
|
271
|
-
|
|
1000
|
+
@Sim
|
|
1001
|
+
@elementDefine('user-detail-page')
|
|
1002
|
+
class UserDetailPage extends HTMLElement {
|
|
1003
|
+
private user: any = null;
|
|
1004
|
+
|
|
1005
|
+
constructor(
|
|
1006
|
+
private userService: UserService,
|
|
1007
|
+
private router: Router
|
|
1008
|
+
) {
|
|
1009
|
+
super();
|
|
1010
|
+
}
|
|
272
1011
|
|
|
273
|
-
|
|
1012
|
+
@onConnected
|
|
1013
|
+
async loadUser() {
|
|
1014
|
+
// Extract route parameter (:id)
|
|
1015
|
+
const id = this.router.getIntent()?.params?.id;
|
|
1016
|
+
if (id) {
|
|
1017
|
+
this.user = await this.userService.getUserById(id);
|
|
1018
|
+
this.render();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
274
1021
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
- **`:parentHost`**: Send a direct message to the immediate parent host.
|
|
279
|
-
- **`:firstHost` / `:firstAppHost`**: Target the absolute root component or the top-level SPA.
|
|
1022
|
+
@onConnectedInnerHtml
|
|
1023
|
+
render() {
|
|
1024
|
+
if (!this.user) return '<p>Loading...</p>';
|
|
280
1025
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
1026
|
+
return `
|
|
1027
|
+
<style>
|
|
1028
|
+
.detail { max-width: 600px; background: #f9f9f9; padding: 20px; border-radius: 8px; }
|
|
1029
|
+
.detail-row { margin: 10px 0; }
|
|
1030
|
+
.detail-label { font-weight: bold; color: #333; }
|
|
1031
|
+
</style>
|
|
1032
|
+
<h2>User Details</h2>
|
|
1033
|
+
<div class="detail">
|
|
1034
|
+
<div class="detail-row">
|
|
1035
|
+
<span class="detail-label">ID:</span> ${this.user.id}
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="detail-row">
|
|
1038
|
+
<span class="detail-label">Name:</span> ${this.user.name}
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="detail-row">
|
|
1041
|
+
<span class="detail-label">Email:</span> ${this.user.email}
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<button swc-on-click="$host.router.back()">Back</button>
|
|
1045
|
+
`;
|
|
1046
|
+
}
|
|
287
1047
|
}
|
|
288
1048
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
1049
|
+
// ============================================
|
|
1050
|
+
// 4. ROOT ROUTER (Entry point)
|
|
1051
|
+
// ============================================
|
|
1052
|
+
@Sim
|
|
1053
|
+
@Router({
|
|
1054
|
+
path: '/',
|
|
1055
|
+
route: {
|
|
1056
|
+
'/': HomePage,
|
|
1057
|
+
'/login': LoginPage,
|
|
1058
|
+
'/admin': AdminRouter
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
@elementDefine('app-router')
|
|
1062
|
+
class RootRouter extends HTMLElement implements RouterAction.CanActivate {
|
|
1063
|
+
constructor(private auth: AuthService) { super(); }
|
|
1064
|
+
|
|
1065
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
1066
|
+
render() {
|
|
1067
|
+
const user = this.auth.getCurrentUser();
|
|
1068
|
+
return `
|
|
1069
|
+
<style>
|
|
1070
|
+
:host { display: block; font-family: Arial, sans-serif; }
|
|
1071
|
+
header { background: #222; color: white; padding: 15px; }
|
|
1072
|
+
header .user-info { float: right; font-size: 12px; }
|
|
1073
|
+
</style>
|
|
1074
|
+
<header>
|
|
1075
|
+
<h1>SWC App</h1>
|
|
1076
|
+
<div class="user-info">
|
|
1077
|
+
${user ? `Logged in as: ${user.id} (${user.role})` : 'Not logged in'}
|
|
1078
|
+
</div>
|
|
1079
|
+
</header>
|
|
1080
|
+
<main><slot></slot></main>
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
1085
|
+
if (data instanceof Node) {
|
|
1086
|
+
this.replaceChildren(data);
|
|
1087
|
+
}
|
|
1088
|
+
return true;
|
|
295
1089
|
}
|
|
296
1090
|
}
|
|
297
1091
|
```
|
|
298
1092
|
|
|
299
1093
|
---
|
|
300
1094
|
|
|
301
|
-
|
|
302
|
-
SWC automatically traverses the DOM to provide meaningful context to your scripts and listeners:
|
|
1095
|
+
### 3. Route Lifecycle & Guard Patterns
|
|
303
1096
|
|
|
304
|
-
|
|
305
|
-
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
1097
|
+
**The `canActivate` lifecycle hook** is called **before** entering a route. Use it for:
|
|
1098
|
+
- **Permission Checks**: Verify user roles and access rights
|
|
1099
|
+
- **Data Preloading**: Fetch required data before rendering
|
|
1100
|
+
- **Route Interception**: Redirect to login or error pages
|
|
1101
|
+
- **Analytics**: Track user navigation
|
|
308
1102
|
|
|
309
|
-
|
|
310
|
-
|
|
1103
|
+
```typescript
|
|
1104
|
+
@Router({ path: '/protected', route: { '/data': ProtectedDataPage } })
|
|
1105
|
+
@elementDefine('protected-router')
|
|
1106
|
+
class ProtectedRouter extends HTMLElement implements RouterAction.CanActivate {
|
|
1107
|
+
constructor(private auth: AuthService) { super(); }
|
|
311
1108
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
1109
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
1110
|
+
// Guard: Redirect if not authenticated
|
|
1111
|
+
if (!this.auth.getCurrentUser()) {
|
|
1112
|
+
console.log('Redirecting to login...');
|
|
1113
|
+
this.parentElement?.querySelector('app-router')?.routing('/login');
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Data injection: Component receives pre-fetched data
|
|
1118
|
+
if (data instanceof Node) {
|
|
1119
|
+
this.replaceChildren(data);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
319
1125
|
```
|
|
320
1126
|
|
|
321
1127
|
---
|
|
322
1128
|
|
|
323
|
-
|
|
1129
|
+
### 4. Advanced Patterns: Nested Routers & State Management
|
|
324
1130
|
|
|
325
|
-
|
|
1131
|
+
```typescript
|
|
1132
|
+
// Real-world: Multi-level nested routing with shared state
|
|
1133
|
+
@Sim
|
|
1134
|
+
class GridStateService {
|
|
1135
|
+
private sortBy = 'name';
|
|
1136
|
+
private filterRole = '';
|
|
1137
|
+
|
|
1138
|
+
setSortBy(field: string) { this.sortBy = field; }
|
|
1139
|
+
setFilterRole(role: string) { this.filterRole = role; }
|
|
1140
|
+
getSortBy() { return this.sortBy; }
|
|
1141
|
+
getFilterRole() { return this.filterRole; }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@Sim
|
|
1145
|
+
@Router({
|
|
1146
|
+
path: '/crm',
|
|
1147
|
+
route: {
|
|
1148
|
+
'/contacts': ContactsRouter,
|
|
1149
|
+
'/accounts': AccountsRouter
|
|
1150
|
+
}
|
|
1151
|
+
})
|
|
1152
|
+
@elementDefine('crm-app')
|
|
1153
|
+
class CRMApp extends HTMLElement implements RouterAction.CanActivate {
|
|
1154
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
1155
|
+
if (data instanceof Node) this.replaceChildren(data);
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
@onConnectedInnerHtml
|
|
1160
|
+
render() {
|
|
1161
|
+
return `<div><slot></slot></div>`;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## ✉️ Declarative & Scoped Messaging System
|
|
1169
|
+
|
|
1170
|
+
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.
|
|
1171
|
+
|
|
1172
|
+
### Message Scope Hierarchy
|
|
1173
|
+
|
|
1174
|
+
Messages can target different scopes using **Special Selectors**:
|
|
1175
|
+
|
|
1176
|
+
| Selector | Scope | Use Case |
|
|
1177
|
+
|----------|-------|----------|
|
|
1178
|
+
| **`:appHost`** | Same `<swc-app>` instance | Module-to-module communication (confined) |
|
|
1179
|
+
| **`:parentHost`** | Direct parent SWC component | Parent-child coordination |
|
|
1180
|
+
| **`:window` / `:document`** | Entire page | Global broadcasts (Cross-app signals) |
|
|
1181
|
+
| **`:firstHost` / `:firstAppHost`** | Root of hierarchy | Communication with top-level container |
|
|
1182
|
+
| **`selector`** (CSS) | Direct query result | Point-to-point messaging |
|
|
1183
|
+
|
|
1184
|
+
### Pattern 1: App-Scoped Communication
|
|
1185
|
+
|
|
1186
|
+
Communicate within a micro-app without affecting siblings.
|
|
1187
|
+
|
|
1188
|
+
```typescript
|
|
1189
|
+
// ========== SENDER COMPONENT ==========
|
|
1190
|
+
@Sim
|
|
1191
|
+
@elementDefine('auth-panel')
|
|
1192
|
+
class AuthPanel extends HTMLElement {
|
|
1193
|
+
constructor(private auth: AuthService) { super(); }
|
|
1194
|
+
|
|
1195
|
+
@onConnectedInnerHtml
|
|
1196
|
+
render() {
|
|
1197
|
+
return `
|
|
1198
|
+
<div style="padding: 10px; background: #f0f0f0; border-radius: 4px;">
|
|
1199
|
+
<button swc-on-click="$host.onLogin()">Login</button>
|
|
1200
|
+
<button swc-on-click="$host.onLogout()">Logout</button>
|
|
1201
|
+
</div>
|
|
1202
|
+
`;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Emit event to the nearest parent <swc-app>
|
|
1206
|
+
@emitCustomEvent(':appHost', 'auth-changed')
|
|
1207
|
+
async onLogin() {
|
|
1208
|
+
await this.auth.login({ username: 'demo', password: 'demo' });
|
|
1209
|
+
return { user: this.auth.getCurrentUser(), timestamp: Date.now() };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
@emitCustomEvent(':appHost', 'auth-changed')
|
|
1213
|
+
async onLogout() {
|
|
1214
|
+
await this.auth.logout();
|
|
1215
|
+
return { user: null, timestamp: Date.now() };
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ========== RECEIVER COMPONENT (SAME APP) ==========
|
|
1220
|
+
@Sim
|
|
1221
|
+
@elementDefine('user-widget')
|
|
1222
|
+
class UserWidget extends HTMLElement {
|
|
1223
|
+
private user: any = null;
|
|
1224
|
+
|
|
1225
|
+
@onConnectedInnerHtml
|
|
1226
|
+
render() {
|
|
1227
|
+
return `
|
|
1228
|
+
<div style="padding: 10px; background: #e8f4f8; border-radius: 4px;">
|
|
1229
|
+
${this.user ? `<p>Welcome, ${this.user.name}!</p>` : '<p>Not logged in</p>'}
|
|
1230
|
+
</div>
|
|
1231
|
+
`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Listen for auth changes within the same app
|
|
1235
|
+
@addEventListener(':appHost', 'auth-changed')
|
|
1236
|
+
onAuthChanged(event: CustomEvent) {
|
|
1237
|
+
const { detail } = event;
|
|
1238
|
+
this.user = detail.user;
|
|
1239
|
+
this.render();
|
|
1240
|
+
console.log('Auth state changed at:', new Date(detail.timestamp).toLocaleTimeString());
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// HTML
|
|
1245
|
+
<swc-app swc-get-application-config="dashboardConfig()">
|
|
1246
|
+
<auth-panel></auth-panel>
|
|
1247
|
+
<user-widget></user-widget> <!-- Reacts to auth-changed event -->
|
|
1248
|
+
</swc-app>
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
**Result**: When user clicks "Login" in `<auth-panel>`, `<user-widget>` is notified and updates automatically. No explicit imperative connection needed.
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
### Pattern 2: Cross-Module (Global) Broadcasting
|
|
1256
|
+
|
|
1257
|
+
Send a signal that reaches all apps on the page.
|
|
1258
|
+
|
|
1259
|
+
```typescript
|
|
1260
|
+
// ========== SENDER (In Module A) ==========
|
|
1261
|
+
@Sim
|
|
1262
|
+
@elementDefine('notification-sender')
|
|
1263
|
+
class NotificationSender extends HTMLElement {
|
|
1264
|
+
@emitCustomEvent(':window', 'system-notification')
|
|
1265
|
+
announceSystemMaintenance() {
|
|
1266
|
+
return {
|
|
1267
|
+
title: 'System Maintenance',
|
|
1268
|
+
message: 'Server will be down for 2 hours',
|
|
1269
|
+
severity: 'warning',
|
|
1270
|
+
timestamp: new Date().toISOString()
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
@onConnectedInnerHtml
|
|
1275
|
+
render() {
|
|
1276
|
+
return `
|
|
1277
|
+
<button swc-on-click="$host.announceSystemMaintenance()">
|
|
1278
|
+
📢 Broadcast Maintenance Alert
|
|
1279
|
+
</button>
|
|
1280
|
+
`;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ========== RECEIVER (In Module B, Different App) ==========
|
|
1285
|
+
@Sim
|
|
1286
|
+
@elementDefine('notification-display')
|
|
1287
|
+
class NotificationDisplay extends HTMLElement {
|
|
1288
|
+
private notifications: any[] = [];
|
|
1289
|
+
|
|
1290
|
+
@addEventListener(':window', 'system-notification')
|
|
1291
|
+
onSystemNotification(event: CustomEvent) {
|
|
1292
|
+
this.notifications.push(event.detail);
|
|
1293
|
+
this.render();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
@onConnectedInnerHtml
|
|
1297
|
+
render() {
|
|
1298
|
+
return `
|
|
1299
|
+
<div style="position: fixed; top: 10px; right: 10px; font-size: 12px;">
|
|
1300
|
+
${this.notifications.map(n => `
|
|
1301
|
+
<div style="background: ${n.severity === 'warning' ? '#fff3cd' : '#fff'};
|
|
1302
|
+
border: 1px solid #ffc107; padding: 10px; margin: 5px 0;
|
|
1303
|
+
border-radius: 4px; max-width: 300px;">
|
|
1304
|
+
<strong>${n.title}</strong><br>
|
|
1305
|
+
${n.message}
|
|
1306
|
+
</div>
|
|
1307
|
+
`).join('')}
|
|
1308
|
+
</div>
|
|
1309
|
+
`;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
<!-- Two independent apps receiving the same broadcast -->
|
|
1314
|
+
<swc-app swc-get-application-config="moduleAConfig()">
|
|
1315
|
+
<notification-sender></notification-sender>
|
|
1316
|
+
</swc-app>
|
|
1317
|
+
|
|
1318
|
+
<swc-app swc-get-application-config="moduleBConfig()">
|
|
1319
|
+
<notification-display></notification-display>
|
|
1320
|
+
</swc-app>
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
**Result**: Clicking the button in Module A instantly triggers the notification in Module B, even though they're in separate `<swc-app>` instances.
|
|
1324
|
+
|
|
1325
|
+
---
|
|
1326
|
+
|
|
1327
|
+
### Pattern 3: Hierarchical Parent-Child Messaging
|
|
1328
|
+
|
|
1329
|
+
Communicate up and down the component tree with `$parentHost` and `:hosts` selector.
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
// ========== PARENT CONTAINER ==========
|
|
1333
|
+
@Sim
|
|
1334
|
+
@elementDefine('modal-container')
|
|
1335
|
+
class ModalContainer extends HTMLElement {
|
|
1336
|
+
private modals: any[] = [];
|
|
1337
|
+
|
|
1338
|
+
@onConnectedInnerHtml
|
|
1339
|
+
render() {
|
|
1340
|
+
return `
|
|
1341
|
+
<div style="position: relative;">
|
|
1342
|
+
<slot></slot>
|
|
1343
|
+
${this.modals.map(m => `
|
|
1344
|
+
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
1345
|
+
background: white; border: 2px solid #333; padding: 20px;
|
|
1346
|
+
border-radius: 8px; z-index: 1000;">
|
|
1347
|
+
${m.content}
|
|
1348
|
+
<button swc-on-click="$host.closeModal()">Close</button>
|
|
1349
|
+
</div>
|
|
1350
|
+
`).join('')}
|
|
1351
|
+
</div>
|
|
1352
|
+
`;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Listen for child components requesting to open a modal
|
|
1356
|
+
@addEventListener(':hosts', 'request-modal')
|
|
1357
|
+
onModalRequest(event: CustomEvent) {
|
|
1358
|
+
this.modals.push({ id: Date.now(), content: event.detail.html });
|
|
1359
|
+
this.render();
|
|
1360
|
+
event.stopPropagation();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
closeModal() {
|
|
1364
|
+
this.modals.pop();
|
|
1365
|
+
this.render();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// ========== CHILD COMPONENT ==========
|
|
1370
|
+
@Sim
|
|
1371
|
+
@elementDefine('form-with-modal')
|
|
1372
|
+
class FormWithModal extends HTMLElement {
|
|
1373
|
+
@emitCustomEvent(':parentHost', 'request-modal')
|
|
1374
|
+
showHelpModal() {
|
|
1375
|
+
return {
|
|
1376
|
+
html: '<h3>Help</h3><p>Fill in all required fields and click Submit.</p>'
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
@onConnectedInnerHtml
|
|
1381
|
+
render() {
|
|
1382
|
+
return `
|
|
1383
|
+
<form>
|
|
1384
|
+
<input type="text" placeholder="Full Name" required>
|
|
1385
|
+
<button type="button" swc-on-click="$host.showHelpModal()">?</button>
|
|
1386
|
+
<button type="submit">Submit</button>
|
|
1387
|
+
</form>
|
|
1388
|
+
`;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
<!-- Structure -->
|
|
1393
|
+
<modal-container>
|
|
1394
|
+
<form-with-modal></form-with-modal>
|
|
1395
|
+
</modal-container>
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
**Result**: Clicking "?" in the child form opens a modal managed by the parent container.
|
|
1399
|
+
|
|
1400
|
+
---
|
|
1401
|
+
|
|
1402
|
+
### Pattern 4: Event Detail Payloads
|
|
1403
|
+
|
|
1404
|
+
Send rich data structures through events.
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
@Sim
|
|
1408
|
+
@elementDefine('data-grid')
|
|
1409
|
+
class DataGrid extends HTMLElement {
|
|
1410
|
+
private rows: any[] = [];
|
|
1411
|
+
|
|
1412
|
+
@onConnected
|
|
1413
|
+
loadData() {
|
|
1414
|
+
this.rows = [
|
|
1415
|
+
{ id: 1, name: 'Alice', age: 28, department: 'Engineering' },
|
|
1416
|
+
{ id: 2, name: 'Bob', age: 34, department: 'Sales' }
|
|
1417
|
+
];
|
|
1418
|
+
this.render();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Emit complex payload when row is clicked
|
|
1422
|
+
@emitCustomEvent(':appHost', 'row-selected', false) // 3rd arg: whether to bubble
|
|
1423
|
+
selectRow(rowId: number) {
|
|
1424
|
+
const row = this.rows.find(r => r.id === rowId);
|
|
1425
|
+
return {
|
|
1426
|
+
rowId,
|
|
1427
|
+
rowData: row,
|
|
1428
|
+
timestamp: Date.now(),
|
|
1429
|
+
gridId: this.id,
|
|
1430
|
+
userAgent: navigator.userAgent
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
@onConnectedInnerHtml
|
|
1435
|
+
render() {
|
|
1436
|
+
return `
|
|
1437
|
+
<table>
|
|
1438
|
+
<tbody>
|
|
1439
|
+
${this.rows.map(row => `
|
|
1440
|
+
<tr swc-on-click="$host.selectRow(${row.id})">
|
|
1441
|
+
<td>${row.name}</td>
|
|
1442
|
+
<td>${row.age}</td>
|
|
1443
|
+
<td>${row.department}</td>
|
|
1444
|
+
</tr>
|
|
1445
|
+
`).join('')}
|
|
1446
|
+
</tbody>
|
|
1447
|
+
</table>
|
|
1448
|
+
`;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Listener receives all payload details
|
|
1453
|
+
@Sim
|
|
1454
|
+
@elementDefine('detail-panel')
|
|
1455
|
+
class DetailPanel extends HTMLElement {
|
|
1456
|
+
private selectedData: any = null;
|
|
1457
|
+
|
|
1458
|
+
@addEventListener(':appHost', 'row-selected')
|
|
1459
|
+
onRowSelect(event: CustomEvent) {
|
|
1460
|
+
const { rowData, timestamp } = event.detail;
|
|
1461
|
+
this.selectedData = rowData;
|
|
1462
|
+
console.log(`Selected at ${new Date(timestamp).toLocaleTimeString()}`);
|
|
1463
|
+
this.render();
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
@onConnectedInnerHtml
|
|
1467
|
+
render() {
|
|
1468
|
+
if (!this.selectedData) return '<p>No selection</p>';
|
|
1469
|
+
return `
|
|
1470
|
+
<div>
|
|
1471
|
+
<h3>${this.selectedData.name}</h3>
|
|
1472
|
+
<p>Age: ${this.selectedData.age}</p>
|
|
1473
|
+
<p>Department: ${this.selectedData.department}</p>
|
|
1474
|
+
</div>
|
|
1475
|
+
`;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
---
|
|
1481
|
+
|
|
1482
|
+
## 🏰 Host Hierarchy, Context Propagation & Template Events
|
|
1483
|
+
|
|
1484
|
+
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.
|
|
1485
|
+
|
|
1486
|
+
### Context Variables (In Templates & Event Handlers)
|
|
1487
|
+
|
|
1488
|
+
When you use `swc-on-*` inline events or callback functions, these variables are automatically injected:
|
|
1489
|
+
|
|
1490
|
+
| Variable | Type | Description |
|
|
1491
|
+
|----------|------|-------------|
|
|
1492
|
+
| **`$host`** | `HTMLElement` | The component that owns the event handler (the element itself) |
|
|
1493
|
+
| **`$parentHost`** | `HTMLElement` | The direct parent SWC component |
|
|
1494
|
+
| **`$appHost`** | `SwcAppInterface` | The nearest parent `<swc-app>` boundary |
|
|
1495
|
+
| **`$hosts`** | `HTMLElement[]` | Array of ALL SWC ancestors: `[Root, Parent, ..., Self]` |
|
|
1496
|
+
| **`$el`** | `HTMLElement` | The actual DOM element that triggered the event |
|
|
1497
|
+
| **`$item`** | `any` | (In `swc-loop`) Current iteration item |
|
|
1498
|
+
| **`$index`** | `number` | (In `swc-loop`) Current iteration index |
|
|
1499
|
+
|
|
1500
|
+
### Pattern 1: Declarative Inline Events (`swc-on-*`)
|
|
1501
|
+
|
|
1502
|
+
Bind events directly in HTML templates. The context is automatically available.
|
|
1503
|
+
|
|
1504
|
+
```typescript
|
|
1505
|
+
@Sim
|
|
1506
|
+
@elementDefine('todo-item')
|
|
1507
|
+
class TodoItem extends HTMLElement {
|
|
1508
|
+
private title = '';
|
|
1509
|
+
private done = false;
|
|
1510
|
+
|
|
1511
|
+
constructor(private todoService: TodoService) { super(); }
|
|
1512
|
+
|
|
1513
|
+
@onConnectedInnerHtml
|
|
1514
|
+
render() {
|
|
1515
|
+
return `
|
|
1516
|
+
<style>
|
|
1517
|
+
.completed { text-decoration: line-through; color: #ccc; }
|
|
1518
|
+
button { margin-left: 10px; padding: 2px 8px; cursor: pointer; }
|
|
1519
|
+
</style>
|
|
1520
|
+
<li class="${this.done ? 'completed' : ''}">
|
|
1521
|
+
<!-- Access $host (self), $appHost (parent app), and $item -->
|
|
1522
|
+
<span>${this.title}</span>
|
|
1523
|
+
|
|
1524
|
+
<!-- Inline script with full context -->
|
|
1525
|
+
<button swc-on-click="
|
|
1526
|
+
$host.toggleDone();
|
|
1527
|
+
console.log('Current item:', $item);
|
|
1528
|
+
console.log('App instance:', $appHost);
|
|
1529
|
+
">
|
|
1530
|
+
${this.done ? '✓ Done' : '○ Pending'}
|
|
1531
|
+
</button>
|
|
1532
|
+
|
|
1533
|
+
<button swc-on-click="$host.deleteTodo($index)">
|
|
1534
|
+
🗑️ Delete
|
|
1535
|
+
</button>
|
|
1536
|
+
|
|
1537
|
+
<!-- Access element coordinates -->
|
|
1538
|
+
<button swc-on-click="
|
|
1539
|
+
console.log('Clicked at:', $el.getBoundingClientRect());
|
|
1540
|
+
">
|
|
1541
|
+
Info
|
|
1542
|
+
</button>
|
|
1543
|
+
</li>
|
|
1544
|
+
`;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
toggleDone() {
|
|
1548
|
+
this.done = !this.done;
|
|
1549
|
+
this.render();
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
deleteTodo(index: number) {
|
|
1553
|
+
console.log('Deleting todo at index:', index);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
### Pattern 2: Loop Iteration Context
|
|
1559
|
+
|
|
1560
|
+
`swc-loop` provides special iteration variables you can access in inline events.
|
|
1561
|
+
|
|
1562
|
+
```typescript
|
|
1563
|
+
@Sim
|
|
1564
|
+
@elementDefine('todo-list')
|
|
1565
|
+
class TodoList extends HTMLElement {
|
|
1566
|
+
private todos = [
|
|
1567
|
+
{ id: 1, title: 'Learn SWC', done: false },
|
|
1568
|
+
{ id: 2, title: 'Build an app', done: false }
|
|
1569
|
+
];
|
|
1570
|
+
|
|
1571
|
+
@onConnectedInnerHtml
|
|
1572
|
+
render() {
|
|
1573
|
+
return `
|
|
1574
|
+
<ul>
|
|
1575
|
+
<template is="swc-loop" on-get-value="todos"
|
|
1576
|
+
on-clone-node="
|
|
1577
|
+
if ($isElement) {
|
|
1578
|
+
console.log('Cloning item:', $item, 'at index:', $index);
|
|
1579
|
+
$node.querySelector('.index').textContent = $index + 1;
|
|
1580
|
+
}
|
|
1581
|
+
">
|
|
1582
|
+
<li class="todo-item">
|
|
1583
|
+
<span class="index">0</span>.
|
|
1584
|
+
<span class="title">${'${item.title}'}</span>
|
|
1585
|
+
<!-- Callback receives full context -->
|
|
1586
|
+
<button swc-on-click="
|
|
1587
|
+
$host.editTodo($item.id, $index, $el);
|
|
1588
|
+
console.log('Hosts hierarchy:', $hosts.map(h => h.tagName));
|
|
1589
|
+
">
|
|
1590
|
+
Edit
|
|
1591
|
+
</button>
|
|
1592
|
+
</li>
|
|
1593
|
+
</template>
|
|
1594
|
+
</ul>
|
|
1595
|
+
`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
editTodo(id: number, index: number, element: HTMLElement) {
|
|
1599
|
+
console.log(`Editing todo #${id} (index: ${index}):`, element);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### Pattern 3: Accessing Ancestor Services via Context
|
|
1605
|
+
|
|
1606
|
+
Use `$appHost` to access services from the parent app's DI container.
|
|
1607
|
+
|
|
1608
|
+
```typescript
|
|
1609
|
+
@Sim
|
|
1610
|
+
@elementDefine('child-component')
|
|
1611
|
+
class ChildComponent extends HTMLElement {
|
|
1612
|
+
@onConnectedInnerHtml
|
|
1613
|
+
render() {
|
|
1614
|
+
return `
|
|
1615
|
+
<div>
|
|
1616
|
+
<!-- Access parent app's router to navigate -->
|
|
1617
|
+
<button swc-on-click="$appHost.routing('/dashboard')">
|
|
1618
|
+
Go to Dashboard
|
|
1619
|
+
</button>
|
|
1620
|
+
|
|
1621
|
+
<!-- Access other components in the app -->
|
|
1622
|
+
<button swc-on-click="
|
|
1623
|
+
const sibling = document.querySelector('other-component');
|
|
1624
|
+
sibling.triggerUpdate();
|
|
1625
|
+
">
|
|
1626
|
+
Notify Sibling
|
|
1627
|
+
</button>
|
|
1628
|
+
|
|
1629
|
+
<!-- Emit event up the tree to parent app -->
|
|
1630
|
+
<button swc-on-click="
|
|
1631
|
+
$appHost.dispatchEvent(new CustomEvent('child-ready', {
|
|
1632
|
+
detail: { child: $host, timestamp: Date.now() }
|
|
1633
|
+
}));
|
|
1634
|
+
">
|
|
1635
|
+
Notify Parent App
|
|
1636
|
+
</button>
|
|
1637
|
+
</div>
|
|
1638
|
+
`;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
### Pattern 4: Host Hierarchy Traversal
|
|
1644
|
+
|
|
1645
|
+
Manually traverse the component tree to find ancestors or siblings.
|
|
1646
|
+
|
|
1647
|
+
```typescript
|
|
1648
|
+
@Sim
|
|
1649
|
+
@elementDefine('deep-child')
|
|
1650
|
+
class DeepChild extends HTMLElement {
|
|
1651
|
+
findRootApp() {
|
|
1652
|
+
// Traverse up until you find <swc-app> or root
|
|
1653
|
+
let current = this.parentElement as any;
|
|
1654
|
+
while (current && current.tagName !== 'SWC-APP') {
|
|
1655
|
+
current = current.parentElement;
|
|
1656
|
+
}
|
|
1657
|
+
return current;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
getSiblingCount() {
|
|
1661
|
+
// Count siblings at same level
|
|
1662
|
+
return this.parentElement?.querySelectorAll(':scope > *').length || 0;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
@onConnectedInnerHtml
|
|
1666
|
+
render() {
|
|
1667
|
+
return `
|
|
1668
|
+
<div>
|
|
1669
|
+
<p>Root app: ${this.findRootApp()?.getAttribute('id')}</p>
|
|
1670
|
+
<p>Sibling count: ${this.getSiblingCount()}</p>
|
|
1671
|
+
</div>
|
|
1672
|
+
`;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
### Real-World Example: Multi-Level Navigation with Context
|
|
1678
|
+
|
|
1679
|
+
```typescript
|
|
1680
|
+
// ============ ROOT ROUTER ============
|
|
1681
|
+
@Sim
|
|
1682
|
+
@Router({ path: '/', route: { '/dashboard': Dashboard, '/profile': Profile } })
|
|
1683
|
+
@elementDefine('app-root')
|
|
1684
|
+
class AppRoot extends HTMLElement {
|
|
1685
|
+
private breadcrumbs: string[] = [];
|
|
1686
|
+
|
|
1687
|
+
@onConnectedInnerHtml
|
|
1688
|
+
render() {
|
|
1689
|
+
return `
|
|
1690
|
+
<div>
|
|
1691
|
+
<header>
|
|
1692
|
+
<h1>My App</h1>
|
|
1693
|
+
<nav>
|
|
1694
|
+
<button swc-on-click="$host.router.go('/dashboard')">Dashboard</button>
|
|
1695
|
+
<button swc-on-click="$host.router.go('/profile')">Profile</button>
|
|
1696
|
+
</nav>
|
|
1697
|
+
<p>Breadcrumbs: ${this.breadcrumbs.join(' > ')}</p>
|
|
1698
|
+
</header>
|
|
1699
|
+
<main><slot></slot></main>
|
|
1700
|
+
</div>
|
|
1701
|
+
`;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ============ CHILD ROUTE ============
|
|
1706
|
+
@Sim
|
|
1707
|
+
@elementDefine('dashboard-page')
|
|
1708
|
+
class DashboardPage extends HTMLElement {
|
|
1709
|
+
@onConnectedInnerHtml
|
|
1710
|
+
render() {
|
|
1711
|
+
return `
|
|
1712
|
+
<div>
|
|
1713
|
+
<h2>Dashboard</h2>
|
|
1714
|
+
<button swc-on-click="
|
|
1715
|
+
console.log('App router:', $appHost.routing);
|
|
1716
|
+
console.log('My tag:', $host.tagName);
|
|
1717
|
+
console.log('All ancestors:', $hosts.map(h => h.tagName).join(' -> '));
|
|
1718
|
+
">
|
|
1719
|
+
Debug Context
|
|
1720
|
+
</button>
|
|
1721
|
+
</div>
|
|
1722
|
+
`;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
---
|
|
1728
|
+
|
|
1729
|
+
## 🏰 Advanced Host Context: Query & Portal Patterns
|
|
1730
|
+
|
|
1731
|
+
### Custom Query with Root Options
|
|
1732
|
+
|
|
1733
|
+
Combine `@query/@queryAll` with the `root` option from host methods to precision-target elements.
|
|
1734
|
+
|
|
1735
|
+
```typescript
|
|
1736
|
+
@Sim
|
|
1737
|
+
@elementDefine('form-validator')
|
|
1738
|
+
class FormValidator extends HTMLElement {
|
|
1739
|
+
@query('form', { root: 'light' }) lightForm?: HTMLFormElement;
|
|
1740
|
+
@query('.hidden-form', { root: 'shadow' }) shadowForm?: HTMLFormElement;
|
|
1741
|
+
@queryAll('input', { root: 'all' }) allInputs?: HTMLInputElement[];
|
|
1742
|
+
|
|
1743
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
1744
|
+
render() {
|
|
1745
|
+
return `
|
|
1746
|
+
<form class="shadow-form">
|
|
1747
|
+
<input type="text" placeholder="Shadow DOM input">
|
|
1748
|
+
</form>
|
|
1749
|
+
<slot></slot>
|
|
1750
|
+
`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
validateAllForms() {
|
|
1754
|
+
// Validate both light and shadow DOM forms
|
|
1755
|
+
const allInputs = Array.from(this.allInputs || []);
|
|
1756
|
+
return allInputs.every(input => input.value.trim() !== '');
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
```
|
|
1760
|
+
|
|
1761
|
+
### Portal Pattern: Render Content Anywhere
|
|
1762
|
+
|
|
1763
|
+
Use `on-get-portal` to render content at a completely different location in the DOM.
|
|
1764
|
+
|
|
1765
|
+
```typescript
|
|
1766
|
+
@Sim
|
|
1767
|
+
@elementDefine('tooltip-manager')
|
|
1768
|
+
class TooltipManager extends HTMLElement {
|
|
1769
|
+
private tooltips: any[] = [];
|
|
1770
|
+
|
|
1771
|
+
@onConnectedInnerHtml
|
|
1772
|
+
render() {
|
|
1773
|
+
return `
|
|
1774
|
+
<style>
|
|
1775
|
+
.tooltip { position: fixed; background: #222; color: white;
|
|
1776
|
+
padding: 8px 12px; border-radius: 4px; font-size: 12px; }
|
|
1777
|
+
</style>
|
|
1778
|
+
<template is="swc-loop" on-get-value="tooltips"
|
|
1779
|
+
on-get-portal="document.body">
|
|
1780
|
+
<div class="tooltip" style="left: ${'${item.x}px'}; top: ${'${item.y}px'}">
|
|
1781
|
+
${'${item.text}'}
|
|
1782
|
+
</div>
|
|
1783
|
+
</template>
|
|
1784
|
+
`;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
addTooltip(text: string, x: number, y: number) {
|
|
1788
|
+
this.tooltips.push({ text, x, y });
|
|
1789
|
+
this.render();
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Usage: Tooltip renders outside component hierarchy
|
|
1794
|
+
const tooltipMgr = document.querySelector('tooltip-manager');
|
|
1795
|
+
tooltipMgr.addTooltip('Hover info', 100, 50);
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
## 🔄 Structural Directives (Advanced)
|
|
1801
|
+
|
|
1802
|
+
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**.
|
|
326
1803
|
|
|
327
1804
|
### Common Attributes:
|
|
328
|
-
- **`on-get-value`**: Script to fetch data (
|
|
329
|
-
- **`on-clone-
|
|
330
|
-
- **`on-clone-
|
|
331
|
-
- **`on-get-portal`**:
|
|
1805
|
+
- **`on-get-value`**: Script to fetch data (evaluated in component context).
|
|
1806
|
+
- **`on-clone-node`**: Callback fired for **each individual node clone** (Args: `$node`, `$item`, `$index`, `$isElement`, etc.).
|
|
1807
|
+
- **`on-clone-nodes`**: Callback fired **once after all nodes cloned** (Args: `$nodes`, `$elements`, `$state`, etc.).
|
|
1808
|
+
- **`on-get-portal`**: Render nodes at a different DOM location using a selector.
|
|
1809
|
+
- **`on-state-changed`**: Track rendering state changes.
|
|
332
1810
|
|
|
333
|
-
### 1. `swc-if`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
1811
|
+
### 1. `swc-if` - Binary Conditional Rendering
|
|
1812
|
+
|
|
1813
|
+
Show/hide content based on a boolean expression. No DOM wrapper.
|
|
1814
|
+
|
|
1815
|
+
```typescript
|
|
1816
|
+
@Sim
|
|
1817
|
+
@elementDefine('access-control')
|
|
1818
|
+
class AccessControl extends HTMLElement {
|
|
1819
|
+
private user: any = null;
|
|
1820
|
+
private isLoading = false;
|
|
1821
|
+
|
|
1822
|
+
@onConnectedInnerHtml
|
|
1823
|
+
render() {
|
|
1824
|
+
return `
|
|
1825
|
+
<div class="access-panel">
|
|
1826
|
+
<!-- Show ONLY if loading (no wrapper) -->
|
|
1827
|
+
<template is="swc-if" on-get-value="isLoading">
|
|
1828
|
+
<div class="loading-spinner">
|
|
1829
|
+
<spinner-component></spinner-component>
|
|
1830
|
+
<p>Verifying access...</p>
|
|
1831
|
+
</div>
|
|
1832
|
+
</template>
|
|
1833
|
+
|
|
1834
|
+
<!-- Show ONLY if user is authenticated -->
|
|
1835
|
+
<template is="swc-if" on-get-value="user">
|
|
1836
|
+
<template is="swc-default">
|
|
1837
|
+
<p class="not-authorized">⛔ You are not authorized to view this content.</p>
|
|
1838
|
+
</template>
|
|
1839
|
+
<div class="admin-content">
|
|
1840
|
+
<h2>Welcome, \${user?.name}! 🎉</h2>
|
|
1841
|
+
<p>VIP Access Level: \${user?.role}</p>
|
|
1842
|
+
</div>
|
|
1843
|
+
</template>
|
|
1844
|
+
</div>
|
|
1845
|
+
`;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
339
1848
|
```
|
|
340
1849
|
|
|
341
|
-
### 2. `swc-loop`
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1850
|
+
### 2. `swc-loop` - High-Performance List Rendering
|
|
1851
|
+
|
|
1852
|
+
Iterate over arrays with **index-preserving stability** and zero virtual DOM overhead.
|
|
1853
|
+
|
|
1854
|
+
```typescript
|
|
1855
|
+
@Sim
|
|
1856
|
+
@elementDefine('product-grid')
|
|
1857
|
+
class ProductGrid extends HTMLElement {
|
|
1858
|
+
private products = [
|
|
1859
|
+
{ id: 1, name: 'Laptop', price: 999 },
|
|
1860
|
+
{ id: 2, name: 'Mouse', price: 29 }
|
|
1861
|
+
];
|
|
1862
|
+
|
|
1863
|
+
@onConnectedInnerHtml
|
|
1864
|
+
render() {
|
|
1865
|
+
return `
|
|
1866
|
+
<template is="swc-loop" on-get-value="products"
|
|
1867
|
+
on-clone-node="
|
|
1868
|
+
if ($isElement) {
|
|
1869
|
+
$node.querySelector('.price').textContent = '$' + $item.price;
|
|
1870
|
+
}
|
|
1871
|
+
">
|
|
1872
|
+
<div class="product-card">
|
|
1873
|
+
<h3>\${item.name}</h3>
|
|
1874
|
+
<p class="price">$\${item.price}</p>
|
|
1875
|
+
</div>
|
|
1876
|
+
</template>
|
|
1877
|
+
`;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
### 3. `swc-choose` - Multi-Branch Conditional
|
|
1883
|
+
|
|
1884
|
+
Switch-like behavior for multiple conditions.
|
|
1885
|
+
|
|
1886
|
+
```typescript
|
|
1887
|
+
@templateIsSim
|
|
1888
|
+
@elementDefine('order-status')
|
|
1889
|
+
class OrderStatus extends HTMLElement {
|
|
1890
|
+
private order = { status: 'shipped' };
|
|
1891
|
+
|
|
1892
|
+
@onConnectedInnerHtml
|
|
1893
|
+
render() {
|
|
1894
|
+
return `
|
|
1895
|
+
<template is="swc-choose" on-get-value="order.status">
|
|
1896
|
+
<template is="swc-when" on-get-value="'pending'">
|
|
1897
|
+
<p>⏳ Your order is being prepared...</p>
|
|
1898
|
+
</template>
|
|
1899
|
+
<template is="swc-when" on-get-value="'shipped'">
|
|
1900
|
+
<p>📦 Your order has shipped!</p>
|
|
1901
|
+
</template>
|
|
1902
|
+
<template is="swc-default">
|
|
1903
|
+
<p>❌ Order status unknown</p>
|
|
1904
|
+
</template>
|
|
1905
|
+
</template>
|
|
1906
|
+
`;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
### 4. `swc-async` - Async Content Loading
|
|
1912
|
+
|
|
1913
|
+
Automatically handle Promise rendering states.
|
|
1914
|
+
|
|
1915
|
+
```typescript
|
|
1916
|
+
@Sim
|
|
1917
|
+
@elementDefine('async-data')
|
|
1918
|
+
class AsyncData extends HTMLElement {
|
|
1919
|
+
async fetchUserData(userId: string) {
|
|
1920
|
+
const response = await fetch(`/api/users/\${userId}`);
|
|
1921
|
+
return response.json();
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
@onConnectedInnerHtml
|
|
1925
|
+
render() {
|
|
1926
|
+
return `
|
|
1927
|
+
<template is="swc-async" on-get-value="fetchUserData('user123')">
|
|
1928
|
+
<template is="swc-pending">
|
|
1929
|
+
<p>Loading...</p>
|
|
1930
|
+
</template>
|
|
1931
|
+
<template is="swc-fulfilled">
|
|
1932
|
+
<h2>\${data.name}</h2>
|
|
1933
|
+
<p>Email: \${data.email}</p>
|
|
1934
|
+
</template>
|
|
1935
|
+
<template is="swc-rejected">
|
|
1936
|
+
<p>❌ Error: \${error.message}</p>
|
|
1937
|
+
</template>
|
|
1938
|
+
</template>
|
|
1939
|
+
`;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
```
|
|
1943
|
+
|
|
1944
|
+
---
|
|
1945
|
+
|
|
1946
|
+
## 🚀 Server-Side Rendering (SSR) Project Example
|
|
1947
|
+
|
|
1948
|
+
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.
|
|
1949
|
+
|
|
1950
|
+
### Project Structure
|
|
1951
|
+
|
|
1952
|
+
```
|
|
1953
|
+
ssr-project/
|
|
1954
|
+
├── back-end/ # Express/HTTP server with SSR
|
|
1955
|
+
│ └── index.ts # SSR entry point
|
|
1956
|
+
├── front-end/ # Client-side app
|
|
1957
|
+
│ └── index.ts # Client bootstrap
|
|
1958
|
+
├── src/
|
|
1959
|
+
│ ├── bootfactory.ts # Universal app configuration
|
|
1960
|
+
│ ├── components/ # Shared components
|
|
1961
|
+
│ └── pages/ # Route components (routers + pages)
|
|
1962
|
+
```
|
|
1963
|
+
|
|
1964
|
+
### 1. Backend SSR Setup
|
|
1965
|
+
|
|
1966
|
+
The backend renders components on the server and sends HTML to the client:
|
|
1967
|
+
|
|
1968
|
+
```typescript
|
|
1969
|
+
// back-end/index.ts
|
|
1970
|
+
import { SimpleBootHttpSSRServer } from '@dooboostore/simple-boot-http-server-ssr';
|
|
1971
|
+
import { SSRSimpleWebComponentFilter } from '@dooboostore/simple-boot-http-server-ssr';
|
|
1972
|
+
import swcRegister from '@dooboostore/simple-web-component';
|
|
1973
|
+
import bootfactory from '@swc-src/bootfactory';
|
|
1974
|
+
|
|
1975
|
+
const swcFilter = new SSRSimpleWebComponentFilter({
|
|
1976
|
+
frontDistPath: path.resolve(__dirname, '../dist-front-end'),
|
|
1977
|
+
frontDistIndexFileName: 'index.html',
|
|
1978
|
+
registerComponents: async (window: any) => {
|
|
1979
|
+
// 1. Register core SWC elements (swc-if, swc-loop, etc.)
|
|
1980
|
+
swcRegister(window);
|
|
1981
|
+
|
|
1982
|
+
// 2. Register and bootstrap your app with routing
|
|
1983
|
+
const urlPath = window.location.pathname;
|
|
1984
|
+
bootfactory(window, urlPath);
|
|
1985
|
+
|
|
1986
|
+
console.log('✅ Components registered for SSR');
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
const server = new SimpleBootHttpSSRServer({
|
|
1991
|
+
listen: { port: 8080 },
|
|
1992
|
+
filters: [swcFilter]
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
server.listen();
|
|
1996
|
+
console.log('🚀 SSR Server running on http://localhost:8080');
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
### 2. Universal Bootfactory
|
|
2000
|
+
|
|
2001
|
+
The bootfactory runs on **both server and client** with different connection modes:
|
|
2002
|
+
|
|
2003
|
+
```typescript
|
|
2004
|
+
// src/bootfactory.ts
|
|
2005
|
+
import register from '@dooboostore/simple-web-component';
|
|
2006
|
+
import { IndexRouter } from './pages/index.router';
|
|
2007
|
+
|
|
2008
|
+
export default (window: Window, initialPath?: string) => {
|
|
2009
|
+
// Register all components globally
|
|
2010
|
+
register(window, [...componentFactories, ...pageFactories]);
|
|
2011
|
+
|
|
2012
|
+
const app = window.document.querySelector('#app') as any;
|
|
2013
|
+
|
|
2014
|
+
const config = {
|
|
2015
|
+
rootRouter: IndexRouter.SYMBOL,
|
|
2016
|
+
routeType: 'path', // Use browser path: /users, /pages/about
|
|
2017
|
+
path: initialPath ?? '/',
|
|
2018
|
+
window: window
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
// Detect client vs server environment
|
|
2022
|
+
const isClientWindow = typeof window !== 'undefined' && window === globalThis.window;
|
|
2023
|
+
|
|
2024
|
+
if (isClientWindow) {
|
|
2025
|
+
// CLIENT: Use 'swap' mode for zero FOUC hydration
|
|
2026
|
+
app.connect({
|
|
2027
|
+
...config,
|
|
2028
|
+
connectMode: 'swap' // Clone → Connect → Replace original
|
|
2029
|
+
});
|
|
2030
|
+
} else {
|
|
2031
|
+
// SERVER: Use 'direct' mode (no swap needed)
|
|
2032
|
+
app.connect(config);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
return app;
|
|
2036
|
+
};
|
|
2037
|
+
```
|
|
2038
|
+
|
|
2039
|
+
### 3. Isomorphic Router & Pages
|
|
2040
|
+
|
|
2041
|
+
Define routers that work the same way on server and client:
|
|
2042
|
+
|
|
2043
|
+
```typescript
|
|
2044
|
+
// src/pages/index.router.ts
|
|
2045
|
+
import { Sim } from '@dooboostore/simple-boot';
|
|
2046
|
+
import { Router } from '@dooboostore/simple-boot';
|
|
2047
|
+
import { elementDefine, onConnectedInnerHtml, setAttribute } from '@dooboostore/simple-web-component';
|
|
2048
|
+
import { RouterAction } from '@dooboostore/simple-boot';
|
|
2049
|
+
|
|
2050
|
+
@Sim
|
|
2051
|
+
@Router({
|
|
2052
|
+
path: '',
|
|
2053
|
+
route: {
|
|
2054
|
+
'/': HomePage.SYMBOL,
|
|
2055
|
+
'/user': UserPage.SYMBOL
|
|
2056
|
+
}
|
|
2057
|
+
})
|
|
2058
|
+
@elementDefine('index-router')
|
|
2059
|
+
class IndexRouter extends HTMLElement implements RouterAction.CanActivate {
|
|
2060
|
+
constructor(private router: Router) {
|
|
2061
|
+
super();
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Lifecycle hook: Called BEFORE entering route
|
|
2065
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
2066
|
+
// Inject rendered page component
|
|
2067
|
+
if (data instanceof Node) {
|
|
2068
|
+
console.log(`✅ Rendering: \${data.nodeName}`);
|
|
2069
|
+
this.replaceChildren(data);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
2074
|
+
render() {
|
|
2075
|
+
return `
|
|
2076
|
+
<style>
|
|
2077
|
+
:host { display: block; }
|
|
2078
|
+
header { background: #222; color: white; padding: 20px; }
|
|
2079
|
+
nav button { margin: 5px; padding: 10px 15px; cursor: pointer; }
|
|
2080
|
+
section { padding: 20px; }
|
|
2081
|
+
</style>
|
|
2082
|
+
<header>
|
|
2083
|
+
<h1>SWC + SSR App</h1>
|
|
2084
|
+
<nav>
|
|
2085
|
+
<button swc-on-click="$host.router.go('/')">🏠 Home</button>
|
|
2086
|
+
<button swc-on-click="$host.router.go('/user')">👤 Users</button>
|
|
2087
|
+
</nav>
|
|
2088
|
+
</header>
|
|
2089
|
+
<section><slot></slot></section>
|
|
2090
|
+
`;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
### 4. Page Components
|
|
2096
|
+
|
|
2097
|
+
```typescript
|
|
2098
|
+
// src/pages/home.page.ts - Leaf route (no children)
|
|
2099
|
+
@Sim
|
|
2100
|
+
@elementDefine('home-page')
|
|
2101
|
+
class HomePage extends HTMLElement {
|
|
2102
|
+
@onConnectedInnerHtml
|
|
2103
|
+
render() {
|
|
2104
|
+
return `
|
|
2105
|
+
<h2>Welcome! 👋</h2>
|
|
2106
|
+
<p>This page was rendered on the server and hydrated on the client.</p>
|
|
2107
|
+
<template is="swc-loop" on-get-value="[1,2,3]">
|
|
2108
|
+
<div style="padding: 10px; background: #f5f5f5; margin: 5px;">
|
|
2109
|
+
Item \${item}
|
|
2110
|
+
</div>
|
|
2111
|
+
</template>
|
|
2112
|
+
`;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/pages/user.router.ts - Nested router with sub-routes
|
|
2117
|
+
@Sim
|
|
2118
|
+
@Router({
|
|
2119
|
+
path: '/user',
|
|
2120
|
+
route: {
|
|
2121
|
+
'': UserListPage.SYMBOL,
|
|
2122
|
+
'/:id': UserDetailPage.SYMBOL
|
|
2123
|
+
}
|
|
2124
|
+
})
|
|
2125
|
+
@elementDefine('user-router')
|
|
2126
|
+
class UserRouter extends HTMLElement implements RouterAction.CanActivate {
|
|
2127
|
+
async canActivate(url: RoutingDataSet, data?: any) {
|
|
2128
|
+
if (data instanceof Node) {
|
|
2129
|
+
this.replaceChildren(data);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
2134
|
+
render() {
|
|
2135
|
+
return `
|
|
2136
|
+
<div style="margin-left: 20px;">
|
|
2137
|
+
<h2>User Management</h2>
|
|
2138
|
+
<slot></slot>
|
|
2139
|
+
</div>
|
|
2140
|
+
`;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
```
|
|
2144
|
+
|
|
2145
|
+
### 5. Client Entry Point
|
|
2146
|
+
|
|
2147
|
+
The client reuses the same bootfactory:
|
|
2148
|
+
|
|
2149
|
+
```typescript
|
|
2150
|
+
// front-end/index.ts
|
|
2151
|
+
import bootfactory from '@swc-src/bootfactory';
|
|
2152
|
+
import { UrlUtils } from '@dooboostore/core/url';
|
|
2153
|
+
|
|
2154
|
+
console.log('🚀 Client-side app initializing...');
|
|
2155
|
+
|
|
2156
|
+
// Get current URL path from browser
|
|
2157
|
+
const currentPath = UrlUtils.getUrlPath(window.location);
|
|
2158
|
+
|
|
2159
|
+
// Bootstrap with swap mode (zero FOUC)
|
|
2160
|
+
bootfactory(window, currentPath);
|
|
2161
|
+
|
|
2162
|
+
console.log('✅ Client app hydrated');
|
|
2163
|
+
```
|
|
2164
|
+
|
|
2165
|
+
### 6. Flow Diagram
|
|
2166
|
+
|
|
2167
|
+
```
|
|
2168
|
+
┌─────────────────────────────────────────────────────────┐
|
|
2169
|
+
│ User Request │
|
|
2170
|
+
│ GET /user/123 │
|
|
2171
|
+
└──────────────────────┬──────────────────────────────────┘
|
|
2172
|
+
│
|
|
2173
|
+
┌──────────────▼──────────────┐
|
|
2174
|
+
│ Backend SSR Renderer │
|
|
2175
|
+
├─────────────────────────────┤
|
|
2176
|
+
│ 1. bootfactory(virtualWin) │
|
|
2177
|
+
│ 2. IndexRouter routes │
|
|
2178
|
+
│ 3. UserRouter renders │
|
|
2179
|
+
│ 4. UserDetailPage → HTML │
|
|
2180
|
+
└──────────────┬──────────────┘
|
|
2181
|
+
│
|
|
2182
|
+
┌──────────────▼──────────────┐
|
|
2183
|
+
│ Sent to Client as HTML │
|
|
2184
|
+
│ <swc-app> │
|
|
2185
|
+
│ <index-router> │
|
|
2186
|
+
│ <user-router> │
|
|
2187
|
+
│ <user-detail-page> │
|
|
2188
|
+
│ </user-detail-page> │
|
|
2189
|
+
│ </user-router> │
|
|
2190
|
+
│ </index-router> │
|
|
2191
|
+
│ </swc-app> │
|
|
2192
|
+
└──────────────┬──────────────┘
|
|
2193
|
+
│
|
|
2194
|
+
┌──────────────▼──────────────┐
|
|
2195
|
+
│ Client Hydration Swap │
|
|
2196
|
+
├─────────────────────────────┤
|
|
2197
|
+
│ 1. Clone SSR HTML │
|
|
2198
|
+
│ 2. Connect listeners │
|
|
2199
|
+
│ 3. Replace original DOM │
|
|
2200
|
+
│ 4. Fully interactive │
|
|
2201
|
+
└─────────────────────────────┘
|
|
2202
|
+
```
|
|
2203
|
+
|
|
2204
|
+
### 7. Key Benefits
|
|
2205
|
+
|
|
2206
|
+
| Aspect | Benefit |
|
|
2207
|
+
|--------|---------|
|
|
2208
|
+
| **SEO** | Server renders initial HTML (searchable) |
|
|
2209
|
+
| **Performance** | Faster First Contentful Paint (FCP) |
|
|
2210
|
+
| **Tag Preservation** | Custom element tags survive SSR → client |
|
|
2211
|
+
| **Zero FOUC** | Swap mode clones & replaces seamlessly |
|
|
2212
|
+
| **Code Sharing** | Same router code on server & client |
|
|
2213
|
+
| **Island Architecture** | Multiple independent `<swc-app>` instances |
|
|
2214
|
+
| **Progressive Enhancement** | Works without JavaScript (graceful fallback) |
|
|
2215
|
+
|
|
2216
|
+
### 8. Build & Run
|
|
2217
|
+
|
|
2218
|
+
```bash
|
|
2219
|
+
# Build backend (Node.js server)
|
|
2220
|
+
cd back-end && npm run build
|
|
2221
|
+
|
|
2222
|
+
# Build frontend (browser bundle)
|
|
2223
|
+
cd front-end && npm run build
|
|
2224
|
+
|
|
2225
|
+
# Run SSR server on http://localhost:8080
|
|
2226
|
+
npm start
|
|
2227
|
+
|
|
2228
|
+
# Visit http://localhost:8080/user/123
|
|
2229
|
+
# Server renders UserDetailPage → Client hydrates with swap mode
|
|
348
2230
|
```
|
|
349
2231
|
|
|
350
2232
|
---
|