@dooboostore/simple-web-component 1.0.29 → 1.0.31
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 +651 -27
- package/dist/cjs/decorators/elementDefine.js +5 -3
- package/dist/cjs/decorators/elementDefine.js.map +2 -2
- package/dist/cjs/decorators/emitCustomEvent.js.map +2 -2
- package/dist/cjs/decorators/index.js +1 -0
- package/dist/cjs/decorators/index.js.map +2 -2
- package/dist/cjs/decorators/setProperty.js +97 -0
- package/dist/cjs/decorators/setProperty.js.map +7 -0
- package/dist/cjs/elements/SwcAppEngine.js +3 -3
- package/dist/cjs/elements/SwcAppEngine.js.map +2 -2
- package/dist/cjs/elements/SwcAppMixin.js +1 -8
- package/dist/cjs/elements/SwcAppMixin.js.map +2 -2
- package/dist/cjs/elements/SwcAsync.js +37 -53
- package/dist/cjs/elements/SwcAsync.js.map +2 -2
- package/dist/cjs/elements/SwcChoose.js +55 -60
- package/dist/cjs/elements/SwcChoose.js.map +2 -2
- package/dist/cjs/elements/SwcIf.js +31 -60
- package/dist/cjs/elements/SwcIf.js.map +2 -2
- package/dist/cjs/elements/SwcLoop.js +20 -43
- package/dist/cjs/elements/SwcLoop.js.map +2 -2
- package/dist/cjs/elements/SwcSubTemplates.js +0 -27
- package/dist/cjs/elements/SwcSubTemplates.js.map +2 -2
- package/dist/cjs/elements/register.js +216 -128
- package/dist/cjs/elements/register.js.map +2 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/router/ElementRouter.js +1 -8
- package/dist/cjs/router/ElementRouter.js.map +2 -2
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils/Utils.js +26 -10
- package/dist/cjs/utils/Utils.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +5 -3
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/decorators/emitCustomEvent.js.map +2 -2
- package/dist/esm/decorators/index.js +1 -0
- package/dist/esm/decorators/index.js.map +2 -2
- package/dist/esm/decorators/setProperty.js +78 -0
- package/dist/esm/decorators/setProperty.js.map +7 -0
- package/dist/esm/elements/SwcAppEngine.js +3 -3
- package/dist/esm/elements/SwcAppEngine.js.map +2 -2
- package/dist/esm/elements/SwcAppMixin.js +1 -8
- package/dist/esm/elements/SwcAppMixin.js.map +2 -2
- package/dist/esm/elements/SwcAsync.js +37 -34
- package/dist/esm/elements/SwcAsync.js.map +2 -2
- package/dist/esm/elements/SwcChoose.js +55 -41
- package/dist/esm/elements/SwcChoose.js.map +2 -2
- package/dist/esm/elements/SwcIf.js +31 -41
- package/dist/esm/elements/SwcIf.js.map +2 -2
- package/dist/esm/elements/SwcLoop.js +20 -24
- package/dist/esm/elements/SwcLoop.js.map +2 -2
- package/dist/esm/elements/SwcSubTemplates.js +0 -8
- package/dist/esm/elements/SwcSubTemplates.js.map +2 -2
- package/dist/esm/elements/register.js +216 -128
- package/dist/esm/elements/register.js.map +2 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/router/ElementRouter.js +1 -8
- package/dist/esm/router/ElementRouter.js.map +2 -2
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/Utils.js +26 -10
- package/dist/esm/utils/Utils.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +105 -13
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/decorators/emitCustomEvent.d.ts.map +1 -1
- package/dist/types/decorators/index.d.ts +1 -0
- package/dist/types/decorators/index.d.ts.map +1 -1
- package/dist/types/decorators/setProperty.d.ts +13 -0
- package/dist/types/decorators/setProperty.d.ts.map +1 -0
- package/dist/types/elements/SwcAppMixin.d.ts.map +1 -1
- package/dist/types/elements/SwcAsync.d.ts +1 -11
- package/dist/types/elements/SwcAsync.d.ts.map +1 -1
- package/dist/types/elements/SwcChoose.d.ts +1 -13
- package/dist/types/elements/SwcChoose.d.ts.map +1 -1
- package/dist/types/elements/SwcIf.d.ts +1 -13
- package/dist/types/elements/SwcIf.d.ts.map +1 -1
- package/dist/types/elements/SwcLoop.d.ts +1 -13
- package/dist/types/elements/SwcLoop.d.ts.map +1 -1
- package/dist/types/elements/SwcSubTemplates.d.ts +6 -6
- package/dist/types/elements/SwcSubTemplates.d.ts.map +1 -1
- package/dist/types/elements/register.d.ts +3 -1
- package/dist/types/elements/register.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/router/ElementRouter.d.ts.map +1 -1
- package/dist/types/types.d.ts +3 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/Utils.d.ts +10 -0
- package/dist/types/utils/Utils.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +105 -13
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
- package/package.json +3 -3
- package/src/decorators/elementDefine.ts +10 -3
- package/src/decorators/emitCustomEvent.ts +1 -0
- package/src/decorators/index.ts +1 -0
- package/src/decorators/setProperty.ts +97 -0
- package/src/elements/SwcAppEngine.ts +3 -3
- package/src/elements/SwcAppMixin.ts +7 -28
- package/src/elements/SwcAsync.ts +46 -30
- package/src/elements/SwcChoose.ts +82 -38
- package/src/elements/SwcIf.ts +38 -38
- package/src/elements/SwcLoop.ts +24 -19
- package/src/elements/SwcSubTemplates.ts +6 -6
- package/src/elements/register.ts +261 -121
- package/src/index.ts +3 -1
- package/src/router/ElementRouter.ts +1 -12
- package/src/types.ts +3 -1
- package/src/utils/Utils.ts +40 -19
package/README.md
CHANGED
|
@@ -17,8 +17,7 @@ Register Web Components with automatic lifecycle management and DI support.
|
|
|
17
17
|
@elementDefine('my-component')
|
|
18
18
|
class MyComponent extends HTMLElement {
|
|
19
19
|
@onInitialize
|
|
20
|
-
|
|
21
|
-
super();
|
|
20
|
+
onconstructor(service: MyService) {
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
```
|
|
@@ -30,10 +29,9 @@ Inject services into Web Components using the `@onInitialize` decorator.
|
|
|
30
29
|
@elementDefine('dashboard')
|
|
31
30
|
class Dashboard extends HTMLElement {
|
|
32
31
|
@onInitialize
|
|
33
|
-
|
|
32
|
+
onconstructor(
|
|
34
33
|
@Inject({ symbol: UserService.SYMBOL }) userService: UserService
|
|
35
34
|
) {
|
|
36
|
-
super();
|
|
37
35
|
this.userService = userService;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
@@ -220,8 +218,7 @@ Bind attributes on host element itself.
|
|
|
220
218
|
productId: string = '';
|
|
221
219
|
|
|
222
220
|
@onInitialize
|
|
223
|
-
|
|
224
|
-
super();
|
|
221
|
+
onconstructor() {
|
|
225
222
|
if (this.productId) {
|
|
226
223
|
this.loadProduct(this.productId);
|
|
227
224
|
}
|
|
@@ -241,39 +238,284 @@ onDisconnected() {
|
|
|
241
238
|
// Called when element leaves DOM
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
@onAttributeChanged
|
|
245
|
-
onAttributeChanged(name: string, oldValue: string, newValue: string) {
|
|
246
|
-
// Called when attribute changes
|
|
247
|
-
}
|
|
248
241
|
|
|
249
242
|
@onConnectedSwcApp
|
|
250
|
-
onAppReady(
|
|
243
|
+
onAppReady(router: Router) {
|
|
251
244
|
// Called after SwcApp.connect() completes on all already-connected components
|
|
252
245
|
// Full DI support available through hostSet
|
|
253
246
|
console.log('Application initialization complete!');
|
|
254
247
|
}
|
|
255
248
|
```
|
|
256
249
|
|
|
257
|
-
### 8. **
|
|
258
|
-
|
|
250
|
+
### 8. **Structural Directives**
|
|
251
|
+
|
|
252
|
+
Structural Directives allow declarative conditional rendering, list iteration, async handling, and routing with automatic attribute substitution and dynamic expressions.
|
|
253
|
+
|
|
254
|
+
#### 8.1 **SwcChoose - Multi-Condition Rendering**
|
|
255
|
+
|
|
256
|
+
Switch-case style conditional rendering with `swc-when` and `swc-otherwise` sub-templates.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// Component with conditional rendering logic
|
|
260
|
+
@elementDefine('status-display')
|
|
261
|
+
class StatusDisplay extends HTMLElement {
|
|
262
|
+
@attributeHost('status')
|
|
263
|
+
status: string = 'pending';
|
|
264
|
+
|
|
265
|
+
@setProperty('#status-template', 'value')
|
|
266
|
+
getStatus() {
|
|
267
|
+
return this.status;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@onConnectedInnerHtml
|
|
271
|
+
render() {
|
|
272
|
+
return `
|
|
273
|
+
<template id="status-template" is="swc-choose">
|
|
274
|
+
<!-- Pending State -->
|
|
275
|
+
<template is="swc-when" value="{{ $value === 'pending' }}">
|
|
276
|
+
<div style="color: orange;">⏳ Processing...</div>
|
|
277
|
+
</template>
|
|
278
|
+
|
|
279
|
+
<!-- Success State -->
|
|
280
|
+
<template is="swc-when" value="{{ $value === 'success' }}">
|
|
281
|
+
<div style="color: green;">✓ Completed</div>
|
|
282
|
+
</template>
|
|
283
|
+
|
|
284
|
+
<!-- Error State -->
|
|
285
|
+
<template is="swc-when" value="{{ $value === 'error' }}">
|
|
286
|
+
<div style="color: red;">✗ Failed</div>
|
|
287
|
+
</template>
|
|
288
|
+
|
|
289
|
+
<!-- Fallback -->
|
|
290
|
+
<template is="swc-otherwise">
|
|
291
|
+
<div style="color: gray;">Unknown status</div>
|
|
292
|
+
</template>
|
|
293
|
+
</template>
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Features:**
|
|
300
|
+
- `{{ }} braces` for dynamic condition evaluation
|
|
301
|
+
- `$value` contains the value passed to `swc-choose`
|
|
302
|
+
- `skip-if-same` attribute prevents re-render when same template selected
|
|
303
|
+
- Attribute substitution: `attribute-name="{{ expression }}"` automatically evaluated
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
#### 8.2 **SwcIf - Binary Conditional**
|
|
308
|
+
|
|
309
|
+
If-then-else rendering with optional `swc-default` fallback.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
@elementDefine('user-profile')
|
|
313
|
+
class UserProfile extends HTMLElement {
|
|
314
|
+
private isLoggedIn = false;
|
|
315
|
+
|
|
316
|
+
@setProperty('#content', 'value')
|
|
317
|
+
checkAuth() {
|
|
318
|
+
return this.isLoggedIn;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@onConnectedInnerHtml
|
|
322
|
+
render() {
|
|
323
|
+
return `
|
|
324
|
+
<!-- True condition: renders main template -->
|
|
325
|
+
<template id="content" is="swc-if" value="{{ $host.isLoggedIn }}">
|
|
326
|
+
<div>
|
|
327
|
+
<h2>Welcome, {{ $host.userName }}</h2>
|
|
328
|
+
<button on-click="logout">Logout</button>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<!-- Fallback template: renders if condition is false -->
|
|
332
|
+
<template is="swc-default">
|
|
333
|
+
<div>
|
|
334
|
+
<p>Please log in</p>
|
|
335
|
+
<button on-click="login">Login</button>
|
|
336
|
+
</div>
|
|
337
|
+
</template>
|
|
338
|
+
</template>
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Features:**
|
|
345
|
+
- Binary true/false logic
|
|
346
|
+
- Main template renders when `value` is truthy
|
|
347
|
+
- `swc-default` sub-template renders when value is falsy
|
|
348
|
+
- Dynamic attribute substitution supported
|
|
349
|
+
- Access `$host`, `$value`, and other context variables
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
#### 8.3 **SwcAsync - Promise/Loading State**
|
|
354
|
+
|
|
355
|
+
Automatically handle async operations with loading, success, and error states.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
@elementDefine('data-loader')
|
|
359
|
+
class DataLoader extends HTMLElement {
|
|
360
|
+
private data: any = null;
|
|
361
|
+
|
|
362
|
+
async fetchData() {
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
resolve({ id: 1, name: 'Sample Data' });
|
|
366
|
+
}, 2000);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@setProperty('#async-content', 'value')
|
|
371
|
+
loadData() {
|
|
372
|
+
return this.fetchData();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@onConnectedInnerHtml
|
|
376
|
+
render() {
|
|
377
|
+
return `
|
|
378
|
+
<template id="async-content" is="swc-async" value="{{ $host.fetchData() }}">
|
|
379
|
+
<!-- Loading State: Shows while Promise is pending -->
|
|
380
|
+
<template is="swc-loading">
|
|
381
|
+
<div>🔄 Loading data...</div>
|
|
382
|
+
</template>
|
|
383
|
+
|
|
384
|
+
<!-- Success State: Shows when Promise resolves -->
|
|
385
|
+
<template is="swc-success">
|
|
386
|
+
<div>
|
|
387
|
+
<p>Data ID: {{ $value.id }}</p>
|
|
388
|
+
<p>Data Name: {{ $value.name }}</p>
|
|
389
|
+
</div>
|
|
390
|
+
</template>
|
|
391
|
+
|
|
392
|
+
<!-- Error State: Shows if Promise rejects -->
|
|
393
|
+
<template is="swc-error">
|
|
394
|
+
<div style="color: red;">❌ Failed to load data</div>
|
|
395
|
+
</template>
|
|
396
|
+
</template>
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Features:**
|
|
403
|
+
- Automatically manages Promise lifecycle
|
|
404
|
+
- `swc-loading` - displays during pending state
|
|
405
|
+
- `swc-success` - displays on successful resolution, `$value` contains result
|
|
406
|
+
- `swc-error` - displays on Promise rejection
|
|
407
|
+
- Dynamic attribute substitution in all states
|
|
408
|
+
- `on-clone-node` and `on-clone-nodes` callbacks for DOM manipulation
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
#### 8.4 **SwcLoop - List Rendering**
|
|
413
|
+
|
|
414
|
+
Iterate over arrays and render templates for each item.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
@elementDefine('product-list')
|
|
418
|
+
class ProductList extends HTMLElement {
|
|
419
|
+
private products = [
|
|
420
|
+
{ id: 1, name: 'Laptop', price: 1200 },
|
|
421
|
+
{ id: 2, name: 'Phone', price: 800 },
|
|
422
|
+
{ id: 3, name: 'Tablet', price: 500 }
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
@setProperty('#products-list', 'value')
|
|
426
|
+
getProducts() {
|
|
427
|
+
return this.products;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@onConnectedInnerHtml
|
|
431
|
+
render() {
|
|
432
|
+
return `
|
|
433
|
+
<template id="products-list" is="swc-loop" value="{{ $host.products }}">
|
|
434
|
+
<div style="margin: 10px; padding: 10px; border: 1px solid #ddd;">
|
|
435
|
+
<h3>{{ $item.name }}</h3>
|
|
436
|
+
<p>Price: ${{ $item.price }}</p>
|
|
437
|
+
<p>ID: {{ $item.id }} (Index: {{ $index }})</p>
|
|
438
|
+
<button data-id="{{ $item.id }}">Add to Cart</button>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- Empty fallback: renders when array is empty -->
|
|
442
|
+
<template is="swc-default">
|
|
443
|
+
<p>No products available</p>
|
|
444
|
+
</template>
|
|
445
|
+
</template>
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Context variables:**
|
|
452
|
+
- `$item` - current item in iteration
|
|
453
|
+
- `$index` - zero-based index
|
|
454
|
+
- `$value` - entire array
|
|
455
|
+
- `$nodes` - cloned DOM nodes
|
|
456
|
+
- `$elements` - cloned HTML elements
|
|
457
|
+
- `$firstElement` - first element in cloned nodes
|
|
458
|
+
|
|
459
|
+
**Features:**
|
|
460
|
+
- Efficient DOM cloning for each item
|
|
461
|
+
- `swc-default` fallback renders when array is empty
|
|
462
|
+
- All cloned attributes support `{{ }}` substitution
|
|
463
|
+
- `on-clone-node` callback fires for each cloned node
|
|
464
|
+
- `on-clone-nodes` callback fires after all nodes cloned
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
#### 8.5 **Declarative Routing with swc-choose**
|
|
469
|
+
|
|
470
|
+
Combine `@subscribeSwcAppRouteChange` with `swc-choose` for clean, declarative routing without boilerplate.
|
|
259
471
|
|
|
260
472
|
```typescript
|
|
261
473
|
@elementDefine('root-router')
|
|
262
474
|
class RootRouter extends HTMLElement {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
475
|
+
private router: Router;
|
|
476
|
+
|
|
477
|
+
@onInitialize
|
|
478
|
+
onconstructor(router: Router) {
|
|
479
|
+
this.router = router;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@setProperty('#router', 'value')
|
|
483
|
+
@subscribeSwcAppRouteChange(['/', '/product/{id}'])
|
|
484
|
+
routeChanged(router: RouterEventType) {
|
|
485
|
+
return router;
|
|
267
486
|
}
|
|
268
487
|
|
|
269
|
-
@
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
488
|
+
@onConnectedInnerHtml
|
|
489
|
+
render() {
|
|
490
|
+
return `
|
|
491
|
+
<template id="router" is="swc-choose">
|
|
492
|
+
<!-- Home -->
|
|
493
|
+
<template is="swc-when" value="{{ ['', '/'].includes($value?.path) }}">
|
|
494
|
+
<home-page/>
|
|
495
|
+
</template>
|
|
496
|
+
|
|
497
|
+
<!-- Product Detail -->
|
|
498
|
+
<template is="swc-when" value="{{ $value?.path.startsWith('/product/') }}">
|
|
499
|
+
<product-page product-id="{{$value?.pathData?.id}}"/>
|
|
500
|
+
</template>
|
|
501
|
+
|
|
502
|
+
<!-- Not Found -->
|
|
503
|
+
<template is="swc-otherwise">
|
|
504
|
+
<not-found-page/>
|
|
505
|
+
</template>
|
|
506
|
+
</template>
|
|
507
|
+
`;
|
|
273
508
|
}
|
|
274
509
|
}
|
|
275
510
|
```
|
|
276
511
|
|
|
512
|
+
**Benefits:**
|
|
513
|
+
- ✅ **No imperative route methods** - routes are declarative templates
|
|
514
|
+
- ✅ **Automatic re-rendering** - attributes like `product-id` update automatically
|
|
515
|
+
- ✅ **Performance optimization** - use `skip-if-same` on templates to prevent re-renders
|
|
516
|
+
- ✅ **Dynamic path parameters** - use `{{$value?.pathData?.id}}` syntax to inject route params
|
|
517
|
+
- ✅ **Cleaner DX** - single unified routing pattern
|
|
518
|
+
|
|
277
519
|
### 9. **Accommodation Pattern**
|
|
278
520
|
Factory-based component registration with explicit DI.
|
|
279
521
|
|
|
@@ -287,8 +529,7 @@ export default (w: Window) => {
|
|
|
287
529
|
@elementDefine(tagName, { window: w })
|
|
288
530
|
class MyComponent extends w.HTMLElement {
|
|
289
531
|
@onInitialize
|
|
290
|
-
|
|
291
|
-
super();
|
|
532
|
+
onconstructor(service: MyService) {
|
|
292
533
|
this.service = service;
|
|
293
534
|
}
|
|
294
535
|
}
|
|
@@ -314,6 +555,390 @@ appElement.connect({
|
|
|
314
555
|
});
|
|
315
556
|
```
|
|
316
557
|
|
|
558
|
+
### 10. **Quickstart - Basic SPA Setup**
|
|
559
|
+
|
|
560
|
+
Complete example of setting up a production SPA with routing, services, and components.
|
|
561
|
+
|
|
562
|
+
#### Step 1. HTML Entry Point
|
|
563
|
+
```html
|
|
564
|
+
<!DOCTYPE html>
|
|
565
|
+
<html>
|
|
566
|
+
<head>
|
|
567
|
+
<meta charset="UTF-8">
|
|
568
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
569
|
+
<title>My SPA</title>
|
|
570
|
+
<!-- Web Components polyfill for older browsers -->
|
|
571
|
+
<script src="https://unpkg.com/@ungap/custom-elements"></script>
|
|
572
|
+
</head>
|
|
573
|
+
<body id="app" is="swc-app-body">
|
|
574
|
+
<!-- Root router will render pages here -->
|
|
575
|
+
<root-router></root-router>
|
|
576
|
+
</body>
|
|
577
|
+
<script src="bundle.js"></script>
|
|
578
|
+
</html>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### Step 2. Services (Dependency Injection)
|
|
582
|
+
```typescript
|
|
583
|
+
// services/UserService.ts
|
|
584
|
+
import { Sim } from '@dooboostore/simple-boot';
|
|
585
|
+
import { ConstructorType } from '@dooboostore/core';
|
|
586
|
+
|
|
587
|
+
export namespace UserService {
|
|
588
|
+
export const SYMBOL = Symbol('UserService');
|
|
589
|
+
|
|
590
|
+
export interface User {
|
|
591
|
+
id: number;
|
|
592
|
+
name: string;
|
|
593
|
+
email: string;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const users: User[] = [
|
|
597
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
598
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export interface UserService {
|
|
603
|
+
getUser(id: number): UserService.User | undefined;
|
|
604
|
+
getAllUsers(): UserService.User[];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Factory function: accepts container and returns service class
|
|
608
|
+
export default (container: symbol): ConstructorType<any> => {
|
|
609
|
+
@Sim({ symbol: UserService.SYMBOL, container: container })
|
|
610
|
+
class UserServiceImpl implements UserService {
|
|
611
|
+
getUser(id: number) {
|
|
612
|
+
return UserService.users.find(u => u.id === id);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
getAllUsers() {
|
|
616
|
+
return [...UserService.users];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return UserServiceImpl;
|
|
621
|
+
};
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
// services/index.ts
|
|
626
|
+
import userServiceFactory from './UserService';
|
|
627
|
+
|
|
628
|
+
export const serviceFactories = [userServiceFactory];
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
#### Step 3. Router Configuration (Declarative with swc-choose)
|
|
632
|
+
```typescript
|
|
633
|
+
// pages/RootRouter.ts
|
|
634
|
+
import { elementDefine, subscribeSwcAppRouteChange, setProperty, onConnectedInnerHtml, onInitialize } from '@dooboostore/simple-web-component';
|
|
635
|
+
import { Router, RouterEventType } from '@dooboostore/core-web';
|
|
636
|
+
|
|
637
|
+
const rootRouterFactory = (w: Window) => {
|
|
638
|
+
const tagName = 'root-router';
|
|
639
|
+
const existing = w.customElements.get(tagName);
|
|
640
|
+
if (existing) return tagName;
|
|
641
|
+
|
|
642
|
+
@elementDefine(tagName, { window: w })
|
|
643
|
+
class RootRouter extends w.HTMLElement {
|
|
644
|
+
private router: Router;
|
|
645
|
+
|
|
646
|
+
@onInitialize
|
|
647
|
+
onconstructor(router: Router) {
|
|
648
|
+
this.router = router;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
@setProperty('#router', 'value')
|
|
652
|
+
@subscribeSwcAppRouteChange(['/', '/users', '/users/{id}'])
|
|
653
|
+
routeChanged(route: RouterEventType) {
|
|
654
|
+
return route;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
navigate(path: string): void {
|
|
658
|
+
this.router?.go(path);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@onConnectedInnerHtml
|
|
662
|
+
render() {
|
|
663
|
+
return `
|
|
664
|
+
<template id="router" is="swc-choose">
|
|
665
|
+
<!-- Home Page -->
|
|
666
|
+
<template is="swc-when" value="{{ ['', '/'].includes($value?.path) }}">
|
|
667
|
+
<home-page/>
|
|
668
|
+
</template>
|
|
669
|
+
|
|
670
|
+
<!-- Users List -->
|
|
671
|
+
<template is="swc-when" value="{{ $value?.path === '/users' }}">
|
|
672
|
+
<users-list-page/>
|
|
673
|
+
</template>
|
|
674
|
+
|
|
675
|
+
<!-- User Detail -->
|
|
676
|
+
<template is="swc-when" value="{{ $value?.path.startsWith('/users/') }}">
|
|
677
|
+
<user-detail-page user-id="{{$value?.pathData?.id}}"/>
|
|
678
|
+
</template>
|
|
679
|
+
|
|
680
|
+
<!-- Not Found -->
|
|
681
|
+
<template is="swc-otherwise">
|
|
682
|
+
<not-found-page/>
|
|
683
|
+
</template>
|
|
684
|
+
</template>
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return tagName;
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
export default rootRouterFactory;
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
#### Step 4. Page Components
|
|
696
|
+
```typescript
|
|
697
|
+
// pages/HomePage.ts
|
|
698
|
+
import { elementDefine, onConnectedInnerHtml } from '@dooboostore/simple-web-component';
|
|
699
|
+
|
|
700
|
+
const HomePage = (w: Window) => {
|
|
701
|
+
const tagName = 'home-page';
|
|
702
|
+
const existing = w.customElements.get(tagName);
|
|
703
|
+
if (existing) return tagName;
|
|
704
|
+
|
|
705
|
+
@elementDefine(tagName, { window: w })
|
|
706
|
+
class HomePage extends w.HTMLElement {
|
|
707
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
708
|
+
render() {
|
|
709
|
+
return `
|
|
710
|
+
<style>
|
|
711
|
+
:host { display: block; padding: 20px; }
|
|
712
|
+
h1 { color: #333; }
|
|
713
|
+
a { color: #0066cc; text-decoration: none; margin-right: 20px; }
|
|
714
|
+
</style>
|
|
715
|
+
<h1>Welcome to My SPA</h1>
|
|
716
|
+
<nav>
|
|
717
|
+
<a href="/users">View All Users</a>
|
|
718
|
+
</nav>
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return tagName;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
export default HomePage;
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
// pages/UsersListPage.ts
|
|
731
|
+
import { elementDefine, onConnectedInnerHtml, onConnectedSwcApp } from '@dooboostore/simple-web-component';
|
|
732
|
+
import { Inject } from '@dooboostore/simple-boot';
|
|
733
|
+
import { UserService } from '../services/UserService';
|
|
734
|
+
|
|
735
|
+
const UsersListPage = (w: Window) => {
|
|
736
|
+
const tagName = 'users-list-page';
|
|
737
|
+
const existing = w.customElements.get(tagName);
|
|
738
|
+
if (existing) return tagName;
|
|
739
|
+
|
|
740
|
+
@elementDefine(tagName, { window: w })
|
|
741
|
+
class UsersListPage extends w.HTMLElement {
|
|
742
|
+
private users: any[] = [];
|
|
743
|
+
|
|
744
|
+
@onConnectedSwcApp
|
|
745
|
+
onconstructor(@Inject({ symbol: UserService.SYMBOL }) userService: UserService) {
|
|
746
|
+
this.users = userService.getAllUsers();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
750
|
+
render() {
|
|
751
|
+
const usersList = this.users
|
|
752
|
+
.map(u => `<li><a href="/users/${u.id}">${u.name}</a></li>`)
|
|
753
|
+
.join('');
|
|
754
|
+
|
|
755
|
+
return `
|
|
756
|
+
<style>
|
|
757
|
+
:host { display: block; padding: 20px; }
|
|
758
|
+
h1 { color: #333; }
|
|
759
|
+
ul { list-style: none; padding: 0; }
|
|
760
|
+
li { margin: 10px 0; }
|
|
761
|
+
a { color: #0066cc; text-decoration: none; }
|
|
762
|
+
</style>
|
|
763
|
+
<h1>Users</h1>
|
|
764
|
+
<ul>${usersList}</ul>
|
|
765
|
+
<a href="/">Back to Home</a>
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return tagName;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
export default UsersListPage;
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
#### Step 4-1. Reusable Components
|
|
777
|
+
```typescript
|
|
778
|
+
// components/UserCard.ts
|
|
779
|
+
import { elementDefine, onConnectedInnerHtml, attributeHost } from '@dooboostore/simple-web-component';
|
|
780
|
+
|
|
781
|
+
const UserCard = (w: Window) => {
|
|
782
|
+
const tagName = 'user-card';
|
|
783
|
+
const existing = w.customElements.get(tagName);
|
|
784
|
+
if (existing) return tagName;
|
|
785
|
+
|
|
786
|
+
@elementDefine(tagName, { window: w })
|
|
787
|
+
class UserCard extends w.HTMLElement {
|
|
788
|
+
@attributeHost('data-username')
|
|
789
|
+
username: string;
|
|
790
|
+
|
|
791
|
+
@attributeHost('data-email')
|
|
792
|
+
email: string;
|
|
793
|
+
|
|
794
|
+
@onConnectedInnerHtml({ useShadow: true })
|
|
795
|
+
render() {
|
|
796
|
+
return `
|
|
797
|
+
<style>
|
|
798
|
+
:host { display: block; }
|
|
799
|
+
.card { border: 1px solid #ddd; border-radius: 4px; padding: 12px; }
|
|
800
|
+
.name { font-weight: bold; font-size: 14px; }
|
|
801
|
+
.email { color: #666; font-size: 12px; }
|
|
802
|
+
</style>
|
|
803
|
+
<div class="card">
|
|
804
|
+
<div class="name">${this.username}</div>
|
|
805
|
+
<div class="email">${this.email}</div>
|
|
806
|
+
</div>
|
|
807
|
+
`;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return tagName;
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
export default UserCard;
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
#### Step 5. Export Factories
|
|
818
|
+
|
|
819
|
+
**services/index.ts** - Export all service factories
|
|
820
|
+
```typescript
|
|
821
|
+
// services/index.ts
|
|
822
|
+
import userServiceFactory from './UserService';
|
|
823
|
+
|
|
824
|
+
export const serviceFactories = [userServiceFactory];
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**components/index.ts** - Export all component factories
|
|
828
|
+
```typescript
|
|
829
|
+
// components/index.ts
|
|
830
|
+
import UserCard from './UserCard';
|
|
831
|
+
|
|
832
|
+
export const componentFactories = [
|
|
833
|
+
UserCard,
|
|
834
|
+
];
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**pages/index.ts** - Export all page factories with router
|
|
838
|
+
```typescript
|
|
839
|
+
// pages/index.ts
|
|
840
|
+
import rootRouterFactory from './RootRouter';
|
|
841
|
+
import HomePage from './HomePage';
|
|
842
|
+
import UsersListPage from './UsersListPage';
|
|
843
|
+
import UserDetailPage from './UserDetailPage';
|
|
844
|
+
|
|
845
|
+
export const pageFactories = [
|
|
846
|
+
rootRouterFactory, // Router first
|
|
847
|
+
HomePage,
|
|
848
|
+
UsersListPage,
|
|
849
|
+
UserDetailPage
|
|
850
|
+
];
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
#### Step 6. Application Bootstrap
|
|
854
|
+
```typescript
|
|
855
|
+
// bootFactory.ts
|
|
856
|
+
import { register } from '@dooboostore/simple-web-component';
|
|
857
|
+
import { serviceFactories } from './services';
|
|
858
|
+
import { componentFactories } from './components';
|
|
859
|
+
import { pageFactories } from './pages';
|
|
860
|
+
|
|
861
|
+
export default (w: Window, container: symbol) => {
|
|
862
|
+
// Initialize services with container (dependency injection)
|
|
863
|
+
// Each factory receives the container symbol and registers its service in the DI container
|
|
864
|
+
serviceFactories.forEach(serviceFactory => serviceFactory(container));
|
|
865
|
+
|
|
866
|
+
// Register pages and components
|
|
867
|
+
register(w, [...pageFactories, ...componentFactories]);
|
|
868
|
+
};
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### Step 7. Application Entry Point
|
|
872
|
+
```typescript
|
|
873
|
+
// index.ts
|
|
874
|
+
import 'reflect-metadata';
|
|
875
|
+
import { SwcAppInterface } from '@dooboostore/simple-web-component';
|
|
876
|
+
import bootFactory from './bootFactory';
|
|
877
|
+
|
|
878
|
+
const w = window;
|
|
879
|
+
|
|
880
|
+
w.document.addEventListener('DOMContentLoaded', async () => {
|
|
881
|
+
const container = Symbol('app');
|
|
882
|
+
|
|
883
|
+
// Initialize services and components
|
|
884
|
+
bootFactory(w, container);
|
|
885
|
+
|
|
886
|
+
// Wait for customized built-in element to be defined
|
|
887
|
+
await w.customElements.whenDefined('swc-app-body');
|
|
888
|
+
|
|
889
|
+
// Get app root element
|
|
890
|
+
const appElement = w.document.querySelector('#app') as SwcAppInterface;
|
|
891
|
+
|
|
892
|
+
if (appElement && typeof appElement.connect === 'function') {
|
|
893
|
+
// Connect SPA with routing and DI
|
|
894
|
+
appElement.connect({
|
|
895
|
+
path: '/',
|
|
896
|
+
routeType: 'path',
|
|
897
|
+
container: container,
|
|
898
|
+
window: w,
|
|
899
|
+
onEngineStarted: () => {
|
|
900
|
+
console.log('🚀 Application started successfully');
|
|
901
|
+
// Root router automatically initialized via @elementDefine
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
} else {
|
|
905
|
+
console.error('Failed to initialize SPA');
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
#### Resulting Application Structure
|
|
911
|
+
```
|
|
912
|
+
src/
|
|
913
|
+
├── index.ts # Entry point
|
|
914
|
+
├── index.html # HTML root with <root-router>
|
|
915
|
+
├── bootFactory.ts # Bootstrap services & components via register()
|
|
916
|
+
├── services/
|
|
917
|
+
│ ├── UserService.ts # Service factory (export default)
|
|
918
|
+
│ └── index.ts # Export serviceFactories
|
|
919
|
+
├── pages/
|
|
920
|
+
│ ├── RootRouter.ts # Router factory (export default)
|
|
921
|
+
│ ├── HomePage.ts # Page factory (export default)
|
|
922
|
+
│ ├── UsersListPage.ts # Page factory (export default)
|
|
923
|
+
│ ├── UserDetailPage.ts # Page factory (export default)
|
|
924
|
+
│ └── index.ts # Export pageFactories
|
|
925
|
+
└── components/
|
|
926
|
+
├── UserCard.ts # Component factory (export default)
|
|
927
|
+
└── index.ts # Export componentFactories
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
**Key Flow:**
|
|
931
|
+
1. HTML loads → `<root-router>` element already in DOM
|
|
932
|
+
2. `index.ts` executes → calls `bootFactory`
|
|
933
|
+
3. `bootFactory` registers services and components (including `rootRouterFactory`)
|
|
934
|
+
4. App waits for `swc-app-body` to be defined
|
|
935
|
+
5. `appElement.connect()` initializes routing & DI
|
|
936
|
+
6. `<root-router>` becomes active with `@subscribeSwcAppRouteChange` + `swc-choose` template
|
|
937
|
+
7. Route changes automatically trigger `swc-choose` re-evaluation and template re-rendering
|
|
938
|
+
8. Template attributes like `user-id="{{$value?.pathData?.id}}"` are dynamically populated with route params
|
|
939
|
+
9. All components have access to injected services via `@onInitialize` or `@onConnectedSwcApp`
|
|
940
|
+
|
|
941
|
+
|
|
317
942
|
---
|
|
318
943
|
|
|
319
944
|
## ⚠️ Critical Rules
|
|
@@ -323,14 +948,14 @@ appElement.connect({
|
|
|
323
948
|
|
|
324
949
|
```typescript
|
|
325
950
|
// ✅ CORRECT
|
|
326
|
-
@Sim
|
|
951
|
+
@Sim
|
|
327
952
|
export class UserService { }
|
|
328
953
|
|
|
329
954
|
@elementDefine(tagName, { window: w })
|
|
330
955
|
class UserWidget extends w.HTMLElement { }
|
|
331
956
|
|
|
332
957
|
// ❌ WRONG
|
|
333
|
-
@Sim
|
|
958
|
+
@Sim
|
|
334
959
|
@elementDefine(tagName, { window: w })
|
|
335
960
|
class UserWidget extends w.HTMLElement { }
|
|
336
961
|
```
|
|
@@ -353,8 +978,7 @@ export default (w: Window) => {
|
|
|
353
978
|
```typescript
|
|
354
979
|
// ✅ CORRECT
|
|
355
980
|
@onInitialize
|
|
356
|
-
|
|
357
|
-
super();
|
|
981
|
+
onconstructor(@Inject({ symbol: Service.SYMBOL }) service: Service) {
|
|
358
982
|
this.service = service;
|
|
359
983
|
}
|
|
360
984
|
|