@dooboostore/simple-web-component 1.0.21 → 1.0.22
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 +771 -95
- package/dist/cjs/decorators/addEventListener.js +4 -0
- package/dist/cjs/decorators/addEventListener.js.map +2 -2
- package/dist/cjs/decorators/{appendChild.js → applyNode.js} +25 -17
- package/dist/cjs/decorators/applyNode.js.map +7 -0
- package/dist/cjs/decorators/attribute.js +31 -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 +11 -1
- package/dist/cjs/decorators/classList.js.map +2 -2
- package/dist/cjs/decorators/elementDefine.js +106 -55
- 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 -3
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/lifecycles.js +4 -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/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/esm/decorators/addEventListener.js +4 -0
- package/dist/esm/decorators/addEventListener.js.map +2 -2
- package/dist/esm/decorators/{appendChild.js → applyNode.js} +22 -14
- package/dist/esm/decorators/applyNode.js.map +7 -0
- package/dist/esm/decorators/attribute.js +31 -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 +11 -1
- package/dist/esm/decorators/classList.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +106 -55
- 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 -3
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/lifecycles.js +4 -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/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-bundle/dooboostore-simple-web-component.esm.js +195 -139
- 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 +38 -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 +25 -0
- 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 -3
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/lifecycles.d.ts +2 -0
- package/dist/types/decorators/lifecycles.d.ts.map +1 -1
- package/dist/types/decorators/query.d.ts +6 -0
- 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/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/umd-bundle/dooboostore-simple-web-component.umd.js +195 -139
- 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 +127 -0
- package/src/decorators/attribute.ts +46 -12
- package/src/decorators/{changedAttribute.ts → changedAttributeHost.ts} +5 -1
- package/src/decorators/classList.ts +32 -0
- package/src/decorators/elementDefine.ts +130 -62
- package/src/decorators/emitCustomEvent.ts +13 -5
- package/src/decorators/index.ts +2 -3
- package/src/decorators/lifecycles.ts +3 -1
- package/src/decorators/query.ts +9 -0
- package/src/decorators/queryAll.ts +9 -0
- package/src/decorators/setAttribute.ts +8 -0
- 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/dist/cjs/decorators/appendChild.js.map +0 -7
- package/dist/cjs/decorators/changedAttribute.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.map +0 -7
- package/dist/esm/decorators/changedAttribute.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/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/replaceChildren.ts +0 -64
package/README.md
CHANGED
|
@@ -30,67 +30,715 @@ 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.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
@elementDefine('resource-component')
|
|
164
|
+
class ResourceComponent extends HTMLElement {
|
|
165
|
+
private timerId: number;
|
|
166
|
+
private subscription: Subscription;
|
|
167
|
+
|
|
168
|
+
@onAfterDisconnected // or @onDisconnected (alias)
|
|
169
|
+
cleanup() {
|
|
170
|
+
// Cancel timers
|
|
171
|
+
clearTimeout(this.timerId);
|
|
172
|
+
|
|
173
|
+
// Unsubscribe from observables
|
|
174
|
+
this.subscription?.unsubscribe();
|
|
175
|
+
|
|
176
|
+
console.log('Resources cleaned up');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Execution Order**: After `disconnectedCallback()`
|
|
182
|
+
|
|
183
|
+
#### 6. `@onBeforeAdopted()`
|
|
184
|
+
Called before the element is adopted by a new document (useful for SSR/iframe scenarios).
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
@elementDefine('document-aware')
|
|
188
|
+
class DocumentAware extends HTMLElement {
|
|
189
|
+
@onBeforeAdopted
|
|
190
|
+
prepareForAdoption() {
|
|
191
|
+
console.log('Adopting to new document...');
|
|
192
|
+
// Prepare for document change
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### 7. `@onAfterAdopted()` / `@onAdopted()`
|
|
198
|
+
Called after the element is adopted by a new document.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
@elementDefine('iframe-component')
|
|
202
|
+
class IframeComponent extends HTMLElement {
|
|
203
|
+
@onAfterAdopted // or @onAdopted (alias)
|
|
204
|
+
onDocumentAdopted() {
|
|
205
|
+
console.log('Now in new document context');
|
|
206
|
+
// Re-attach to new document's resources
|
|
207
|
+
this.initializeInNewDocument();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private initializeInNewDocument() {
|
|
211
|
+
// Setup for new document
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### 8. `@onAddEventListener()`
|
|
217
|
+
Called when an event listener is added to the element. For event tracking/analytics.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
@elementDefine('event-tracked')
|
|
221
|
+
class EventTracked extends HTMLElement {
|
|
222
|
+
@onAddEventListener
|
|
223
|
+
onListenerAdded(type: string, listener: EventListener, options?: AddEventListenerOptions) {
|
|
224
|
+
console.log(`Event listener added: ${type}`);
|
|
225
|
+
// Track event listeners for debugging
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### 9. `@onConnectedInnerHtml(options?)`
|
|
231
|
+
Defines the component's template. Renders to Shadow DOM or Light DOM based on `useShadow` option.
|
|
47
232
|
|
|
48
233
|
```typescript
|
|
49
|
-
@elementDefine('
|
|
50
|
-
class
|
|
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
|
+
`;
|
|
54
251
|
}
|
|
55
252
|
}
|
|
56
253
|
```
|
|
57
254
|
|
|
58
|
-
|
|
59
|
-
|
|
255
|
+
**Options:**
|
|
256
|
+
- `useShadow: true` - Render to Shadow DOM (style encapsulation)
|
|
257
|
+
- `useShadow: false` - Render to Light DOM (default)
|
|
258
|
+
|
|
259
|
+
---
|
|
60
260
|
|
|
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.
|
|
261
|
+
### Attribute & Property Decorators
|
|
66
262
|
|
|
67
|
-
|
|
68
|
-
|
|
263
|
+
#### 10. `@attributeHost(name, options?)`
|
|
264
|
+
Bidirectional attribute binding with automatic type conversion.
|
|
69
265
|
|
|
70
266
|
```typescript
|
|
71
|
-
@elementDefine('
|
|
72
|
-
class
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
76
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(name, options?)`
|
|
316
|
+
Reflect method's return value to a 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
|
+
|
|
323
|
+
// Reflect status to DOM attribute
|
|
324
|
+
@setAttribute('data-status')
|
|
325
|
+
getStatus() {
|
|
326
|
+
return this.status;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Reflect computed data to attribute
|
|
77
330
|
@setAttribute('count')
|
|
78
|
-
|
|
331
|
+
getItemCount() {
|
|
332
|
+
return this.getItems().length.toString();
|
|
333
|
+
}
|
|
79
334
|
|
|
80
|
-
|
|
81
|
-
|
|
335
|
+
// Reflect to Shadow DOM element
|
|
336
|
+
@setAttribute('aria-label', { root: 'shadow' })
|
|
337
|
+
getAriaLabel() {
|
|
338
|
+
return `Status is ${this.status}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private getItems() {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@onConnectedInnerHtml
|
|
346
|
+
render() {
|
|
347
|
+
return `
|
|
348
|
+
<div class="badge" data-status="${this.status}">
|
|
349
|
+
Status: ${this.status}
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
82
353
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
//
|
|
87
|
-
if (this.displayEl) this.displayEl.textContent = newVal;
|
|
354
|
+
// Change status and update DOM
|
|
355
|
+
setStatus(newStatus: typeof this.status) {
|
|
356
|
+
this.status = newStatus;
|
|
357
|
+
this.getStatus(); // Triggers @setAttribute update
|
|
88
358
|
}
|
|
89
359
|
}
|
|
90
360
|
```
|
|
91
361
|
|
|
92
|
-
|
|
93
|
-
|
|
362
|
+
**Key Differences:**
|
|
363
|
+
- **`@attributeHost`** - Two-way binding: DOM ↔ Class (Bidirectional)
|
|
364
|
+
- **`@setAttribute`** - One-way binding: Class → DOM (Method return reflects to attribute)
|
|
365
|
+
- **`@changedAttributeHost`** - Watch DOM attribute changes (Reactive handler)
|
|
366
|
+
|
|
367
|
+
**Options:**
|
|
368
|
+
- `root: 'light'` - Set attribute on light DOM element
|
|
369
|
+
- `root: 'shadow'` - Set attribute on shadow DOM element
|
|
370
|
+
- `root: 'auto'` - Auto-detect (default)
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### Query & DOM Selection Decorators
|
|
375
|
+
|
|
376
|
+
#### 13. `@query(selector, options?)`
|
|
377
|
+
Inject a single element into a property.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
@elementDefine('form-handler')
|
|
381
|
+
class FormHandler extends HTMLElement {
|
|
382
|
+
@query('#submit-btn') submitBtn?: HTMLButtonElement;
|
|
383
|
+
@query('.form-field', { root: 'shadow' }) formField?: HTMLElement;
|
|
384
|
+
@query(':host') self?: HTMLElement;
|
|
385
|
+
|
|
386
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
387
|
+
render() {
|
|
388
|
+
return `
|
|
389
|
+
<div class="form-field">
|
|
390
|
+
<input type="text" placeholder="Enter name">
|
|
391
|
+
<button id="submit-btn">Submit</button>
|
|
392
|
+
</div>
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Root Options:**
|
|
399
|
+
- `'auto'` (default) - Search shadow DOM first, then light DOM
|
|
400
|
+
- `'light'` - Search only light DOM
|
|
401
|
+
- `'shadow'` - Search only shadow DOM
|
|
402
|
+
- `'all'` - Search both, return `[lightElements, shadowElements]`
|
|
403
|
+
|
|
404
|
+
**Special Targets:**
|
|
405
|
+
```typescript
|
|
406
|
+
@query(':host') self?: HTMLElement; // Element itself
|
|
407
|
+
@query(':parentHost') parent?: HTMLElement; // Parent SWC component
|
|
408
|
+
@query(':appHost') app?: SwcAppInterface; // Nearest <swc-app>
|
|
409
|
+
@query(':window') win?: Window; // Global window
|
|
410
|
+
@query(':document') doc?: Document; // Global document
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### 14. `@queryAll(selector, options?)`
|
|
414
|
+
Inject multiple elements as an array.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
@elementDefine('list-renderer')
|
|
418
|
+
class ListRenderer extends HTMLElement {
|
|
419
|
+
@queryAll('.item') items?: HTMLElement[];
|
|
420
|
+
@queryAll('button', { root: 'shadow' }) buttons?: HTMLButtonElement[];
|
|
421
|
+
|
|
422
|
+
getItemCount() {
|
|
423
|
+
return this.items?.length ?? 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
### Event & Messaging Decorators
|
|
431
|
+
|
|
432
|
+
#### 15. `@addEventListener(selector, event, options?)`
|
|
433
|
+
Declarative event binding with automatic delegation.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
@elementDefine('todo-app')
|
|
437
|
+
class TodoApp extends HTMLElement {
|
|
438
|
+
private todos: any[] = [];
|
|
439
|
+
|
|
440
|
+
// Direct binding to specific element
|
|
441
|
+
@addEventListener('#add-btn', 'click')
|
|
442
|
+
onAddTodo(event: MouseEvent) {
|
|
443
|
+
console.log('Add button clicked');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Event delegation for dynamic elements
|
|
447
|
+
@addEventListener('.todo-item', 'click', { delegate: true })
|
|
448
|
+
onTodoClick(event: MouseEvent, hostSet: any, target: HTMLElement) {
|
|
449
|
+
const itemId = target.getAttribute('data-id');
|
|
450
|
+
console.log('Clicked todo:', itemId);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Global window events
|
|
454
|
+
@addEventListener(':window', 'resize', { removeOnDisconnected: true })
|
|
455
|
+
onWindowResize(event: Event) {
|
|
456
|
+
console.log('Window resized');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Parent app events
|
|
460
|
+
@addEventListener(':appHost', 'custom-event')
|
|
461
|
+
onCustomEvent(event: CustomEvent) {
|
|
462
|
+
console.log('Event received:', event.detail);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@onConnectedInnerHtml
|
|
466
|
+
render() {
|
|
467
|
+
return `
|
|
468
|
+
<div>
|
|
469
|
+
<input type="text" id="todo-input" placeholder="Add todo...">
|
|
470
|
+
<button id="add-btn">Add</button>
|
|
471
|
+
<ul class="todo-list">
|
|
472
|
+
${this.todos.map(t => `
|
|
473
|
+
<li class="todo-item" data-id="${t.id}">${t.text}</li>
|
|
474
|
+
`).join('')}
|
|
475
|
+
</ul>
|
|
476
|
+
</div>
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Features:**
|
|
483
|
+
- ✅ Direct binding to static selectors
|
|
484
|
+
- ✅ Event delegation for dynamic elements
|
|
485
|
+
- ✅ Global events (`:window`, `:document`)
|
|
486
|
+
- ✅ Parent communication (`:parentHost`, `:appHost`)
|
|
487
|
+
- ✅ Auto-cleanup on `disconnectedCallback`
|
|
488
|
+
|
|
489
|
+
#### 16. `@emitCustomEvent(target, type, options?)`
|
|
490
|
+
Emit custom events to component hierarchy or global targets.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
@elementDefine('auth-panel')
|
|
494
|
+
class AuthPanel extends HTMLElement {
|
|
495
|
+
constructor(private authService: AuthService) { super(); }
|
|
496
|
+
|
|
497
|
+
// Emit to parent app when user logs in
|
|
498
|
+
@emitCustomEvent(':appHost', 'user-logged-in')
|
|
499
|
+
async onLogin() {
|
|
500
|
+
const user = await this.authService.login();
|
|
501
|
+
return { user, timestamp: Date.now() };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Emit globally when logout happens
|
|
505
|
+
@emitCustomEvent(':window', 'user-logged-out')
|
|
506
|
+
async onLogout() {
|
|
507
|
+
await this.authService.logout();
|
|
508
|
+
return { timestamp: Date.now() };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Emit to direct parent
|
|
512
|
+
@emitCustomEvent(':parentHost', 'child-ready')
|
|
513
|
+
notifyParent() {
|
|
514
|
+
return { childElement: this };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@onConnectedInnerHtml
|
|
518
|
+
render() {
|
|
519
|
+
return `
|
|
520
|
+
<div>
|
|
521
|
+
<button swc-on-click="$host.onLogin()">Login</button>
|
|
522
|
+
<button swc-on-click="$host.onLogout()">Logout</button>
|
|
523
|
+
</div>
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Listen to the emitted event
|
|
529
|
+
@elementDefine('user-widget')
|
|
530
|
+
class UserWidget extends HTMLElement {
|
|
531
|
+
private user: any = null;
|
|
532
|
+
|
|
533
|
+
@addEventListener(':appHost', 'user-logged-in')
|
|
534
|
+
onUserLoggedIn(event: CustomEvent) {
|
|
535
|
+
this.user = event.detail.user;
|
|
536
|
+
this.render();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@onConnectedInnerHtml
|
|
540
|
+
render() {
|
|
541
|
+
return `<p>User: ${this.user?.name ?? 'Anonymous'}</p>`;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Emission Targets:**
|
|
547
|
+
- `':appHost'` - Nearest parent `<swc-app>` (contained within app)
|
|
548
|
+
- `':parentHost'` - Direct parent SWC component
|
|
549
|
+
- `':firstHost'` / `':lastHost'` - First/last component in hierarchy
|
|
550
|
+
- `':firstAppHost'` / `':lastAppHost'` - First/last `<swc-app>` in hierarchy
|
|
551
|
+
- `':hosts'` - All SWC ancestors
|
|
552
|
+
- `':appHosts'` - All `<swc-app>` ancestors
|
|
553
|
+
- `':window'` - Global window (all apps can listen)
|
|
554
|
+
- `':document'` - Global document
|
|
555
|
+
- `selector` - CSS selector (emit to matching elements)
|
|
556
|
+
|
|
557
|
+
**Options:**
|
|
558
|
+
- `bubbles: true` (default) - Event bubbles up
|
|
559
|
+
- `composed: true` (default) - Event crosses Shadow DOM boundary
|
|
560
|
+
- `cancelable: true` - Event can be canceled
|
|
561
|
+
- `root: 'auto' | 'light' | 'shadow'` - Where to find target elements
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
### DOM Manipulation Decorators
|
|
566
|
+
|
|
567
|
+
#### 17. `@applyNode(selector, options?)`
|
|
568
|
+
Surgically add, update, or replace DOM nodes at specific positions.
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
@elementDefine('counter-display')
|
|
572
|
+
class CounterDisplay extends HTMLElement {
|
|
573
|
+
private count = 0;
|
|
574
|
+
|
|
575
|
+
// Replace all children with new content
|
|
576
|
+
@applyNode('#display', { position: 'replaceChildren' })
|
|
577
|
+
updateDisplayCount() {
|
|
578
|
+
return `<span>${this.count}</span>`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Append to container
|
|
582
|
+
@applyNode('#history', { position: 'beforeEnd' })
|
|
583
|
+
addHistoryEntry() {
|
|
584
|
+
return `<p>Count was: ${this.count}</p>`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Set HTML content
|
|
588
|
+
@applyNode('#status', { position: 'innerHtml' })
|
|
589
|
+
setStatus() {
|
|
590
|
+
return `<strong>Status: Active</strong>`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Set text content
|
|
594
|
+
@applyNode('#counter', { position: 'innerText' })
|
|
595
|
+
updateCounter() {
|
|
596
|
+
return this.count.toString();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@onConnectedInnerHtml
|
|
600
|
+
render() {
|
|
601
|
+
return `
|
|
602
|
+
<div id="display">0</div>
|
|
603
|
+
<div id="counter">0</div>
|
|
604
|
+
<div id="status"></div>
|
|
605
|
+
<button swc-on-click="$host.count++; $host.updateDisplayCount?.()">+</button>
|
|
606
|
+
<div id="history"></div>
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Position Options:**
|
|
613
|
+
- `'beforeEnd'` (default) - Append to end
|
|
614
|
+
- `'afterBegin'` - Prepend to start
|
|
615
|
+
- `'beforeBegin'` - Insert before element
|
|
616
|
+
- `'afterEnd'` - Insert after element
|
|
617
|
+
- `'replace'` - Replace entire element
|
|
618
|
+
- `'replaceChildren'` - Replace all children
|
|
619
|
+
- `'innerHtml'` - Set `innerHTML`
|
|
620
|
+
- `'innerText'` - Set `innerText`
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
### Dependency Injection & Singleton
|
|
625
|
+
|
|
626
|
+
#### 18. `@Sim`
|
|
627
|
+
Marks a class as a **Singleton** in the `<swc-app>` DI container for **constructor injection**.
|
|
628
|
+
|
|
629
|
+
**Two DI Mechanisms:**
|
|
630
|
+
|
|
631
|
+
1. **Lifecycle DI** (`@onBeforeConnected`, etc.) - ❌ NO `@Sim` needed
|
|
632
|
+
- Works on any `@elementDefine` component
|
|
633
|
+
- Services injected via method parameters
|
|
634
|
+
- Can be used outside `<swc-app>`
|
|
635
|
+
|
|
636
|
+
2. **Constructor DI** - ✅ `@Sim` REQUIRED
|
|
637
|
+
- Only works inside `<swc-app>` boundary
|
|
638
|
+
- Both provider AND consumer need `@Sim`
|
|
639
|
+
- Singleton pattern within the app
|
|
640
|
+
|
|
641
|
+
**Example: Both Approaches**
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
// ============ SERVICE ============
|
|
645
|
+
@Sim
|
|
646
|
+
class CartService {
|
|
647
|
+
private items: any[] = [];
|
|
648
|
+
|
|
649
|
+
addItem(item: any) {
|
|
650
|
+
this.items.push(item);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
getItems() {
|
|
654
|
+
return this.items;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ============ APPROACH 1: Lifecycle DI (NO @Sim needed) ============
|
|
659
|
+
@elementDefine('cart-header') // ← NO @Sim
|
|
660
|
+
class CartHeader extends HTMLElement {
|
|
661
|
+
#cartService: CartService;
|
|
662
|
+
|
|
663
|
+
@onBeforeConnected
|
|
664
|
+
async setup(cartService: CartService) { // ← Injected here
|
|
665
|
+
this.#cartService = cartService;
|
|
666
|
+
await this.#cartService.load?.();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
@onConnectedInnerHtml
|
|
670
|
+
render() {
|
|
671
|
+
return `<div>Cart: ${this.#cartService?.getItems().length}</div>`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ============ APPROACH 2: Constructor DI (NEEDS @Sim) ============
|
|
676
|
+
@Sim // ← Required for constructor injection
|
|
677
|
+
@elementDefine('cart-component')
|
|
678
|
+
class CartComponent extends HTMLElement {
|
|
679
|
+
// Constructor injection - CartService is auto-injected
|
|
680
|
+
constructor(private cartService: CartService) { super(); }
|
|
681
|
+
|
|
682
|
+
@onConnectedInnerHtml
|
|
683
|
+
render() {
|
|
684
|
+
const itemCount = this.cartService.getItems().length;
|
|
685
|
+
return `<div>Items: ${itemCount}</div>`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ============ ROUTING & BOOTSTRAP ============
|
|
690
|
+
@Sim
|
|
691
|
+
@Router({
|
|
692
|
+
path: '/',
|
|
693
|
+
route: {
|
|
694
|
+
'/home': HomePage,
|
|
695
|
+
'/cart': CartComponent // Constructor DI works here
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
@elementDefine('app-router')
|
|
699
|
+
class AppRouter extends HTMLElement {
|
|
700
|
+
@onConnectedInnerHtml
|
|
701
|
+
render() {
|
|
702
|
+
return `<div><slot></slot></div>`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ============ HTML ============
|
|
707
|
+
<!-- Lifecycle DI works OUTSIDE <swc-app> -->
|
|
708
|
+
<cart-header></cart-header>
|
|
709
|
+
|
|
710
|
+
<!-- Constructor DI works INSIDE <swc-app> -->
|
|
711
|
+
<swc-app swc-get-application-config="appConfig()">
|
|
712
|
+
<app-router></app-router>
|
|
713
|
+
</swc-app>
|
|
714
|
+
|
|
715
|
+
<script>
|
|
716
|
+
window.appConfig = () => ({
|
|
717
|
+
rootRouter: AppRouter,
|
|
718
|
+
routeType: 'element',
|
|
719
|
+
path: '/'
|
|
720
|
+
});
|
|
721
|
+
</script>
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**When to Use Each:**
|
|
725
|
+
|
|
726
|
+
| Scenario | Use | Mark |
|
|
727
|
+
|----------|-----|------|
|
|
728
|
+
| Component needs services anywhere | Lifecycle DI in `@onBeforeConnected` | NO `@Sim` |
|
|
729
|
+
| Component inside `<swc-app>` route | Constructor DI | YES `@Sim` |
|
|
730
|
+
| Service/Singleton registration | Register in DI container | YES `@Sim` |
|
|
731
|
+
| Standalone component | Lifecycle DI | NO `@Sim` |
|
|
732
|
+
|
|
733
|
+
**Key Points:**
|
|
734
|
+
- `@Sim` = Mark as singleton for **constructor injection inside `<swc-app>`**
|
|
735
|
+
- Lifecycle DI = Inject via method parameters (works everywhere, no `@Sim`)
|
|
736
|
+
- Each `<swc-app>` has its own isolated DI container
|
|
737
|
+
- Services are local to their `<swc-app>` instance (for constructor DI)
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## Core Decorators
|
|
94
742
|
|
|
95
743
|
#### Root Options
|
|
96
744
|
The `root` option controls which DOM tree is searched:
|
|
@@ -117,7 +765,7 @@ Standard CSS selectors support precise root control.
|
|
|
117
765
|
@query('.btn', { root: 'shadow' }) shadowBtn!: HTMLButtonElement;
|
|
118
766
|
```
|
|
119
767
|
|
|
120
|
-
### 5.
|
|
768
|
+
### 5. Event Binding (`@addEventListener`)
|
|
121
769
|
|
|
122
770
|
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
771
|
|
|
@@ -128,6 +776,43 @@ Bind events declaratively with built-in **Event Delegation**, **Auto-Cleanup**,
|
|
|
128
776
|
- **Context Injection**: Receive `$host`, `$parentHost`, `$appHost` automatically
|
|
129
777
|
- **Special Targets**: `:window`, `:document`, `:parentHost`, `:appHost`
|
|
130
778
|
|
|
779
|
+
#### 6. Surgical DOM Manipulation (`@applyNode`)
|
|
780
|
+
|
|
781
|
+
Surgically add, prepend, insert, or replace nodes with a single decorator. Perfect for dynami content updates without imperatively manipulating the DOM.
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
@elementDefine('list-manager')
|
|
785
|
+
class ListManager extends HTMLElement {
|
|
786
|
+
@applyNode('#list') addItem() {
|
|
787
|
+
return '<li>New Item</li>';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
@applyNode('#header', { position: 'afterBegin' }) prependTitle() {
|
|
791
|
+
return '<h1>Title</h1>';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
@applyNode('#item', { position: 'replace' }) replaceItem() {
|
|
795
|
+
return '<div>Replaced Content</div>';
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
**Positions:**
|
|
801
|
+
- `'beforeEnd'` (default): Append to end of target
|
|
802
|
+
- `'afterBegin'`: Prepend to start of target
|
|
803
|
+
- `'beforeBegin'`: Insert before target
|
|
804
|
+
- `'afterEnd'`: Insert after target
|
|
805
|
+
- `'replace'`: Replace entire target element
|
|
806
|
+
- `'replaceChildren'`: Replace all children of target element
|
|
807
|
+
- `'innerHtml'`: Set target's `innerHTML` with string content
|
|
808
|
+
- `'innerText'`: Set target's `innerText` with string content
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
### 6. DOM Manipulation (`@applyNode`)
|
|
813
|
+
|
|
814
|
+
Surgically add, prepend, insert, or replace nodes. Perfect for reactive updates without full re-renders.
|
|
815
|
+
|
|
131
816
|
#### Pattern 1: Direct Binding & Static Selectors
|
|
132
817
|
|
|
133
818
|
```typescript
|
|
@@ -257,17 +942,11 @@ class ResponsiveLayout extends HTMLElement {
|
|
|
257
942
|
}
|
|
258
943
|
```
|
|
259
944
|
|
|
260
|
-
###
|
|
261
|
-
|
|
262
|
-
Surgically replaces the children of a target element. Perfect for dynamic content updates without full-page re-renders.
|
|
945
|
+
### 7. Dynamic Content Updates with `@applyNode`
|
|
263
946
|
|
|
264
|
-
|
|
265
|
-
- Search results updating without page reload
|
|
266
|
-
- Filtering product lists
|
|
267
|
-
- Content swaps in tabbed interfaces
|
|
268
|
-
- Live data refreshes
|
|
947
|
+
#### Advanced Patterns
|
|
269
948
|
|
|
270
|
-
|
|
949
|
+
**Pattern 1: Simple Content Replacement with `replaceChildren`**
|
|
271
950
|
|
|
272
951
|
```typescript
|
|
273
952
|
@Sim
|
|
@@ -326,7 +1005,21 @@ class SearchResults extends HTMLElement {
|
|
|
326
1005
|
}
|
|
327
1006
|
```
|
|
328
1007
|
|
|
329
|
-
|
|
1008
|
+
**Pattern 2: Setting HTML Content**
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1011
|
+
@applyNode('#message', { position: 'innerHtml' })
|
|
1012
|
+
displayMessage(text: string) {
|
|
1013
|
+
return `<p style="color: blue;">${text}</p>`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
@applyNode('#count', { position: 'innerText' })
|
|
1017
|
+
updateCounter() {
|
|
1018
|
+
return this.count.toString();
|
|
1019
|
+
}
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
**Pattern 3: Tab Content Switching
|
|
330
1023
|
|
|
331
1024
|
```typescript
|
|
332
1025
|
@Sim
|
|
@@ -370,7 +1063,7 @@ class TabbedPanel extends HTMLElement {
|
|
|
370
1063
|
}
|
|
371
1064
|
```
|
|
372
1065
|
|
|
373
|
-
###
|
|
1066
|
+
### 8. `@appendChild(selector, options?)`
|
|
374
1067
|
|
|
375
1068
|
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
1069
|
|
|
@@ -767,66 +1460,49 @@ Unlike traditional SPAs that force a global router and monolithic state tree, SW
|
|
|
767
1460
|
- **Decentralized DI**: Service dependencies are resolved within app scope, enabling natural code splitting and lazy loading
|
|
768
1461
|
- **Island Architecture**: Apps boot only when needed, reducing initial JavaScript payload
|
|
769
1462
|
|
|
1463
|
+
### ✨ Complete Commerce App Example
|
|
1464
|
+
|
|
1465
|
+
See the **[Commerce Example](examples/commerce/)** for a production-ready SPA implementation featuring:
|
|
1466
|
+
|
|
1467
|
+
- **Multi-page routing** (Home, Product Detail, Cart, Orders)
|
|
1468
|
+
- **Service layer** (ProductService, CartService, OrderService) with localStorage persistence
|
|
1469
|
+
- **Reactive state management** using BehaviorSubject
|
|
1470
|
+
- **Modern UI design** with gradient backgrounds, responsive layouts, and sticky header
|
|
1471
|
+
- **Product images** from dynamic CDN (picsum.photos)
|
|
1472
|
+
- **DI integration** with `@onBeforeConnected` lifecycle hooks
|
|
1473
|
+
- **Shadow DOM** for style encapsulation
|
|
1474
|
+
- **Real-time updates** with cart count synchronization
|
|
1475
|
+
|
|
1476
|
+
```bash
|
|
1477
|
+
cd packages/@dooboostore/simple-web-component/examples/commerce
|
|
1478
|
+
pnpm install
|
|
1479
|
+
pnpm build
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
This example demonstrates all decorators working together in a real-world e-commerce scenario.
|
|
1483
|
+
|
|
770
1484
|
---
|
|
771
1485
|
|
|
772
|
-
###
|
|
1486
|
+
### Advanced Patterns
|
|
1487
|
+
|
|
1488
|
+
#### Isolated App Scopes & Micro-Frontend Pattern
|
|
773
1489
|
|
|
774
|
-
Create
|
|
1490
|
+
Create self-contained apps with `<swc-app>` where routing and DI are completely isolated:
|
|
775
1491
|
|
|
776
1492
|
```html
|
|
777
|
-
<!-- Example: Multi-tenant dashboard with independent feature modules -->
|
|
778
1493
|
<div class="dashboard">
|
|
779
|
-
<!-- User Management Module -->
|
|
780
1494
|
<swc-app swc-get-application-config="createUserModule()"></swc-app>
|
|
781
|
-
|
|
782
|
-
<!-- Reports Module -->
|
|
783
1495
|
<swc-app swc-get-application-config="createReportsModule()"></swc-app>
|
|
784
|
-
|
|
785
|
-
<!-- Settings Module -->
|
|
786
|
-
<swc-app swc-get-application-config="createSettingsModule()"></swc-app>
|
|
787
1496
|
</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
1497
|
```
|
|
819
1498
|
|
|
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**
|
|
1499
|
+
Each app is independent: state, services, and routes don't cross boundaries.
|
|
824
1500
|
|
|
825
1501
|
---
|
|
826
1502
|
|
|
827
|
-
|
|
1503
|
+
#### DI-Powered Routing with Lifecycle Hooks
|
|
828
1504
|
|
|
829
|
-
SWC routing
|
|
1505
|
+
SWC routing injects services directly into components using `canActivate` lifecycle hook for permission checks and data preloading:
|
|
830
1506
|
|
|
831
1507
|
```typescript
|
|
832
1508
|
// ============================================
|