@dooboostore/simple-web-component 1.0.21 → 1.0.23
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 +1113 -95
- package/dist/cjs/decorators/addEventListener.js +4 -0
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/applyNode.js +174 -0
- package/dist/cjs/decorators/applyNode.js.map +7 -0
- package/dist/cjs/decorators/attribute.js +30 -10
- package/dist/cjs/decorators/attribute.js.map +3 -3
- package/dist/cjs/decorators/{changedAttribute.js → changedAttributeHost.js} +6 -6
- package/dist/cjs/decorators/changedAttributeHost.js.map +7 -0
- package/dist/cjs/decorators/classList.js +45 -15
- package/dist/cjs/decorators/classList.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +107 -57
- package/dist/cjs/decorators/elementDefine.js.map +2 -2
- package/dist/cjs/decorators/emitCustomEvent.js +4 -3
- package/dist/cjs/decorators/emitCustomEvent.js.map +2 -2
- package/dist/cjs/decorators/index.js +2 -4
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/lifecycles.js +29 -0
- package/dist/cjs/decorators/lifecycles.js.map +2 -2
- package/dist/cjs/decorators/query.js +5 -1
- package/dist/cjs/decorators/query.js.map +2 -2
- package/dist/cjs/decorators/queryAll.js +5 -1
- package/dist/cjs/decorators/queryAll.js.map +2 -2
- package/dist/cjs/decorators/setAttribute.js +5 -1
- package/dist/cjs/decorators/setAttribute.js.map +2 -2
- package/dist/cjs/decorators/style.js +39 -13
- package/dist/cjs/decorators/style.js.map +2 -2
- package/dist/cjs/elements/SwcAppEngine.js.map +2 -2
- package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
- package/dist/cjs/elements/SwcAsync.js +2 -2
- package/dist/cjs/elements/SwcAsync.js.map +2 -2
- package/dist/cjs/elements/SwcChoose.js +2 -2
- package/dist/cjs/elements/SwcChoose.js.map +2 -2
- package/dist/cjs/elements/SwcIf.js +2 -2
- package/dist/cjs/elements/SwcIf.js.map +2 -2
- package/dist/cjs/elements/SwcLoop.js +2 -2
- package/dist/cjs/elements/SwcLoop.js.map +2 -2
- package/dist/cjs/elements/register.js +6 -5
- package/dist/cjs/elements/register.js.map +2 -2
- package/dist/cjs/index.js +6 -18
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/types.js.map +2 -2
- package/dist/cjs/utils/TemplateUtils.js +6 -0
- package/dist/cjs/utils/TemplateUtils.js.map +2 -2
- package/dist/esm/decorators/addEventListener.js +4 -0
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/applyNode.js +155 -0
- package/dist/esm/decorators/applyNode.js.map +7 -0
- package/dist/esm/decorators/attribute.js +30 -10
- package/dist/esm/decorators/attribute.js.map +3 -3
- package/dist/esm/decorators/{changedAttribute.js → changedAttributeHost.js} +3 -3
- package/dist/esm/decorators/changedAttributeHost.js.map +7 -0
- package/dist/esm/decorators/classList.js +45 -15
- package/dist/esm/decorators/classList.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +106 -56
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/decorators/emitCustomEvent.js +4 -3
- package/dist/esm/decorators/emitCustomEvent.js.map +2 -2
- package/dist/esm/decorators/index.js +2 -4
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/lifecycles.js +29 -0
- package/dist/esm/decorators/lifecycles.js.map +2 -2
- package/dist/esm/decorators/query.js +5 -1
- package/dist/esm/decorators/query.js.map +2 -2
- package/dist/esm/decorators/queryAll.js +5 -1
- package/dist/esm/decorators/queryAll.js.map +2 -2
- package/dist/esm/decorators/setAttribute.js +5 -1
- package/dist/esm/decorators/setAttribute.js.map +2 -2
- package/dist/esm/decorators/style.js +39 -13
- package/dist/esm/decorators/style.js.map +2 -2
- package/dist/esm/elements/SwcAppEngine.js.map +2 -2
- package/dist/esm/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm/elements/SwcAsync.js +2 -2
- package/dist/esm/elements/SwcAsync.js.map +2 -2
- package/dist/esm/elements/SwcChoose.js +2 -2
- package/dist/esm/elements/SwcChoose.js.map +2 -2
- package/dist/esm/elements/SwcIf.js +2 -2
- package/dist/esm/elements/SwcIf.js.map +2 -2
- package/dist/esm/elements/SwcLoop.js +2 -2
- package/dist/esm/elements/SwcLoop.js.map +2 -2
- package/dist/esm/elements/register.js +6 -5
- package/dist/esm/elements/register.js.map +2 -2
- package/dist/esm/index.js +6 -18
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/types.js.map +2 -2
- package/dist/esm/utils/TemplateUtils.js +6 -0
- package/dist/esm/utils/TemplateUtils.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +385 -206
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/decorators/addEventListener.d.ts +6 -0
- package/dist/types/decorators/addEventListener.d.ts.map +1 -1
- package/dist/types/decorators/applyNode.d.ts +121 -0
- package/dist/types/decorators/applyNode.d.ts.map +1 -0
- package/dist/types/decorators/attribute.d.ts +6 -0
- package/dist/types/decorators/attribute.d.ts.map +1 -1
- package/dist/types/decorators/{changedAttribute.d.ts → changedAttributeHost.d.ts} +6 -2
- package/dist/types/decorators/changedAttributeHost.d.ts.map +1 -0
- package/dist/types/decorators/classList.d.ts +32 -5
- package/dist/types/decorators/classList.d.ts.map +1 -1
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/decorators/emitCustomEvent.d.ts +14 -2
- package/dist/types/decorators/emitCustomEvent.d.ts.map +1 -1
- package/dist/types/decorators/index.d.ts +2 -4
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/lifecycles.d.ts +16 -0
- package/dist/types/decorators/lifecycles.d.ts.map +1 -1
- package/dist/types/decorators/query.d.ts +6 -3
- package/dist/types/decorators/query.d.ts.map +1 -1
- package/dist/types/decorators/queryAll.d.ts +6 -0
- package/dist/types/decorators/queryAll.d.ts.map +1 -1
- package/dist/types/decorators/setAttribute.d.ts +5 -0
- package/dist/types/decorators/setAttribute.d.ts.map +1 -1
- package/dist/types/decorators/style.d.ts +28 -3
- package/dist/types/decorators/style.d.ts.map +1 -1
- package/dist/types/elements/SwcAppEngine.d.ts.map +1 -1
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/types/elements/register.d.ts +1 -1
- package/dist/types/elements/register.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -15
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts +2 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/TemplateUtils.d.ts +5 -0
- package/dist/types/utils/TemplateUtils.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +385 -206
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/package.json +3 -3
- package/src/decorators/addEventListener.ts +9 -0
- package/src/decorators/applyNode.ts +290 -0
- package/src/decorators/attribute.ts +46 -12
- package/src/decorators/{changedAttribute.ts → changedAttributeHost.ts} +5 -1
- package/src/decorators/classList.ts +71 -16
- package/src/decorators/elementDefine.ts +130 -63
- package/src/decorators/emitCustomEvent.ts +13 -5
- package/src/decorators/index.ts +2 -4
- package/src/decorators/lifecycles.ts +43 -1
- package/src/decorators/query.ts +9 -2
- package/src/decorators/queryAll.ts +9 -0
- package/src/decorators/setAttribute.ts +8 -0
- package/src/decorators/style.ts +64 -13
- package/src/elements/SwcAppEngine.ts +1 -0
- package/src/elements/SwcAppMixin.ts +2 -1
- package/src/elements/SwcAsync.ts +2 -2
- package/src/elements/SwcChoose.ts +2 -2
- package/src/elements/SwcIf.ts +2 -2
- package/src/elements/SwcLoop.ts +2 -2
- package/src/elements/register.ts +7 -5
- package/src/index.ts +8 -17
- package/src/types.ts +2 -1
- package/src/utils/TemplateUtils.ts +9 -0
- package/dist/cjs/decorators/appendChild.js +0 -92
- package/dist/cjs/decorators/appendChild.js.map +0 -7
- package/dist/cjs/decorators/changedAttribute.js.map +0 -7
- package/dist/cjs/decorators/onConnectedInnerHtml.js +0 -48
- package/dist/cjs/decorators/onConnectedInnerHtml.js.map +0 -7
- package/dist/cjs/decorators/replaceChildren.js +0 -79
- package/dist/cjs/decorators/replaceChildren.js.map +0 -7
- package/dist/esm/decorators/appendChild.js +0 -73
- package/dist/esm/decorators/appendChild.js.map +0 -7
- package/dist/esm/decorators/changedAttribute.js.map +0 -7
- package/dist/esm/decorators/onConnectedInnerHtml.js +0 -29
- package/dist/esm/decorators/onConnectedInnerHtml.js.map +0 -7
- package/dist/esm/decorators/replaceChildren.js +0 -60
- package/dist/esm/decorators/replaceChildren.js.map +0 -7
- package/dist/types/decorators/appendChild.d.ts +0 -17
- package/dist/types/decorators/appendChild.d.ts.map +0 -1
- package/dist/types/decorators/changedAttribute.d.ts.map +0 -1
- package/dist/types/decorators/onConnectedInnerHtml.d.ts +0 -15
- package/dist/types/decorators/onConnectedInnerHtml.d.ts.map +0 -1
- package/dist/types/decorators/replaceChildren.d.ts +0 -9
- package/dist/types/decorators/replaceChildren.d.ts.map +0 -1
- package/src/decorators/appendChild.ts +0 -86
- package/src/decorators/onConnectedInnerHtml.ts +0 -42
- package/src/decorators/replaceChildren.ts +0 -64
package/README.md
CHANGED
|
@@ -30,67 +30,1037 @@ For **Safari** support of the `is` attribute, add this to your `<head>`:
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
## 💡
|
|
33
|
+
## 💡 Complete Decorator Reference
|
|
34
34
|
|
|
35
|
-
###
|
|
36
|
-
Registers the class as a custom element.
|
|
35
|
+
### Lifecycle Decorators
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| `name` | `string` | The custom element tag name (e.g., `'my-el'`). |
|
|
41
|
-
| `extends` | `string` | (Optional) The native tag to extend. |
|
|
42
|
-
| `observedAttributes` | `string[]` | (Optional) Extra attributes to observe. |
|
|
43
|
-
| `window` | `Window` | (Optional) Custom window object (useful for SSR). |
|
|
37
|
+
#### 1. `@onConstructor()`
|
|
38
|
+
Called during component construction (in `constructor`). Perfect for dependency injection setup.
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
```typescript
|
|
41
|
+
@elementDefine('my-component')
|
|
42
|
+
class MyComponent extends HTMLElement {
|
|
43
|
+
private dataService: DataService;
|
|
44
|
+
|
|
45
|
+
@onConstructor
|
|
46
|
+
setupDI(dataService: DataService) {
|
|
47
|
+
this.dataService = dataService;
|
|
48
|
+
console.log('Component constructed with DI');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Execution Order**: First - during `new MyComponent()`
|
|
54
|
+
|
|
55
|
+
#### 2. `@onBeforeConnected()`
|
|
56
|
+
**Dependency Injection & Pre-connection Setup**
|
|
57
|
+
|
|
58
|
+
Called before the component connects to the DOM. Perfect for initializing services via DI.
|
|
59
|
+
|
|
60
|
+
**⚠️ Important**: Lifecycle DI works **WITHOUT `@Sim`** - regular `@elementDefine` components can receive services here!
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
@elementDefine('my-component') // ← NO @Sim needed for lifecycle DI
|
|
64
|
+
class MyComponent extends HTMLElement {
|
|
65
|
+
#cartService: CartService;
|
|
66
|
+
#subscription?: Subscription;
|
|
67
|
+
|
|
68
|
+
// Lifecycle DI injection - works even without @Sim
|
|
69
|
+
@onBeforeConnected
|
|
70
|
+
async setupComponent(cartService: CartService) {
|
|
71
|
+
this.#cartService = cartService;
|
|
72
|
+
|
|
73
|
+
// Can be async - waits before rendering
|
|
74
|
+
await this.#cartService.load();
|
|
75
|
+
|
|
76
|
+
// Subscribe to reactive updates
|
|
77
|
+
this.#subscription = this.#cartService.store.subscribe(state => {
|
|
78
|
+
this.updateUI();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
disconnectedCallback() {
|
|
83
|
+
this.#subscription?.unsubscribe();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@onConnectedInnerHtml
|
|
87
|
+
render() {
|
|
88
|
+
return `<div>Component ready</div>`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Key Features:**
|
|
94
|
+
- ✅ Services automatically injected via lifecycle decorators
|
|
95
|
+
- ✅ **NO `@Sim` required** for lifecycle DI injection
|
|
96
|
+
- ✅ Can be async - waits before rendering
|
|
97
|
+
- ✅ Called once, before `connectedCallback`
|
|
98
|
+
- ✅ Perfect for initialization and subscriptions
|
|
99
|
+
|
|
100
|
+
**DI Injection Methods:**
|
|
101
|
+
|
|
102
|
+
| Method | Requires `@Sim`? | Location |
|
|
103
|
+
|--------|-----------------|----------|
|
|
104
|
+
| Lifecycle (`@onBeforeConnected`) | ❌ NO | Lifecycle decorators |
|
|
105
|
+
| Constructor Injection | ✅ YES | Inside `<swc-app>` only |
|
|
106
|
+
| Static Registration | ✅ YES | `<swc-app>` DI container |
|
|
107
|
+
|
|
108
|
+
**Execution Order**: Before `connectedCallback()`
|
|
109
|
+
|
|
110
|
+
#### 3. `@onAfterConnected()` / `@onConnected()`
|
|
111
|
+
Called after the component connects to the DOM. For post-render operations.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
@elementDefine('my-element')
|
|
115
|
+
class MyElement extends HTMLElement {
|
|
116
|
+
@onAfterConnected // or @onConnected (alias)
|
|
117
|
+
initializeAfterConnect() {
|
|
118
|
+
console.log('Element fully connected to DOM');
|
|
119
|
+
this.focus(); // Can interact with DOM now
|
|
120
|
+
|
|
121
|
+
// Measure element size after render
|
|
122
|
+
const rect = this.getBoundingClientRect();
|
|
123
|
+
console.log('Size:', rect.width, rect.height);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Execution Order**: After `connectedCallback()`
|
|
129
|
+
|
|
130
|
+
#### 4. `@onBeforeDisconnected()`
|
|
131
|
+
Called before the component disconnects from the DOM. For cleanup preparation.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
@elementDefine('event-listener-component')
|
|
135
|
+
class EventListenerComponent extends HTMLElement {
|
|
136
|
+
private resizeObserver: ResizeObserver;
|
|
137
|
+
|
|
138
|
+
@onBeforeDisconnected
|
|
139
|
+
prepareForDisconnect() {
|
|
140
|
+
console.log('About to disconnect, saving state...');
|
|
141
|
+
// Save state, cancel pending requests, etc.
|
|
142
|
+
const state = {
|
|
143
|
+
scrollPosition: window.scrollY,
|
|
144
|
+
formData: this.getFormData()
|
|
145
|
+
};
|
|
146
|
+
localStorage.setItem('component-state', JSON.stringify(state));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@onAfterDisconnected
|
|
150
|
+
cleanup() {
|
|
151
|
+
this.resizeObserver?.disconnect();
|
|
152
|
+
console.log('Cleanup completed');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Execution Order**: Before `disconnectedCallback()`
|
|
158
|
+
|
|
159
|
+
#### 5. `@onAfterDisconnected()` / `@onDisconnected()`
|
|
160
|
+
Called after the component disconnects from the DOM. Final cleanup.
|
|
47
161
|
|
|
48
162
|
```typescript
|
|
49
|
-
@elementDefine('
|
|
50
|
-
class
|
|
163
|
+
@elementDefine('resource-component')
|
|
164
|
+
class ResourceComponent extends HTMLElement {
|
|
165
|
+
private timerId: number;
|
|
166
|
+
private subscription: Subscription;
|
|
167
|
+
|
|
168
|
+
@onAfterDisconnected // or @onDisconnected (alias)
|
|
169
|
+
cleanup() {
|
|
170
|
+
// Cancel timers
|
|
171
|
+
clearTimeout(this.timerId);
|
|
172
|
+
|
|
173
|
+
// Unsubscribe from observables
|
|
174
|
+
this.subscription?.unsubscribe();
|
|
175
|
+
|
|
176
|
+
console.log('Resources cleaned up');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Execution Order**: After `disconnectedCallback()`
|
|
182
|
+
|
|
183
|
+
#### 6. `@onBeforeAdopted()`
|
|
184
|
+
Called before the element is adopted by a new document (useful for SSR/iframe scenarios).
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
@elementDefine('document-aware')
|
|
188
|
+
class DocumentAware extends HTMLElement {
|
|
189
|
+
@onBeforeAdopted
|
|
190
|
+
prepareForAdoption() {
|
|
191
|
+
console.log('Adopting to new document...');
|
|
192
|
+
// Prepare for document change
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### 7. `@onAfterAdopted()` / `@onAdopted()`
|
|
198
|
+
Called after the element is adopted by a new document.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
@elementDefine('iframe-component')
|
|
202
|
+
class IframeComponent extends HTMLElement {
|
|
203
|
+
@onAfterAdopted // or @onAdopted (alias)
|
|
204
|
+
onDocumentAdopted() {
|
|
205
|
+
console.log('Now in new document context');
|
|
206
|
+
// Re-attach to new document's resources
|
|
207
|
+
this.initializeInNewDocument();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private initializeInNewDocument() {
|
|
211
|
+
// Setup for new document
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### 8. `@onAddEventListener()`
|
|
217
|
+
Called when an event listener is added to the element. For event tracking/analytics.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
@elementDefine('event-tracked')
|
|
221
|
+
class EventTracked extends HTMLElement {
|
|
222
|
+
@onAddEventListener
|
|
223
|
+
onListenerAdded(type: string, listener: EventListener, options?: AddEventListenerOptions) {
|
|
224
|
+
console.log(`Event listener added: ${type}`);
|
|
225
|
+
// Track event listeners for debugging
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### 9. `@onConnectedInnerHtml(options?)`
|
|
231
|
+
Defines the component's template. Renders to Shadow DOM or Light DOM based on `useShadow` option.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
@elementDefine('styled-card')
|
|
235
|
+
class StyledCard extends HTMLElement {
|
|
51
236
|
@onConnectedInnerHtml({ useShadow: true })
|
|
52
237
|
render() {
|
|
53
|
-
return
|
|
238
|
+
return `
|
|
239
|
+
<style>
|
|
240
|
+
:host {
|
|
241
|
+
display: block;
|
|
242
|
+
padding: 20px;
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
245
|
+
}
|
|
246
|
+
</style>
|
|
247
|
+
<div class="card-content">
|
|
248
|
+
<slot></slot>
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Options:**
|
|
256
|
+
- `useShadow: true` - Render to Shadow DOM (style encapsulation)
|
|
257
|
+
- `useShadow: false` - Render to Light DOM (default)
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Attribute & Property Decorators
|
|
262
|
+
|
|
263
|
+
#### 10. `@attributeHost(name, options?)`
|
|
264
|
+
Bidirectional attribute binding with automatic type conversion.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
@elementDefine('input-field')
|
|
268
|
+
class InputField extends HTMLElement {
|
|
269
|
+
@attributeHost('value', { connectedInitialize: true })
|
|
270
|
+
value: string = '';
|
|
271
|
+
|
|
272
|
+
@attributeHost('disabled')
|
|
273
|
+
disabled: boolean = false;
|
|
274
|
+
|
|
275
|
+
@attributeHost('count')
|
|
276
|
+
count: number = 0;
|
|
277
|
+
|
|
278
|
+
@onConnectedInnerHtml
|
|
279
|
+
render() {
|
|
280
|
+
return `
|
|
281
|
+
<input type="text" value="${this.value}" ${this.disabled ? 'disabled' : ''}>
|
|
282
|
+
<p>Count: ${this.count}</p>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Options:**
|
|
289
|
+
- `connectedInitialize: true` - Push class value to DOM on connect
|
|
290
|
+
- `connectedInitialize: false` - Pull DOM value on connect (default)
|
|
291
|
+
- Auto-converts: `String`, `Number`, `Boolean`
|
|
292
|
+
|
|
293
|
+
#### 11. `@changedAttributeHost(name)`
|
|
294
|
+
Reactive handler triggered when attribute changes.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
@elementDefine('reactive-input')
|
|
298
|
+
class ReactiveInput extends HTMLElement {
|
|
299
|
+
private internalValue = '';
|
|
300
|
+
|
|
301
|
+
@changedAttributeHost('value')
|
|
302
|
+
onValueChange(newVal: string, oldVal: string) {
|
|
303
|
+
console.log(`Value changed: ${oldVal} → ${newVal}`);
|
|
304
|
+
this.internalValue = newVal;
|
|
305
|
+
this.updateDisplay();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
updateDisplay() {
|
|
309
|
+
const input = this.querySelector('input') as HTMLInputElement;
|
|
310
|
+
if (input) input.value = this.internalValue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### 12. `@setAttribute(selector, name, options?)`
|
|
316
|
+
Reflect method's return value to a target element's DOM attribute. One-way binding (class → DOM).
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
@elementDefine('status-badge')
|
|
320
|
+
class StatusBadge extends HTMLElement {
|
|
321
|
+
private status: 'active' | 'inactive' | 'pending' = 'pending';
|
|
322
|
+
private items: any[] = [];
|
|
323
|
+
|
|
324
|
+
// Reflect status to host element's data-status attribute
|
|
325
|
+
@setAttribute(':host', 'data-status')
|
|
326
|
+
getStatus() {
|
|
327
|
+
return this.status;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Reflect computed data to specific element
|
|
331
|
+
@setAttribute('#count-display', 'data-count')
|
|
332
|
+
getItemCount() {
|
|
333
|
+
return this.items.length.toString();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Reflect to Shadow DOM element's aria-label
|
|
337
|
+
@setAttribute('.badge-label', 'aria-label', { root: 'shadow' })
|
|
338
|
+
getAriaLabel() {
|
|
339
|
+
return `Status is ${this.status}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@onConnectedInnerHtml
|
|
343
|
+
render() {
|
|
344
|
+
return `
|
|
345
|
+
<div data-status="${this.status}">
|
|
346
|
+
<span class="badge-label">Status: ${this.status}</span>
|
|
347
|
+
<span id="count-display" data-count="${this.items.length}">Items: ${this.items.length}</span>
|
|
348
|
+
</div>
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Change status and update DOM attributes
|
|
353
|
+
setStatus(newStatus: typeof this.status) {
|
|
354
|
+
this.status = newStatus;
|
|
355
|
+
this.getStatus(); // Updates :host[data-status]
|
|
356
|
+
this.getItemCount(); // Updates #count-display[data-count]
|
|
357
|
+
this.getAriaLabel(); // Updates .badge-label[aria-label]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Signature:**
|
|
363
|
+
- `@setAttribute(selector, attributeName, options?)`
|
|
364
|
+
- Returns method's value → reflects to target's attribute
|
|
365
|
+
|
|
366
|
+
**Selectors:**
|
|
367
|
+
- `':host'` - The component itself
|
|
368
|
+
- `'#element-id'` - Element by ID
|
|
369
|
+
- `'.class-name'` - Element by class
|
|
370
|
+
- `selector` - Any CSS selector
|
|
371
|
+
|
|
372
|
+
#### 12b. `@setAttributeHost(name, options?)` - Shorthand for `:host`
|
|
373
|
+
Simplified version of `@setAttribute` that always targets `:host`.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
@elementDefine('counter')
|
|
377
|
+
class Counter extends HTMLElement {
|
|
378
|
+
private count = 0;
|
|
379
|
+
|
|
380
|
+
// Equivalent to @setAttribute(':host', 'aria-label')
|
|
381
|
+
@setAttributeHost('aria-label')
|
|
382
|
+
getAriaLabel() {
|
|
383
|
+
return `Count is ${this.count}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@setAttributeHost('data-count')
|
|
387
|
+
getCountValue() {
|
|
388
|
+
return this.count.toString();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@setAttributeHost('aria-disabled')
|
|
392
|
+
getDisabledState() {
|
|
393
|
+
return (this.count >= 10).toString();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@onConnectedInnerHtml
|
|
397
|
+
render() {
|
|
398
|
+
return `
|
|
399
|
+
<div>
|
|
400
|
+
<p>Count: ${this.count}</p>
|
|
401
|
+
<button swc-on-click="$host.increment()">+</button>
|
|
402
|
+
</div>
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
increment() {
|
|
407
|
+
this.count++;
|
|
408
|
+
this.getAriaLabel(); // Updates :host[aria-label]
|
|
409
|
+
this.getCountValue(); // Updates :host[data-count]
|
|
410
|
+
this.getDisabledState(); // Updates :host[aria-disabled]
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Usage:**
|
|
416
|
+
```typescript
|
|
417
|
+
// Full form
|
|
418
|
+
@setAttribute(':host', 'data-status')
|
|
419
|
+
// vs
|
|
420
|
+
// Shorthand for :host
|
|
421
|
+
@setAttributeHost('data-status')
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Key Differences:**
|
|
425
|
+
- **`@attributeHost`** - Two-way binding: DOM ↔ Class (Bidirectional)
|
|
426
|
+
- **`@setAttribute`** - One-way binding: Class → DOM (Any selector)
|
|
427
|
+
- **`@setAttributeHost`** - One-way binding: Class → DOM (`:host` only)
|
|
428
|
+
- **`@changedAttributeHost`** - Watch DOM attribute changes (Reactive handler)
|
|
429
|
+
|
|
430
|
+
**Options:**
|
|
431
|
+
- `root: 'light'` - Set attribute on light DOM element
|
|
432
|
+
- `root: 'shadow'` - Set attribute on shadow DOM element
|
|
433
|
+
- `root: 'auto'` - Auto-detect (default)
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
### Query & DOM Selection Decorators
|
|
438
|
+
|
|
439
|
+
#### 13. `@query(selector, options?)`
|
|
440
|
+
Inject a single element into a property.
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
@elementDefine('form-handler')
|
|
444
|
+
class FormHandler extends HTMLElement {
|
|
445
|
+
@query('#submit-btn') submitBtn?: HTMLButtonElement;
|
|
446
|
+
@query('.form-field', { root: 'shadow' }) formField?: HTMLElement;
|
|
447
|
+
@query(':host') self?: HTMLElement;
|
|
448
|
+
|
|
449
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
450
|
+
render() {
|
|
451
|
+
return `
|
|
452
|
+
<div class="form-field">
|
|
453
|
+
<input type="text" placeholder="Enter name">
|
|
454
|
+
<button id="submit-btn">Submit</button>
|
|
455
|
+
</div>
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Root Options:**
|
|
462
|
+
- `'auto'` (default) - Search shadow DOM first, then light DOM
|
|
463
|
+
- `'light'` - Search only light DOM
|
|
464
|
+
- `'shadow'` - Search only shadow DOM
|
|
465
|
+
- `'all'` - Search both, return `[lightElements, shadowElements]`
|
|
466
|
+
|
|
467
|
+
**Special Targets:**
|
|
468
|
+
```typescript
|
|
469
|
+
@query(':host') self?: HTMLElement; // Element itself
|
|
470
|
+
@query(':parentHost') parent?: HTMLElement; // Parent SWC component
|
|
471
|
+
@query(':appHost') app?: SwcAppInterface; // Nearest <swc-app>
|
|
472
|
+
@query(':window') win?: Window; // Global window
|
|
473
|
+
@query(':document') doc?: Document; // Global document
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### 14. `@queryAll(selector, options?)`
|
|
477
|
+
Inject multiple elements as an array.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
@elementDefine('list-renderer')
|
|
481
|
+
class ListRenderer extends HTMLElement {
|
|
482
|
+
@queryAll('.item') items?: HTMLElement[];
|
|
483
|
+
@queryAll('button', { root: 'shadow' }) buttons?: HTMLButtonElement[];
|
|
484
|
+
|
|
485
|
+
getItemCount() {
|
|
486
|
+
return this.items?.length ?? 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### 14b. Removing Elements via `null` or `undefined`
|
|
492
|
+
Both `@query` and `@queryAll` support element removal by setting the property to `null` or `undefined`. This triggers automatic removal of the matching element(s).
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
@elementDefine('todo-list')
|
|
496
|
+
class TodoList extends HTMLElement {
|
|
497
|
+
@query('.notification') notification?: HTMLElement;
|
|
498
|
+
@queryAll('.todo-item') todoItems?: HTMLElement[];
|
|
499
|
+
|
|
500
|
+
removeNotification() {
|
|
501
|
+
// Setting to null triggers automatic removal
|
|
502
|
+
this.notification = null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
removeTodoItem(id: string) {
|
|
506
|
+
// Find and remove a specific todo item
|
|
507
|
+
const item = this.querySelector(`[data-id="${id}"]`);
|
|
508
|
+
if (item) {
|
|
509
|
+
// Setting @query to null removes the element from DOM
|
|
510
|
+
this.notification = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
clearAllTodos() {
|
|
515
|
+
// Setting @queryAll to null/undefined removes all matching elements
|
|
516
|
+
this.todoItems = null; // Removes all .todo-item elements
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@onConnectedInnerHtml
|
|
520
|
+
render() {
|
|
521
|
+
return `
|
|
522
|
+
<div class="notification">New todos available!</div>
|
|
523
|
+
<ul>
|
|
524
|
+
<li class="todo-item" data-id="1">Task 1</li>
|
|
525
|
+
<li class="todo-item" data-id="2">Task 2</li>
|
|
526
|
+
<li class="todo-item" data-id="3">Task 3</li>
|
|
527
|
+
</ul>
|
|
528
|
+
<button swc-on-click="$host.removeNotification()">Clear Notification</button>
|
|
529
|
+
<button swc-on-click="$host.clearAllTodos()">Clear All Tasks</button>
|
|
530
|
+
`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Removal Rules:**
|
|
536
|
+
- `@query`: Setting to `null` or `undefined` removes the single element
|
|
537
|
+
- `@queryAll`: Setting to `null` or `undefined` removes all matching elements
|
|
538
|
+
- Special selectors (`:host`, `:window`, `:document`, etc.) are **NOT removed** (protected)
|
|
539
|
+
- Only affects Light DOM by default; use `{ root: 'shadow' }` to remove from Shadow DOM
|
|
540
|
+
|
|
541
|
+
**Use Cases:**
|
|
542
|
+
- Close notification banners
|
|
543
|
+
- Clear dynamic list items
|
|
544
|
+
- Remove temporary overlays
|
|
545
|
+
- Clean up modal dialogs
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### Event & Messaging Decorators
|
|
550
|
+
|
|
551
|
+
#### 15. `@addEventListener(selector, event, options?)`
|
|
552
|
+
Declarative event binding with automatic delegation.
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
@elementDefine('todo-app')
|
|
556
|
+
class TodoApp extends HTMLElement {
|
|
557
|
+
private todos: any[] = [];
|
|
558
|
+
|
|
559
|
+
// Direct binding to specific element
|
|
560
|
+
@addEventListener('#add-btn', 'click')
|
|
561
|
+
onAddTodo(event: MouseEvent) {
|
|
562
|
+
console.log('Add button clicked');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Event delegation for dynamic elements
|
|
566
|
+
@addEventListener('.todo-item', 'click', { delegate: true })
|
|
567
|
+
onTodoClick(event: MouseEvent, hostSet: any, target: HTMLElement) {
|
|
568
|
+
const itemId = target.getAttribute('data-id');
|
|
569
|
+
console.log('Clicked todo:', itemId);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Global window events
|
|
573
|
+
@addEventListener(':window', 'resize', { removeOnDisconnected: true })
|
|
574
|
+
onWindowResize(event: Event) {
|
|
575
|
+
console.log('Window resized');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Parent app events
|
|
579
|
+
@addEventListener(':appHost', 'custom-event')
|
|
580
|
+
onCustomEvent(event: CustomEvent) {
|
|
581
|
+
console.log('Event received:', event.detail);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@onConnectedInnerHtml
|
|
585
|
+
render() {
|
|
586
|
+
return `
|
|
587
|
+
<div>
|
|
588
|
+
<input type="text" id="todo-input" placeholder="Add todo...">
|
|
589
|
+
<button id="add-btn">Add</button>
|
|
590
|
+
<ul class="todo-list">
|
|
591
|
+
${this.todos.map(t => `
|
|
592
|
+
<li class="todo-item" data-id="${t.id}">${t.text}</li>
|
|
593
|
+
`).join('')}
|
|
594
|
+
</ul>
|
|
595
|
+
</div>
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Features:**
|
|
602
|
+
- ✅ Direct binding to static selectors
|
|
603
|
+
- ✅ Event delegation for dynamic elements
|
|
604
|
+
- ✅ Global events (`:window`, `:document`)
|
|
605
|
+
- ✅ Parent communication (`:parentHost`, `:appHost`)
|
|
606
|
+
- ✅ Auto-cleanup on `disconnectedCallback`
|
|
607
|
+
|
|
608
|
+
#### 16. `@emitCustomEvent(target, type, options?)`
|
|
609
|
+
Emit custom events to component hierarchy or global targets.
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
@elementDefine('auth-panel')
|
|
613
|
+
class AuthPanel extends HTMLElement {
|
|
614
|
+
constructor(private authService: AuthService) { super(); }
|
|
615
|
+
|
|
616
|
+
// Emit to parent app when user logs in
|
|
617
|
+
@emitCustomEvent(':appHost', 'user-logged-in')
|
|
618
|
+
async onLogin() {
|
|
619
|
+
const user = await this.authService.login();
|
|
620
|
+
return { user, timestamp: Date.now() };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Emit globally when logout happens
|
|
624
|
+
@emitCustomEvent(':window', 'user-logged-out')
|
|
625
|
+
async onLogout() {
|
|
626
|
+
await this.authService.logout();
|
|
627
|
+
return { timestamp: Date.now() };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Emit to direct parent
|
|
631
|
+
@emitCustomEvent(':parentHost', 'child-ready')
|
|
632
|
+
notifyParent() {
|
|
633
|
+
return { childElement: this };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
@onConnectedInnerHtml
|
|
637
|
+
render() {
|
|
638
|
+
return `
|
|
639
|
+
<div>
|
|
640
|
+
<button swc-on-click="$host.onLogin()">Login</button>
|
|
641
|
+
<button swc-on-click="$host.onLogout()">Logout</button>
|
|
642
|
+
</div>
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Listen to the emitted event
|
|
648
|
+
@elementDefine('user-widget')
|
|
649
|
+
class UserWidget extends HTMLElement {
|
|
650
|
+
private user: any = null;
|
|
651
|
+
|
|
652
|
+
@addEventListener(':appHost', 'user-logged-in')
|
|
653
|
+
onUserLoggedIn(event: CustomEvent) {
|
|
654
|
+
this.user = event.detail.user;
|
|
655
|
+
this.render();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@onConnectedInnerHtml
|
|
659
|
+
render() {
|
|
660
|
+
return `<p>User: ${this.user?.name ?? 'Anonymous'}</p>`;
|
|
54
661
|
}
|
|
55
662
|
}
|
|
56
663
|
```
|
|
57
664
|
|
|
58
|
-
|
|
59
|
-
|
|
665
|
+
**Emission Targets:**
|
|
666
|
+
- `':appHost'` - Nearest parent `<swc-app>` (contained within app)
|
|
667
|
+
- `':parentHost'` - Direct parent SWC component
|
|
668
|
+
- `':firstHost'` / `':lastHost'` - First/last component in hierarchy
|
|
669
|
+
- `':firstAppHost'` / `':lastAppHost'` - First/last `<swc-app>` in hierarchy
|
|
670
|
+
- `':hosts'` - All SWC ancestors
|
|
671
|
+
- `':appHosts'` - All `<swc-app>` ancestors
|
|
672
|
+
- `':window'` - Global window (all apps can listen)
|
|
673
|
+
- `':document'` - Global document
|
|
674
|
+
- `selector` - CSS selector (emit to matching elements)
|
|
675
|
+
|
|
676
|
+
**Options:**
|
|
677
|
+
- `bubbles: true` (default) - Event bubbles up
|
|
678
|
+
- `composed: true` (default) - Event crosses Shadow DOM boundary
|
|
679
|
+
- `cancelable: true` - Event can be canceled
|
|
680
|
+
- `root: 'auto' | 'light' | 'shadow'` - Where to find target elements
|
|
681
|
+
|
|
682
|
+
---
|
|
60
683
|
|
|
61
|
-
|
|
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.
|
|
684
|
+
### DOM Manipulation Decorators
|
|
66
685
|
|
|
67
|
-
|
|
68
|
-
|
|
686
|
+
#### 17. `@applyNode(selector, options?)`
|
|
687
|
+
Surgically add, update, or replace DOM nodes at specific positions.
|
|
69
688
|
|
|
70
689
|
```typescript
|
|
71
|
-
@elementDefine('
|
|
72
|
-
class
|
|
690
|
+
@elementDefine('counter-display')
|
|
691
|
+
class CounterDisplay extends HTMLElement {
|
|
73
692
|
private count = 0;
|
|
74
|
-
|
|
75
|
-
|
|
693
|
+
private errorMessage = '';
|
|
694
|
+
|
|
695
|
+
// Replace all children with new content
|
|
696
|
+
@applyNode('#display', { position: 'replaceChildren' })
|
|
697
|
+
updateDisplayCount() {
|
|
698
|
+
return `<span>${this.count}</span>`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Append to container
|
|
702
|
+
@applyNode('#history', { position: 'beforeEnd' })
|
|
703
|
+
addHistoryEntry() {
|
|
704
|
+
return `<p>Count was: ${this.count}</p>`;
|
|
705
|
+
}
|
|
76
706
|
|
|
77
|
-
|
|
78
|
-
|
|
707
|
+
// Set HTML content
|
|
708
|
+
@applyNode('#status', { position: 'innerHtml' })
|
|
709
|
+
setStatus() {
|
|
710
|
+
return `<strong>Status: Active</strong>`;
|
|
711
|
+
}
|
|
79
712
|
|
|
80
|
-
|
|
81
|
-
|
|
713
|
+
// Set text content
|
|
714
|
+
@applyNode('#counter', { position: 'innerText' })
|
|
715
|
+
updateCounter() {
|
|
716
|
+
return this.count.toString();
|
|
717
|
+
}
|
|
82
718
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
719
|
+
// Remove element if condition is truthy
|
|
720
|
+
@applyNode('#error-banner', { position: 'remove' })
|
|
721
|
+
dismissError() {
|
|
722
|
+
return this.errorMessage === ''; // Remove if no error
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
@onConnectedInnerHtml
|
|
726
|
+
render() {
|
|
727
|
+
return `
|
|
728
|
+
<div id="display">0</div>
|
|
729
|
+
<div id="counter">0</div>
|
|
730
|
+
<div id="status"></div>
|
|
731
|
+
<button swc-on-click="$host.count++; $host.updateDisplayCount?.()">+</button>
|
|
732
|
+
<div id="history"></div>
|
|
733
|
+
<div id="error-banner" style="display: ${this.errorMessage ? 'block' : 'none'};"></div>
|
|
734
|
+
`;
|
|
88
735
|
}
|
|
89
736
|
}
|
|
90
737
|
```
|
|
91
738
|
|
|
92
|
-
|
|
93
|
-
|
|
739
|
+
**Position Options:**
|
|
740
|
+
- `'beforeEnd'` (default) - Append to end
|
|
741
|
+
- `'afterBegin'` - Prepend to start
|
|
742
|
+
- `'beforeBegin'` - Insert before element
|
|
743
|
+
- `'afterEnd'` - Insert after element
|
|
744
|
+
- `'replace'` - Replace entire element
|
|
745
|
+
- `'replaceChildren'` - Replace all children
|
|
746
|
+
- `'innerHtml'` - Set `innerHTML`
|
|
747
|
+
- `'innerText'` - Set `innerText`
|
|
748
|
+
- `'remove'` - Remove element if returned value is truthy
|
|
749
|
+
|
|
750
|
+
**Root Option:**
|
|
751
|
+
Control which DOM tree is searched with the `root` option:
|
|
752
|
+
- `'auto'` (default) - Search shadow DOM first, then light DOM
|
|
753
|
+
- `'shadow'` - Search only shadow DOM
|
|
754
|
+
- `'light'` - Search only light DOM
|
|
755
|
+
- `'all'` - Search all ancestors
|
|
756
|
+
|
|
757
|
+
#### Convenience Decorators
|
|
758
|
+
|
|
759
|
+
Shorthand decorators for common position-specific operations:
|
|
760
|
+
|
|
761
|
+
**Position-Specific Variants:**
|
|
762
|
+
```typescript
|
|
763
|
+
@applyBeforeBeginNode('#target') prependTarget() { return `<div>Before</div>`; }
|
|
764
|
+
@applyAfterBeginNode('#target') afterBeginTarget() { return `<div>After Begin</div>`; }
|
|
765
|
+
@applyBeforeEndNode('#target') beforeEndTarget() { return `<div>Before End</div>`; }
|
|
766
|
+
@applyAfterEndNode('#target') afterEndTarget() { return `<div>After</div>`; }
|
|
767
|
+
@applyReplaceNode('#target') replaceTarget() { return `<div>Replaced</div>`; }
|
|
768
|
+
@applyReplaceChildrenNode('#target') replaceChildrenTarget() { return `<div>New Children</div>`; }
|
|
769
|
+
@applyInnerHtmlNode('#target') setInnerHtml() { return `<b>Bold</b>`; }
|
|
770
|
+
@applyInnerTextNode('#target') setInnerText() { return 'Plain text'; }
|
|
771
|
+
@applyRemoveNode('#target') removeTarget() { return true; }
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**Host-Targeted Variants (`:host` selector):**
|
|
775
|
+
```typescript
|
|
776
|
+
@applyBeforeBeginNodeHost() prependHost() { return `<div>Before Host</div>`; }
|
|
777
|
+
@applyAfterBeginNodeHost() afterBeginHost() { return `<div>After Begin</div>`; }
|
|
778
|
+
@applyBeforeEndNodeHost() beforeEndHost() { return `<div>Before End</div>`; }
|
|
779
|
+
@applyAfterEndNodeHost({ root: 'shadow' }) afterEndHost() { return `<div>After Host</div>`; }
|
|
780
|
+
@applyReplaceNodeHost() replaceHost() { return `<div>Replaced Host</div>`; }
|
|
781
|
+
@applyReplaceChildrenNodeHost() replaceChildrenHost() { return `<div>Shadow Children</div>`; }
|
|
782
|
+
@applyInnerHtmlNodeHost() setHostInnerHtml() { return `<b>Host Content</b>`; }
|
|
783
|
+
@applyInnerTextNodeHost() setHostInnerText() { return 'Host text'; }
|
|
784
|
+
@applyRemoveNodeHost() removeHost() { return this.shouldRemove; }
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
#### 18. `@setStyle(selector, options?)` / `@updateStyle(selector, options?)` / `@removeStyle(selector, options?)`
|
|
790
|
+
|
|
791
|
+
Manipulate CSS styles on elements with full Shadow DOM support.
|
|
792
|
+
|
|
793
|
+
**Core Features:**
|
|
794
|
+
- `@setStyle` - Set inline style (replaces all)
|
|
795
|
+
- `@updateStyle` - Merge with existing styles
|
|
796
|
+
- `@removeStyle` - Remove specific properties
|
|
797
|
+
- `root` option for Shadow DOM / Light DOM targeting
|
|
798
|
+
- Support for multiple elements with `querySelectorAll`
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
@elementDefine('styled-component')
|
|
802
|
+
class StyledComponent extends HTMLElement {
|
|
803
|
+
private isActive = false;
|
|
804
|
+
|
|
805
|
+
@setStyle('.button', { root: 'shadow' })
|
|
806
|
+
setPrimaryButtonStyle() {
|
|
807
|
+
return {
|
|
808
|
+
backgroundColor: '#0066cc',
|
|
809
|
+
color: 'white',
|
|
810
|
+
padding: '10px 20px',
|
|
811
|
+
border: 'none',
|
|
812
|
+
borderRadius: '4px'
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
@updateStyle('.button', { root: 'shadow' })
|
|
817
|
+
highlightButton() {
|
|
818
|
+
return {
|
|
819
|
+
boxShadow: '0 0 10px rgba(0, 102, 204, 0.5)',
|
|
820
|
+
transform: 'scale(1.05)'
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
@removeStyle('.button', { root: 'shadow' })
|
|
825
|
+
clearHighlight() {
|
|
826
|
+
return ['boxShadow', 'transform'];
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
830
|
+
render() {
|
|
831
|
+
return `
|
|
832
|
+
<style>
|
|
833
|
+
.button { cursor: pointer; transition: all 0.2s; }
|
|
834
|
+
</style>
|
|
835
|
+
<button class="button">Styled</button>
|
|
836
|
+
`;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**Host-Targeted Variants:**
|
|
842
|
+
```typescript
|
|
843
|
+
@setStyleHost({ root: 'shadow' })
|
|
844
|
+
setHostStyle() {
|
|
845
|
+
return { display: 'flex', gap: '10px' };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
@updateStyleHost({ root: 'shadow' })
|
|
849
|
+
updateHostStyle() {
|
|
850
|
+
return { backgroundColor: '#f0f0f0' };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
@removeStyleHost({ root: 'shadow' })
|
|
854
|
+
clearHostStyle() {
|
|
855
|
+
return ['backgroundColor'];
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
#### 19. `@setClassList(selector, options?)` / `@addClassList(selector, options?)` / `@removeClassList(selector, options?)` / `@toggleClassList(selector, options?)` / `@updateClassList(selector, options?)`
|
|
862
|
+
|
|
863
|
+
Manipulate CSS classes on elements declaratively with Shadow DOM support.
|
|
864
|
+
|
|
865
|
+
**Core Features:**
|
|
866
|
+
- `@addClassList` - Add classes
|
|
867
|
+
- `@removeClassList` - Remove classes
|
|
868
|
+
- `@toggleClassList` - Toggle classes (returns boolean or bool array)
|
|
869
|
+
- `@setClassList` - Replace all classes
|
|
870
|
+
- `@updateClassList` - Merge with existing classes
|
|
871
|
+
- `root` option for DOM layer targeting
|
|
872
|
+
- Support for multiple elements
|
|
873
|
+
|
|
874
|
+
```typescript
|
|
875
|
+
@elementDefine('tab-component')
|
|
876
|
+
class TabComponent extends HTMLElement {
|
|
877
|
+
activeTab = 'home';
|
|
878
|
+
|
|
879
|
+
@addClassList('.tab', { root: 'shadow' })
|
|
880
|
+
addTabClasses() {
|
|
881
|
+
return ['interactive', 'focusable'];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
@toggleClassList('.tab', { root: 'shadow' })
|
|
885
|
+
toggleActiveTab() {
|
|
886
|
+
return this.activeTab === 'home'; // Returns boolean for all matched elements
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@removeClassList('.tab-inactive', { root: 'shadow' })
|
|
890
|
+
removeInactiveClass() {
|
|
891
|
+
return ['disabled'];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
@setClassList('.tab-pane', { root: 'shadow' })
|
|
895
|
+
updateTabPane() {
|
|
896
|
+
return [`pane-${this.activeTab}`, 'content'];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
900
|
+
render() {
|
|
901
|
+
return `
|
|
902
|
+
<style>
|
|
903
|
+
.tab { cursor: pointer; padding: 10px; }
|
|
904
|
+
.tab.active { border-bottom: 2px solid blue; }
|
|
905
|
+
</style>
|
|
906
|
+
<div class="tabs">
|
|
907
|
+
<div class="tab" swc-on-click="$host.activeTab='home'; $host.toggleActiveTab?.();">Home</div>
|
|
908
|
+
<div class="tab" swc-on-click="$host.activeTab='about'; $host.toggleActiveTab?.();">About</div>
|
|
909
|
+
</div>
|
|
910
|
+
<div class="tab-pane">Content here</div>
|
|
911
|
+
`;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**Host-Targeted Variants:**
|
|
917
|
+
```typescript
|
|
918
|
+
@addClassListHost({ root: 'shadow' })
|
|
919
|
+
addHostClasses() {
|
|
920
|
+
return ['component', 'interactive'];
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
@removeClassListHost({ root: 'shadow' })
|
|
924
|
+
removeHostClasses() {
|
|
925
|
+
return ['loading'];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
@toggleClassListHost({ root: 'shadow' })
|
|
929
|
+
toggleHostClass() {
|
|
930
|
+
return this.isOpen; // Apply 'open' class if true
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
@setClassListHost({ root: 'shadow' })
|
|
934
|
+
setHostClasses() {
|
|
935
|
+
return this.isOpen ? ['open', 'expanded'] : ['closed'];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@updateClassListHost({ root: 'shadow' })
|
|
939
|
+
updateHostClasses() {
|
|
940
|
+
return ['visible'];
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
### Dependency Injection & Singleton
|
|
947
|
+
|
|
948
|
+
#### 20. `@Sim`
|
|
949
|
+
Marks a class as a **Singleton** in the `<swc-app>` DI container for **constructor injection**.
|
|
950
|
+
|
|
951
|
+
**Two DI Mechanisms:**
|
|
952
|
+
|
|
953
|
+
1. **Lifecycle DI** (`@onBeforeConnected`, etc.) - ❌ NO `@Sim` needed
|
|
954
|
+
- Works on any `@elementDefine` component
|
|
955
|
+
- Services injected via method parameters
|
|
956
|
+
- Can be used outside `<swc-app>`
|
|
957
|
+
|
|
958
|
+
2. **Constructor DI** - ✅ `@Sim` REQUIRED
|
|
959
|
+
- Only works inside `<swc-app>` boundary
|
|
960
|
+
- Both provider AND consumer need `@Sim`
|
|
961
|
+
- Singleton pattern within the app
|
|
962
|
+
|
|
963
|
+
**Example: Both Approaches**
|
|
964
|
+
|
|
965
|
+
```typescript
|
|
966
|
+
// ============ SERVICE ============
|
|
967
|
+
@Sim
|
|
968
|
+
class CartService {
|
|
969
|
+
private items: any[] = [];
|
|
970
|
+
|
|
971
|
+
addItem(item: any) {
|
|
972
|
+
this.items.push(item);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
getItems() {
|
|
976
|
+
return this.items;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ============ APPROACH 1: Lifecycle DI (NO @Sim needed) ============
|
|
981
|
+
@elementDefine('cart-header') // ← NO @Sim
|
|
982
|
+
class CartHeader extends HTMLElement {
|
|
983
|
+
#cartService: CartService;
|
|
984
|
+
|
|
985
|
+
@onBeforeConnected
|
|
986
|
+
async setup(cartService: CartService) { // ← Injected here
|
|
987
|
+
this.#cartService = cartService;
|
|
988
|
+
await this.#cartService.load?.();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
@onConnectedInnerHtml
|
|
992
|
+
render() {
|
|
993
|
+
return `<div>Cart: ${this.#cartService?.getItems().length}</div>`;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ============ APPROACH 2: Constructor DI (NEEDS @Sim) ============
|
|
998
|
+
@Sim // ← Required for constructor injection
|
|
999
|
+
@elementDefine('cart-component')
|
|
1000
|
+
class CartComponent extends HTMLElement {
|
|
1001
|
+
// Constructor injection - CartService is auto-injected
|
|
1002
|
+
constructor(private cartService: CartService) { super(); }
|
|
1003
|
+
|
|
1004
|
+
@onConnectedInnerHtml
|
|
1005
|
+
render() {
|
|
1006
|
+
const itemCount = this.cartService.getItems().length;
|
|
1007
|
+
return `<div>Items: ${itemCount}</div>`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ============ ROUTING & BOOTSTRAP ============
|
|
1012
|
+
@Sim
|
|
1013
|
+
@Router({
|
|
1014
|
+
path: '/',
|
|
1015
|
+
route: {
|
|
1016
|
+
'/home': HomePage,
|
|
1017
|
+
'/cart': CartComponent // Constructor DI works here
|
|
1018
|
+
}
|
|
1019
|
+
})
|
|
1020
|
+
@elementDefine('app-router')
|
|
1021
|
+
class AppRouter extends HTMLElement {
|
|
1022
|
+
@onConnectedInnerHtml
|
|
1023
|
+
render() {
|
|
1024
|
+
return `<div><slot></slot></div>`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ============ HTML ============
|
|
1029
|
+
<!-- Lifecycle DI works OUTSIDE <swc-app> -->
|
|
1030
|
+
<cart-header></cart-header>
|
|
1031
|
+
|
|
1032
|
+
<!-- Constructor DI works INSIDE <swc-app> -->
|
|
1033
|
+
<swc-app swc-get-application-config="appConfig()">
|
|
1034
|
+
<app-router></app-router>
|
|
1035
|
+
</swc-app>
|
|
1036
|
+
|
|
1037
|
+
<script>
|
|
1038
|
+
window.appConfig = () => ({
|
|
1039
|
+
rootRouter: AppRouter,
|
|
1040
|
+
routeType: 'element',
|
|
1041
|
+
path: '/'
|
|
1042
|
+
});
|
|
1043
|
+
</script>
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
**When to Use Each:**
|
|
1047
|
+
|
|
1048
|
+
| Scenario | Use | Mark |
|
|
1049
|
+
|----------|-----|------|
|
|
1050
|
+
| Component needs services anywhere | Lifecycle DI in `@onBeforeConnected` | NO `@Sim` |
|
|
1051
|
+
| Component inside `<swc-app>` route | Constructor DI | YES `@Sim` |
|
|
1052
|
+
| Service/Singleton registration | Register in DI container | YES `@Sim` |
|
|
1053
|
+
| Standalone component | Lifecycle DI | NO `@Sim` |
|
|
1054
|
+
|
|
1055
|
+
**Key Points:**
|
|
1056
|
+
- `@Sim` = Mark as singleton for **constructor injection inside `<swc-app>`**
|
|
1057
|
+
- Lifecycle DI = Inject via method parameters (works everywhere, no `@Sim`)
|
|
1058
|
+
- Each `<swc-app>` has its own isolated DI container
|
|
1059
|
+
- Services are local to their `<swc-app>` instance (for constructor DI)
|
|
1060
|
+
|
|
1061
|
+
---
|
|
1062
|
+
|
|
1063
|
+
## Core Decorators
|
|
94
1064
|
|
|
95
1065
|
#### Root Options
|
|
96
1066
|
The `root` option controls which DOM tree is searched:
|
|
@@ -117,7 +1087,7 @@ Standard CSS selectors support precise root control.
|
|
|
117
1087
|
@query('.btn', { root: 'shadow' }) shadowBtn!: HTMLButtonElement;
|
|
118
1088
|
```
|
|
119
1089
|
|
|
120
|
-
### 5.
|
|
1090
|
+
### 5. Event Binding (`@addEventListener`)
|
|
121
1091
|
|
|
122
1092
|
Bind events declaratively with built-in **Event Delegation**, **Auto-Cleanup**, and **Context Injection**. Events flow through the component hierarchy with full access to parent scopes.
|
|
123
1093
|
|
|
@@ -128,6 +1098,43 @@ Bind events declaratively with built-in **Event Delegation**, **Auto-Cleanup**,
|
|
|
128
1098
|
- **Context Injection**: Receive `$host`, `$parentHost`, `$appHost` automatically
|
|
129
1099
|
- **Special Targets**: `:window`, `:document`, `:parentHost`, `:appHost`
|
|
130
1100
|
|
|
1101
|
+
#### 6. Surgical DOM Manipulation (`@applyNode`)
|
|
1102
|
+
|
|
1103
|
+
Surgically add, prepend, insert, or replace nodes with a single decorator. Perfect for dynami content updates without imperatively manipulating the DOM.
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
@elementDefine('list-manager')
|
|
1107
|
+
class ListManager extends HTMLElement {
|
|
1108
|
+
@applyNode('#list') addItem() {
|
|
1109
|
+
return '<li>New Item</li>';
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
@applyNode('#header', { position: 'afterBegin' }) prependTitle() {
|
|
1113
|
+
return '<h1>Title</h1>';
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
@applyNode('#item', { position: 'replace' }) replaceItem() {
|
|
1117
|
+
return '<div>Replaced Content</div>';
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
**Positions:**
|
|
1123
|
+
- `'beforeEnd'` (default): Append to end of target
|
|
1124
|
+
- `'afterBegin'`: Prepend to start of target
|
|
1125
|
+
- `'beforeBegin'`: Insert before target
|
|
1126
|
+
- `'afterEnd'`: Insert after target
|
|
1127
|
+
- `'replace'`: Replace entire target element
|
|
1128
|
+
- `'replaceChildren'`: Replace all children of target element
|
|
1129
|
+
- `'innerHtml'`: Set target's `innerHTML` with string content
|
|
1130
|
+
- `'innerText'`: Set target's `innerText` with string content
|
|
1131
|
+
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
### 6. DOM Manipulation (`@applyNode`)
|
|
1135
|
+
|
|
1136
|
+
Surgically add, prepend, insert, or replace nodes. Perfect for reactive updates without full re-renders.
|
|
1137
|
+
|
|
131
1138
|
#### Pattern 1: Direct Binding & Static Selectors
|
|
132
1139
|
|
|
133
1140
|
```typescript
|
|
@@ -257,17 +1264,11 @@ class ResponsiveLayout extends HTMLElement {
|
|
|
257
1264
|
}
|
|
258
1265
|
```
|
|
259
1266
|
|
|
260
|
-
###
|
|
1267
|
+
### 7. Dynamic Content Updates with `@applyNode`
|
|
261
1268
|
|
|
262
|
-
|
|
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
|
|
1269
|
+
#### Advanced Patterns
|
|
269
1270
|
|
|
270
|
-
|
|
1271
|
+
**Pattern 1: Simple Content Replacement with `replaceChildren`**
|
|
271
1272
|
|
|
272
1273
|
```typescript
|
|
273
1274
|
@Sim
|
|
@@ -326,7 +1327,21 @@ class SearchResults extends HTMLElement {
|
|
|
326
1327
|
}
|
|
327
1328
|
```
|
|
328
1329
|
|
|
329
|
-
|
|
1330
|
+
**Pattern 2: Setting HTML Content**
|
|
1331
|
+
|
|
1332
|
+
```typescript
|
|
1333
|
+
@applyNode('#message', { position: 'innerHtml' })
|
|
1334
|
+
displayMessage(text: string) {
|
|
1335
|
+
return `<p style="color: blue;">${text}</p>`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
@applyNode('#count', { position: 'innerText' })
|
|
1339
|
+
updateCounter() {
|
|
1340
|
+
return this.count.toString();
|
|
1341
|
+
}
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
**Pattern 3: Tab Content Switching
|
|
330
1345
|
|
|
331
1346
|
```typescript
|
|
332
1347
|
@Sim
|
|
@@ -370,7 +1385,7 @@ class TabbedPanel extends HTMLElement {
|
|
|
370
1385
|
}
|
|
371
1386
|
```
|
|
372
1387
|
|
|
373
|
-
###
|
|
1388
|
+
### 8. `@appendChild(selector, options?)`
|
|
374
1389
|
|
|
375
1390
|
Appends or inserts the method's return value relative to a target element. Perfect for dynamically building lists, stacking notifications, or adding children without replacing existing content.
|
|
376
1391
|
|
|
@@ -577,6 +1592,26 @@ class TextRenderer extends HTMLElement {
|
|
|
577
1592
|
}
|
|
578
1593
|
```
|
|
579
1594
|
|
|
1595
|
+
### 2b. `commentNode` - HTML Comments
|
|
1596
|
+
|
|
1597
|
+
Create HTML comment nodes (useful for debugging, markers, or template documentation).
|
|
1598
|
+
|
|
1599
|
+
```typescript
|
|
1600
|
+
@elementDefine('debug-component')
|
|
1601
|
+
class DebugComponent extends HTMLElement {
|
|
1602
|
+
@appendChild('#debug-area')
|
|
1603
|
+
addDebugComment(message: string) {
|
|
1604
|
+
// Create HTML comment node
|
|
1605
|
+
return commentNode`DEBUG: ${message} at ${new Date().toISOString()}`;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
@onConnectedInnerHtml
|
|
1609
|
+
render() {
|
|
1610
|
+
return `<div id="debug-area"></div>`;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
```
|
|
1614
|
+
|
|
580
1615
|
### 3. Element Factories (`html***Element`)
|
|
581
1616
|
|
|
582
1617
|
Specific factories for common tags. Each returns a **typed element instance**.
|
|
@@ -767,66 +1802,49 @@ Unlike traditional SPAs that force a global router and monolithic state tree, SW
|
|
|
767
1802
|
- **Decentralized DI**: Service dependencies are resolved within app scope, enabling natural code splitting and lazy loading
|
|
768
1803
|
- **Island Architecture**: Apps boot only when needed, reducing initial JavaScript payload
|
|
769
1804
|
|
|
1805
|
+
### ✨ Complete Commerce App Example
|
|
1806
|
+
|
|
1807
|
+
See the **[Commerce Example](examples/commerce/)** for a production-ready SPA implementation featuring:
|
|
1808
|
+
|
|
1809
|
+
- **Multi-page routing** (Home, Product Detail, Cart, Orders)
|
|
1810
|
+
- **Service layer** (ProductService, CartService, OrderService) with localStorage persistence
|
|
1811
|
+
- **Reactive state management** using BehaviorSubject
|
|
1812
|
+
- **Modern UI design** with gradient backgrounds, responsive layouts, and sticky header
|
|
1813
|
+
- **Product images** from dynamic CDN (picsum.photos)
|
|
1814
|
+
- **DI integration** with `@onBeforeConnected` lifecycle hooks
|
|
1815
|
+
- **Shadow DOM** for style encapsulation
|
|
1816
|
+
- **Real-time updates** with cart count synchronization
|
|
1817
|
+
|
|
1818
|
+
```bash
|
|
1819
|
+
cd packages/@dooboostore/simple-web-component/examples/commerce
|
|
1820
|
+
pnpm install
|
|
1821
|
+
pnpm build
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
This example demonstrates all decorators working together in a real-world e-commerce scenario.
|
|
1825
|
+
|
|
770
1826
|
---
|
|
771
1827
|
|
|
772
|
-
###
|
|
1828
|
+
### Advanced Patterns
|
|
1829
|
+
|
|
1830
|
+
#### Isolated App Scopes & Micro-Frontend Pattern
|
|
773
1831
|
|
|
774
|
-
Create
|
|
1832
|
+
Create self-contained apps with `<swc-app>` where routing and DI are completely isolated:
|
|
775
1833
|
|
|
776
1834
|
```html
|
|
777
|
-
<!-- Example: Multi-tenant dashboard with independent feature modules -->
|
|
778
1835
|
<div class="dashboard">
|
|
779
|
-
<!-- User Management Module -->
|
|
780
1836
|
<swc-app swc-get-application-config="createUserModule()"></swc-app>
|
|
781
|
-
|
|
782
|
-
<!-- Reports Module -->
|
|
783
1837
|
<swc-app swc-get-application-config="createReportsModule()"></swc-app>
|
|
784
|
-
|
|
785
|
-
<!-- Settings Module -->
|
|
786
|
-
<swc-app swc-get-application-config="createSettingsModule()"></swc-app>
|
|
787
1838
|
</div>
|
|
788
|
-
|
|
789
|
-
<script>
|
|
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
|
-
}
|
|
810
|
-
|
|
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
|
-
});
|
|
816
|
-
}
|
|
817
|
-
</script>
|
|
818
1839
|
```
|
|
819
1840
|
|
|
820
|
-
|
|
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**
|
|
1841
|
+
Each app is independent: state, services, and routes don't cross boundaries.
|
|
824
1842
|
|
|
825
1843
|
---
|
|
826
1844
|
|
|
827
|
-
|
|
1845
|
+
#### DI-Powered Routing with Lifecycle Hooks
|
|
828
1846
|
|
|
829
|
-
SWC routing
|
|
1847
|
+
SWC routing injects services directly into components using `canActivate` lifecycle hook for permission checks and data preloading:
|
|
830
1848
|
|
|
831
1849
|
```typescript
|
|
832
1850
|
// ============================================
|