@dooboostore/simple-web-component 1.0.9 → 1.0.10

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 CHANGED
@@ -1,124 +1,1007 @@
1
- # ✨ @dooboostore/simple-web-component
1
+ # @dooboostore/simple-web-component
2
2
 
3
- > **Modern Reactivity meets Native Web Standards.**
4
- > Build high-performance UIs with pinpoint precision, leveraging the power of Web Components without the Virtual DOM overhead.
3
+ A lightweight UI library built on top of the native Web Components API with a **TypeScript decorator-based reactive system**.
4
+ Updates only the specific text nodes and attributes bound to data — no Virtual DOM, no diffing.
5
5
 
6
6
  ---
7
7
 
8
- ## 🌈 Why SWC?
8
+ ## Installation
9
9
 
10
- In a world of heavy frameworks, **SWC** (Simple Web Component) stands out by being invisible. It doesn't fight the browser; it enhances it.
10
+ ```bash
11
+ pnpm add @dooboostore/simple-web-component reflect-metadata
12
+ ```
13
+
14
+ Add this at the very top of your entry point:
15
+
16
+ ```typescript
17
+ import 'reflect-metadata';
18
+ ```
19
+
20
+ > **Safari Support** — The `is` attribute requires a polyfill:
21
+ > ```bash
22
+ > pnpm add @ungap/custom-elements
23
+ > ```
24
+ > ```typescript
25
+ > import '@ungap/custom-elements';
26
+ > import 'reflect-metadata';
27
+ > ```
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { elementDefine, innerHtml, state, addEventListener } from '@dooboostore/simple-web-component';
35
+
36
+ @elementDefine('my-counter')
37
+ class MyCounter extends HTMLElement {
38
+ @state count = 0;
39
+
40
+ @innerHtml({ useShadow: true })
41
+ render() {
42
+ return `<button id="btn">Clicked: {{count}} times</button>`;
43
+ }
44
+
45
+ @addEventListener({ type: 'click', query: '#btn' })
46
+ onClick() {
47
+ this.count++;
48
+ }
49
+ }
50
+ ```
51
+
52
+ ```html
53
+ <my-counter></my-counter>
54
+ ```
55
+
56
+ ---
11
57
 
12
- ### 🎯 Surgical Updates (No VDOM!)
13
- Forget full-tree diffing. SWC uses a **Proxy-based reactivity engine** that maps data changes directly to specific **Text Nodes** or **Attributes**. When a property changes, only that tiny piece of the DOM updates. It's not just fast; it's surgically precise.
58
+ ## Decorator API
14
59
 
15
- ### 🧩 Pure Web Standards
16
- SWC is built on **Customized Built-in Elements**. Use the native `is` attribute to supercharge standard tags like `<ul>`, `<table>`, or `<template>`. Your HTML remains semantic, accessible, and SEO-friendly.
60
+ ### `@elementDefine(config)`
17
61
 
18
- ### ⚡ Zero-JS Logic (Declarative Attributes)
19
- Write less code. Handle complex events and lifecycles directly in your HTML. SWC parses your attributes and turns them into high-performance execution blocks.
62
+ Registers a Custom Element.
63
+
64
+ ```typescript
65
+ // Tag name only
66
+ @elementDefine('my-component')
67
+ class MyComponent extends HTMLElement {}
68
+
69
+ // With options
70
+ @elementDefine({
71
+ name: 'my-component',
72
+ extends: 'div', // Extend a built-in tag (used with `is` attribute)
73
+ observedAttributes: ['data-id'], // Additional observed attributes
74
+ autoRemoveEventListeners: true, // Auto-remove listeners on disconnect (default: true)
75
+ window: w, // Custom window injection (SSR, multi-window)
76
+ })
77
+ class MyComponent extends HTMLElement {}
78
+ ```
20
79
 
21
80
  ---
22
81
 
23
- ## 🚀 The "Sexy" SPA Pattern
82
+ ### `@innerHtml(options?)`
24
83
 
25
- SWC redefines routing. By combining `replaceChildren` with Shadow DOM `<slot>`, you get instant, flicker-free transitions while keeping your layout logic perfectly encapsulated.
84
+ Defines the component template. Use `{{varName}}` syntax for data binding.
26
85
 
27
86
  ```typescript
28
- @elementDefine('app-router')
29
- class AppRouter extends HTMLElement {
30
- async canActivate(url: RoutingDataSet, data?: any) {
31
- // Only swap if the page actually changes
32
- if (data instanceof Node && !this.contains(data)) {
33
- this.replaceChildren(data); // 👈 Atomic, high-performance swap
34
- }
87
+ @elementDefine('my-card')
88
+ class MyCard extends HTMLElement {
89
+ @state title = 'Hello';
90
+ @state count = 0;
91
+
92
+ // Light DOM (default)
93
+ @innerHtml
94
+ render() {
95
+ return `<h1>{{title}}</h1><p>Count: {{count}}</p>`;
35
96
  }
97
+ }
98
+ ```
99
+
100
+ ```typescript
101
+ // Shadow DOM
102
+ @innerHtml({ useShadow: true })
103
+ render() {
104
+ return `
105
+ <style>h1 { color: blue; }</style>
106
+ <h1>{{title}}</h1>
107
+ `;
108
+ }
109
+ ```
110
+
111
+ ```typescript
112
+ // Shadow DOM + Light DOM simultaneously
113
+ @innerHtml({ useShadow: true })
114
+ shadowContent() {
115
+ return `<style>:host { display: block; }</style><slot></slot>`;
116
+ }
117
+
118
+ @innerHtml
119
+ lightContent() {
120
+ return `<p>Light DOM: {{name}}</p>`;
121
+ }
122
+ ```
123
+
124
+ Async templates are supported:
125
+
126
+ ```typescript
127
+ @innerHtml
128
+ async render() {
129
+ const data = await fetch('/api/data').then(r => r.json());
130
+ return `<p>${data.message}</p>`;
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ### `@state(alias?)`
137
+
138
+ Declares a reactive property. When the value changes, only the bound DOM nodes are surgically updated.
139
+
140
+ ```typescript
141
+ @elementDefine('user-card')
142
+ class UserCard extends HTMLElement {
143
+ @state name = 'Kim';
144
+ @state age = 30;
145
+ @state('userName') displayName = 'Kim'; // Access as {{userName}} in template
146
+
147
+ @innerHtml
148
+ render() {
149
+ return `
150
+ <p>Name: {{name}}</p>
151
+ <p>Age: {{age}}</p>
152
+ <p>Display: {{userName}}</p>
153
+ `;
154
+ }
155
+ }
156
+ ```
157
+
158
+ Objects and arrays are also reactive:
159
+
160
+ ```typescript
161
+ @state user = { name: 'Kim', age: 30 };
162
+ @state items = ['a', 'b', 'c'];
163
+
164
+ // Nested property changes are detected
165
+ this.user.name = 'Lee'; // DOM auto-updates
166
+ this.items.push('d'); // DOM auto-updates
167
+ ```
168
+
169
+ ---
170
+
171
+ ### `@attribute(options?)`
172
+
173
+ Syncs an HTML attribute with a JS property bidirectionally.
174
+
175
+ ```typescript
176
+ @elementDefine('my-input')
177
+ class MyInput extends HTMLElement {
178
+ @attribute() label = ''; // attribute name: label
179
+ @attribute('data-count') count = 0; // attribute name: data-count
180
+ @attribute({ type: Number }) size = 10; // type conversion
181
+ @attribute({ type: Boolean }) disabled = false;
182
+ @attribute({ type: Object }) config = {};
183
+ @attribute({ disableReflect: true }) secret = ''; // JS→HTML reflection disabled
184
+
185
+ @innerHtml
186
+ render() {
187
+ return `<label>{{label}}: {{count}}</label>`;
188
+ }
189
+ }
190
+ ```
191
+
192
+ ```html
193
+ <my-input label="Name" data-count="5" disabled></my-input>
194
+ ```
195
+
196
+ Combine with `@state` for reactive attribute handling:
197
+
198
+ ```typescript
199
+ @state @attribute({ type: Number }) count = 0;
200
+ ```
201
+
202
+ ---
203
+
204
+ ### `@query(selector?)` / `@queryAll(selector?)`
205
+
206
+ Lazily binds DOM elements.
207
+
208
+ ```typescript
209
+ @elementDefine('form-component')
210
+ class FormComponent extends HTMLElement {
211
+ // Property getter
212
+ @query('input[name="email"]') emailInput!: HTMLInputElement;
213
+ @queryAll('li') listItems!: NodeListOf<HTMLLIElement>;
214
+
215
+ // Method — called once after connectedCallback
216
+ @query('input')
217
+ onInputReady(el: HTMLInputElement) {
218
+ el.focus();
219
+ }
220
+
221
+ // Search in Shadow DOM
222
+ @query({ selector: 'p', root: 'shadow' }) shadowP!: HTMLElement;
223
+
224
+ // Search in Light DOM
225
+ @query({ selector: 'slot', root: 'light' }) slotEl!: HTMLSlotElement;
226
+
227
+ @innerHtml({ useShadow: true })
228
+ render() {
229
+ return `<input name="email" type="email" /><p>Status</p>`;
230
+ }
231
+ }
232
+ ```
233
+
234
+ ---
235
+
236
+ ### `@addEventListener(options)`
237
+
238
+ Declaratively registers event listeners. Auto-removed on disconnect when `autoRemoveEventListeners: true` (default).
239
+
240
+ ```typescript
241
+ @elementDefine('event-demo')
242
+ class EventDemo extends HTMLElement {
243
+ @state value = '';
36
244
 
37
245
  @innerHtml({ useShadow: true })
38
246
  render() {
39
247
  return `
40
- <header>My Sexy App</header>
41
- <main><slot></slot></main> <!-- 👈 Routed pages dock here automatically -->
248
+ <input id="input" type="text" />
249
+ <button id="submit-btn">Submit</button>
42
250
  `;
43
251
  }
252
+
253
+ @addEventListener({ type: 'input', query: '#input' })
254
+ onInput(event: InputEvent) {
255
+ this.value = (event.target as HTMLInputElement).value;
256
+ }
257
+
258
+ @addEventListener({
259
+ type: 'click',
260
+ query: '#submit-btn',
261
+ stopPropagation: true,
262
+ preventDefault: true,
263
+ })
264
+ onSubmit() {
265
+ console.log('Submitted:', this.value);
266
+ }
267
+
268
+ // Host element event (no query)
269
+ @addEventListener({ type: 'click' })
270
+ onHostClick(event: MouseEvent) {
271
+ console.log('Component clicked');
272
+ }
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ### `@emitCustomEvent(options)`
279
+
280
+ Automatically dispatches a `CustomEvent` with the method's return value as `detail`.
281
+
282
+ ```typescript
283
+ @elementDefine('data-emitter')
284
+ class DataEmitter extends HTMLElement {
285
+ @emitCustomEvent('data-change')
286
+ updateData() {
287
+ return { value: 42, timestamp: Date.now() };
288
+ // → CustomEvent('data-change', { detail: { value: 42, ... } })
289
+ }
290
+
291
+ @emitCustomEvent({ type: 'user-select', bubbles: true, composed: true })
292
+ selectUser(id: number) {
293
+ return { id, name: 'Kim' };
294
+ }
295
+ }
296
+ ```
297
+
298
+ ```typescript
299
+ // Listen from parent
300
+ document.querySelector('data-emitter')?.addEventListener('data-change', (e: CustomEvent) => {
301
+ console.log(e.detail); // { value: 42, timestamp: ... }
302
+ });
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Lifecycle Hooks
308
+
309
+ ### Decorator Hooks
310
+
311
+ ```typescript
312
+ @elementDefine('lifecycle-demo')
313
+ class LifecycleDemo extends HTMLElement {
314
+ @onBeforeConnected
315
+ beforeConnect() { console.log('Before template injection'); }
316
+
317
+ @onAfterConnected // alias: @onConnected
318
+ afterConnect() { console.log('After template injection and binding'); }
319
+
320
+ @onBeforeDisconnected
321
+ beforeDisconnect() { console.log('Before removal from DOM'); }
322
+
323
+ @onAfterDisconnected // alias: @onDisconnected
324
+ afterDisconnect() { console.log('After removal and cleanup'); }
325
+
326
+ // Single attribute
327
+ @onAttributeChanged('name')
328
+ onNameChange(newVal: string, oldVal: string, attrName: string) {
329
+ console.log(`name: ${oldVal} → ${newVal}`);
330
+ }
331
+
332
+ // MutationSubscriber
333
+ @onMutationSubscribe({root: 'all'})
334
+ onMutationSubscribe(mutations: MutationRecord[], observer: MutationObserver, root: 'light' | 'shadow') {
335
+ console.log('mutations', mutations, observer, root);
336
+ }
337
+
338
+ // Wildcard: any attribute change
339
+ @onAttributeChanged
340
+ onAnyChange(newVal: string, oldVal: string, attrName: string) {
341
+ console.log(`${attrName} changed`);
342
+ }
343
+
344
+ // Multiple attributes
345
+ @onAttributeChanged(['name', 'age'])
346
+ onNameOrAgeChange(newVal: string, oldVal: string, attrName: string) {
347
+ console.log(`${attrName} changed`);
348
+ }
349
+
350
+ @onAddEventListener
351
+ onListenerAdded(el: Element, type: string, handler: EventListener) {
352
+ console.log(`Event registered: ${type}`);
353
+ }
44
354
  }
45
355
  ```
46
356
 
357
+ ### HTML Attribute Hooks (Zero-JS)
358
+
359
+ Execute scripts directly in HTML without any JS class definition.
360
+
361
+ ```html
362
+ <my-component
363
+ swc-on-constructor="console.log('created', this)"
364
+ swc-on-connected="console.log('connected', $host)"
365
+ swc-on-disconnected="console.log('removed')"
366
+ swc-on-attribute-changed="console.log($name, $old, '→', $new)"
367
+ ></my-component>
368
+ ```
369
+
47
370
  ---
48
371
 
49
- ## ⚡ Hierarchical Power ($host, $hosts)
372
+ ## Template Binding & Expressions
50
373
 
51
- Stop prop-drilling. SWC provides a built-in hierarchical context for all declarative events and lifecycles.
374
+ ```typescript
375
+ @state user = { name: 'Kim', age: 30 };
376
+ @state items = ['apple', 'banana'];
377
+ @state isActive = true;
378
+ @state count = 0;
379
+ ```
380
+
381
+ ```html
382
+ <!-- Simple value -->
383
+ <p>{{name}}</p>
52
384
 
53
- - **`this`**: The element itself.
54
- - **`$host`**: The nearest parent SWC component (the logic owner).
55
- - **`$hosts`**: The full lineage `[Root, ..., Parent]`.
56
- - **`$firstHost`**: The top-most Root component.
385
+ <!-- Object path -->
386
+ <p>{{user.name}} (age: {{user.age}})</p>
387
+
388
+ <!-- Expressions -->
389
+ <p>Item #{{count + 1}}</p>
390
+ <p class="{{isActive ? 'active' : 'inactive'}}">Status</p>
391
+ <p>{{items.length}} items</p>
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Inline Event Handler Variables
397
+
398
+ Available variables in all `swc-on-*` attribute scripts:
399
+
400
+ | Variable | Description |
401
+ |---|---|
402
+ | `this` | The DOM element where the event fired |
403
+ | `event` | The event object |
404
+ | `$data` | `event.detail` (CustomEvent) |
405
+ | `$host` / `$lastHost` | Nearest SWC parent component |
406
+ | `$firstHost` | Top-most SWC ancestor component |
407
+ | `$hosts` | All SWC ancestors array `[root, ..., parent]` |
408
+ | `$q(sel, root?)` | `querySelector` shorthand |
409
+ | `$qa(sel, root?)` | `querySelectorAll` → Array |
410
+ | `$qi(id, root?)` | `getElementById` shorthand |
411
+ | `$d` | `document` |
412
+ | `$w` | `window` |
57
413
 
58
414
  ```html
59
- <!-- Control your parent component directly from a plain button -->
60
- <button swc-on-click="$host.user.active = true; this.style.color='red';">
61
- Activate & Style Me
62
- </button>
415
+ <!-- Call host component method -->
416
+ <button swc-on-click="$host.save();">Save</button>
417
+
418
+ <!-- Access top-most component in nested structure -->
419
+ <button swc-on-click="$firstHost.logout();">Logout</button>
420
+
421
+ <!-- DOM query shorthands -->
422
+ <button swc-on-click="$qi('modal')?.show();">Open Modal</button>
423
+ <button swc-on-click="$q('.panel', $host.shadowRoot)?.classList.toggle('open');">Panel</button>
424
+
425
+ <!-- Event object -->
426
+ <input swc-on-input="$host.name = event.target.value" />
427
+
428
+ <!-- Run once on connect -->
429
+ <div swc-on-connected="console.log('connected', $host, $firstHost)"></div>
63
430
  ```
64
431
 
65
432
  ---
66
433
 
67
- ## 🛠 Reactive Logic Components (`is` Variants)
434
+ ## Built-in Logic Components
435
+
436
+ All standard HTML tags support logic via the `is` attribute.
437
+ Pass data with `swc-value`, item alias with `as`, index alias with `as-index`.
438
+
439
+ ### `swc-for-of` — Loop (removes element when array is empty)
440
+
441
+ ```html
442
+ <!-- Standalone component -->
443
+ <swc-for-of swc-value="{{users}}" as="u">
444
+ <div>{{u.name}}</div>
445
+ </swc-for-of>
446
+
447
+ <!-- is attribute (keeps semantic HTML) -->
448
+ <ul is="swc-for-of-ul" swc-value="{{users}}" as="u" as-index="i">
449
+ <li>{{i + 1}}. {{u.name}} — {{u.active ? 'Active' : 'Inactive'}}</li>
450
+ </ul>
451
+ ```
452
+
453
+ ```typescript
454
+ const list = document.querySelector('ul[is="swc-for-of-ul"]') as SwcForOfUl;
455
+ list.swcValue = [{ name: 'Kim', active: true }, { name: 'Lee', active: false }];
456
+ list.addRow({ name: 'Park', active: true }); // add row
457
+ list.removeRow(0); // remove first row
458
+ ```
68
459
 
69
- SWC supercharges standard HTML tags using the native `is` attribute. This keeps your DOM semantic while adding powerful reactive logic.
460
+ ### `swc-for-of-children` — Loop (keeps wrapper element)
70
461
 
71
- ### 1. Dynamic Lists (`swc-for-of`)
72
- Supports high-performance `addRow` and `removeRow` for pinpoint list management.
73
462
  ```html
74
- <!-- The <ul> remains, <li>s are surgically added/removed -->
75
- <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item">
76
- <li>{{index + 1}}. {{item.title}}</li>
463
+ <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item" as-index="idx">
464
+ <li class="{{item.done ? 'done' : ''}}">
465
+ {{idx + 1}}. {{item.text}}
466
+ <button swc-on-click="$firstHost.removeItem({{idx}});">Delete</button>
467
+ </li>
77
468
  </ul>
78
469
  ```
79
470
 
80
- ### 2. Conditional Rendering (`swc-if`)
81
- Choose between physical removal or content swapping.
82
- - **`swc-if-[tag]`**: Truly removes the element from the DOM (leaving a comment placeholder).
83
- - **`swc-if-children-[tag]`**: Keeps the element (wrapper) but swaps its internal children.
471
+ ### `swc-if` — Conditional rendering (removes from DOM when false)
84
472
 
85
473
  ```html
86
- <div is="swc-if-div" swc-value="{{isAdmin}}">
87
- <p>Sensitive Admin Panel</p>
474
+ <swc-if swc-value="{{isLoggedIn}}">
475
+ <p>Welcome!</p>
476
+ </swc-if>
477
+
478
+ <div is="swc-if-div" swc-value="{{isVisible}}" style="background: yellow;">
479
+ <b>Conditional content</b>
88
480
  </div>
89
481
  ```
90
482
 
91
- ### 3. Powerful Branching (`swc-choose`)
483
+ ```typescript
484
+ const el = document.querySelector('[is="swc-if-div"]') as SwcIfDiv;
485
+ el.swcValue = true; // insert into DOM
486
+ el.swcValue = false; // remove from DOM
487
+ ```
488
+
489
+ ### `swc-if-children` — Conditional rendering (keeps wrapper, swaps children)
490
+
491
+ ```html
492
+ <section is="swc-if-children-section" swc-value="{{showContent}}">
493
+ <p>Shown only when showContent is true.</p>
494
+ </section>
495
+ ```
496
+
497
+ ### `swc-choose` / `swc-when` / `swc-other` — Switch/Case
498
+
92
499
  ```html
500
+ <!-- Removes element if no case matches -->
93
501
  <div is="swc-choose-div" swc-value="{{status}}">
94
- <span is="swc-when-span" test="'loading'">⌛ Loading...</span>
95
- <span is="swc-when-span" test="'success'">✅ Success!</span>
96
- <span is="swc-other-span">❌ Unknown Error</span>
502
+ <span is="swc-when-span" test="'loading'">⏳ Loading...</span>
503
+ <span is="swc-when-span" test="'error'">❌ Error</span>
504
+ <span is="swc-when-span" test="'success'">✅ Done</span>
505
+ <span is="swc-other-span">⚪ Idle</span>
506
+ </div>
507
+ ```
508
+
509
+ ```typescript
510
+ const el = document.querySelector('[is="swc-choose-div"]') as SwcChooseDiv;
511
+ el.swcValue = 'loading'; // shows "⏳ Loading..."
512
+ el.swcValue = 'success'; // shows "✅ Done"
513
+ el.swcValue = 'unknown'; // shows "⚪ Idle" (swc-other)
514
+ ```
515
+
516
+ ### `swc-choose-children` — Switch/Case (keeps wrapper)
517
+
518
+ ```html
519
+ <div is="swc-choose-children-div" swc-value="{{tab}}">
520
+ <div is="swc-when-div" test="'home'">Home tab content</div>
521
+ <div is="swc-when-div" test="'profile'">Profile tab content</div>
522
+ <div is="swc-other-div">Default tab content</div>
523
+ </div>
524
+ ```
525
+
526
+ ### `swc-object` — Object binding
527
+
528
+ ```html
529
+ <div is="swc-object-div" swc-value="{{user}}" as="u">
530
+ <p>Name: {{u.name}}</p>
531
+ <p>Age: {{u.age}}</p>
97
532
  </div>
98
533
  ```
99
534
 
535
+ ```typescript
536
+ const el = document.querySelector('[is="swc-object-div"]') as SwcObjectDiv;
537
+ el.swcValue = { name: 'Kim', age: 30 };
538
+ el.swcValue.name = 'Lee'; // Proxy reactive — DOM auto-updates
539
+ ```
540
+
100
541
  ---
101
542
 
102
- ## 📑 API Quick Start
543
+ ## Hierarchical Context (`$host` System)
544
+
545
+ Access parent components from nested components.
546
+
547
+ ```typescript
548
+ @elementDefine('parent-comp')
549
+ class ParentComp extends HTMLElement {
550
+ @state count = 0;
551
+ @state items = [{ name: 'Item 1' }, { name: 'Item 2' }];
552
+
553
+ removeItem(index: number) {
554
+ this.items.splice(index, 1);
555
+ }
556
+
557
+ @innerHtml({ useShadow: true })
558
+ render() {
559
+ return `
560
+ <p>Count: {{count}}</p>
561
+ <!-- Call parent method from child event -->
562
+ <button swc-on-click="$host.count++;">Increment</button>
563
+
564
+ <!-- In nested for-of: $host = swc-for-of-children-ul, $firstHost = parent-comp -->
565
+ <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item" as-index="i">
566
+ <li>
567
+ {{item.name}}
568
+ <button swc-on-click="$firstHost.removeItem({{i}});">Delete</button>
569
+ </li>
570
+ </ul>
571
+ `;
572
+ }
573
+ }
574
+ ```
575
+
576
+ ### Accessing parent `@state` from nested components
103
577
 
104
- | Decorator | Description |
105
- | :--- | :--- |
106
- | **`@elementDefine`** | Registers your class as a Custom Element. |
107
- | **`@innerHtml`** | Defines markup (supports async/shadow DOM). |
108
- | **`@state`** | Creates reactive properties for surgical updates. |
109
- | **`@attribute`** | Syncs JS properties with HTML attributes automatically. |
110
- | **`@query`** | Lazy-loads elements from Shadow or Light DOM. |
111
- | **`@addEventListener`** | Declarative event binding with auto-cleanup. |
578
+ ```typescript
579
+ @elementDefine('app-root')
580
+ class AppRoot extends HTMLElement {
581
+ @state currentUser = { name: 'Kim', role: 'admin' };
582
+ @state items = [{ id: 1, text: 'Item 1' }, { id: 2, text: 'Item 2' }];
583
+
584
+ @innerHtml({ useShadow: true })
585
+ render() {
586
+ return `
587
+ <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item">
588
+ <li>
589
+ {{item.text}}
590
+ <!-- Access parent's currentUser.name directly in child template -->
591
+ <small>(by: {{currentUser.name}})</small>
592
+ </li>
593
+ </ul>
594
+ `;
595
+ }
596
+ }
597
+ ```
112
598
 
113
599
  ---
114
600
 
115
- ## 📦 Installation
601
+ ## Shadow DOM + Slot Pattern
116
602
 
117
- ```bash
118
- pnpm add @dooboostore/simple-web-component
603
+ ```typescript
604
+ @elementDefine('card-layout')
605
+ class CardLayout extends HTMLElement {
606
+ @state title = '';
607
+
608
+ @innerHtml({ useShadow: true })
609
+ render() {
610
+ return `
611
+ <style>
612
+ :host { display: block; border: 1px solid #ccc; border-radius: 8px; }
613
+ .header { background: #f5f5f5; padding: 12px; font-weight: bold; }
614
+ .body { padding: 16px; }
615
+ </style>
616
+ <div class="header">{{title}}</div>
617
+ <div class="body">
618
+ <slot></slot>
619
+ <slot name="footer"></slot>
620
+ </div>
621
+ `;
622
+ }
623
+ }
119
624
  ```
120
625
 
121
- *Note: Requires `reflect-metadata` and `@ungap/custom-elements` for Safari support.*
626
+ ```html
627
+ <card-layout title="User Info">
628
+ <p>Name: Kim</p>
629
+ <p>Email: kim@example.com</p>
630
+ <div slot="footer">
631
+ <button>Edit</button>
632
+ </div>
633
+ </card-layout>
634
+ ```
635
+
636
+ ---
637
+
638
+ ## File Separation Pattern
639
+
640
+ When splitting a component into its own file, use `export default (w: Window) => {...}` to inject `window`:
641
+
642
+ ```typescript
643
+ // MyComponent.ts
644
+ import { elementDefine, innerHtml, state } from '@dooboostore/simple-web-component';
645
+
646
+ export default (w: Window) => {
647
+ @elementDefine({ name: 'my-component', window: w })
648
+ class MyComponent extends HTMLElement {
649
+ @state name = '';
650
+
651
+ @innerHtml({ useShadow: true })
652
+ render() {
653
+ return `<p>Hello, {{name}}!</p>`;
654
+ }
655
+ }
656
+ return MyComponent;
657
+ };
658
+ ```
659
+
660
+ ```typescript
661
+ // index.ts — inject window at the entry point
662
+ import createMyComponent from './MyComponent';
663
+
664
+ const MyComponent = createMyComponent(window);
665
+ ```
666
+
667
+ > For browser-only SPAs, you can omit the `window` option entirely.
668
+ > `@elementDefine('my-component')` works fine — `buildEnv` falls back to the global `window` automatically.
669
+
670
+ ---
671
+
672
+ ## SPA Routing — Full Working Example
673
+
674
+ This example uses `@dooboostore/simple-boot` for DI and routing.
675
+ Copy and paste the following into a single `index.ts` file.
676
+
677
+ ```typescript
678
+ import 'reflect-metadata';
679
+ import {
680
+ elementDefine, innerHtml, state, attribute,
681
+ query, addEventListener, onAfterConnected, Swc
682
+ } from '@dooboostore/simple-web-component';
683
+ import { SimpleApplication } from '@dooboostore/simple-boot/SimpleApplication';
684
+ import { Sim } from '@dooboostore/simple-boot/decorators/SimDecorator';
685
+ import { Router as WebRouter } from '@dooboostore/core-web/routers/Router';
686
+ import { Router } from '@dooboostore/simple-boot/decorators/route/Router';
687
+ import { RouterAction } from '@dooboostore/simple-boot/route/RouterAction';
688
+ import { RoutingDataSet } from '@dooboostore/simple-boot/route/RouterManager';
689
+
690
+ const w = window;
691
+
692
+ // ─── Page: Home ──────────────────────────────────────────────────────────────
693
+
694
+ @Sim
695
+ @elementDefine({ name: 'page-home', window: w })
696
+ class PageHome extends HTMLElement {
697
+ @state count = 0;
698
+ @state todos: { text: string; done: boolean }[] = [
699
+ { text: 'Learn SWC', done: true },
700
+ { text: 'Build something', done: false },
701
+ ];
702
+
703
+ addTodo() {
704
+ this.todos = [...this.todos, { text: 'New todo ' + Date.now(), done: false }];
705
+ }
706
+
707
+ toggleTodo(index: number) {
708
+ this.todos[index].done = !this.todos[index].done;
709
+ }
710
+
711
+ @innerHtml({ useShadow: true })
712
+ render() {
713
+ return `
714
+ <style>
715
+ :host { display: block; padding: 20px; }
716
+ .done { text-decoration: line-through; color: #999; }
717
+ button { cursor: pointer; margin: 4px; padding: 6px 12px; }
718
+ li { margin: 6px 0; }
719
+ </style>
720
+
721
+ <h2>🏠 Home Page</h2>
722
+
723
+ <section>
724
+ <h3>Counter: {{count}}</h3>
725
+ <button swc-on-click="$host.count++;">+1</button>
726
+ <button swc-on-click="$host.count--;">-1</button>
727
+ <button swc-on-click="$host.count = 0;">Reset</button>
728
+ </section>
729
+
730
+ <hr/>
731
+
732
+ <section>
733
+ <h3>Todo List ({{todos.length}} items)</h3>
734
+ <button id="add-btn">Add Todo</button>
735
+ <ul is="swc-for-of-children-ul" swc-value="{{todos}}" as="todo" as-index="i">
736
+ <li class="{{todo.done ? 'done' : ''}}">
737
+ <input type="checkbox"
738
+ swc-on-change="$firstHost.toggleTodo({{i}});"
739
+ {{todo.done ? 'checked' : ''}}
740
+ />
741
+ {{todo.text}}
742
+ </li>
743
+ </ul>
744
+ </section>
745
+ `;
746
+ }
747
+
748
+ @addEventListener({ type: 'click', query: '#add-btn' })
749
+ onAddTodo() {
750
+ this.addTodo();
751
+ }
752
+
753
+ @onAfterConnected
754
+ init() {
755
+ console.log('PageHome connected');
756
+ }
757
+ }
758
+
759
+ // ─── Page: About ─────────────────────────────────────────────────────────────
760
+
761
+ @Sim
762
+ @elementDefine({ name: 'page-about', window: w })
763
+ class PageAbout extends HTMLElement {
764
+ @state @attribute name = 'World';
765
+
766
+ @innerHtml({ useShadow: true })
767
+ render() {
768
+ return `
769
+ <style>
770
+ :host { display: block; padding: 20px; }
771
+ input { padding: 6px; margin-right: 8px; }
772
+ </style>
773
+
774
+ <h2>ℹ️ About Page</h2>
775
+ <p>Hello, <strong>{{name}}</strong>!</p>
776
+ <input
777
+ type="text"
778
+ placeholder="Enter your name"
779
+ value="{{name}}"
780
+ swc-on-input="$host.name = event.target.value"
781
+ />
782
+
783
+ <hr/>
784
+
785
+ <h3>Status Panel</h3>
786
+ <div is="swc-choose-children-div" swc-value="{{name}}">
787
+ <p is="swc-when-p" test="'World'">👋 Default greeting</p>
788
+ <p is="swc-when-p" test="'Admin'">🔑 Admin mode activated</p>
789
+ <p is="swc-other-p">😊 Hello, {{name}}!</p>
790
+ </div>
791
+ `;
792
+ }
793
+ }
794
+
795
+ // ─── Sub Router ──────────────────────────────────────────────────────────────
796
+
797
+ @Sim
798
+ @Router({
799
+ path: '/app',
800
+ route: {
801
+ '/home': PageHome,
802
+ '/about': PageAbout,
803
+ }
804
+ })
805
+ @elementDefine({ name: 'app-sub-router', window: w })
806
+ class AppSubRouter extends HTMLElement implements RouterAction.CanActivate {
807
+ @query('#outlet') outlet!: HTMLElement;
808
+
809
+ @innerHtml({ useShadow: true })
810
+ render() {
811
+ return `
812
+ <style>
813
+ :host { display: block; }
814
+ nav { background: #f0f0f0; padding: 10px; }
815
+ nav a { margin-right: 12px; cursor: pointer; color: #1a73e8; text-decoration: none; }
816
+ nav a:hover { text-decoration: underline; }
817
+ #outlet { min-height: 200px; }
818
+ </style>
819
+ <nav>
820
+ <a id="nav-home">Home</a>
821
+ <a id="nav-about">About</a>
822
+ </nav>
823
+ <div id="outlet"></div>
824
+ `;
825
+ }
826
+
827
+ async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
828
+ if (data instanceof Node && !this.outlet?.contains(data)) {
829
+ this.outlet?.replaceChildren(data);
830
+ }
831
+ }
832
+
833
+ @addEventListener({ type: 'click', query: '#nav-home', root: 'shadow' })
834
+ goHome() { (this as any)._sim?.routing('/app/home'); }
835
+
836
+ @addEventListener({ type: 'click', query: '#nav-about', root: 'shadow' })
837
+ goAbout() { (this as any)._sim?.routing('/app/about'); }
838
+ }
839
+
840
+ // ─── Root Router ─────────────────────────────────────────────────────────────
841
+
842
+ @Sim
843
+ @Router({ routers: [AppSubRouter] })
844
+ @elementDefine({ name: 'root-router', window: w })
845
+ class RootRouter extends HTMLElement implements RouterAction.CanActivate {
846
+ @state currentPath = '/';
847
+
848
+ constructor(
849
+ private router: WebRouter,
850
+ private sim: SimpleApplication
851
+ ) {
852
+ super();
853
+ }
854
+
855
+ async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
856
+ this.currentPath = url.intent.pathname;
857
+ if (data instanceof Node && !this.contains(data)) {
858
+ this.replaceChildren(data);
859
+ }
860
+ }
861
+
862
+ @innerHtml({ useShadow: true })
863
+ render() {
864
+ return `
865
+ <style>
866
+ :host { display: block; font-family: sans-serif; max-width: 800px; margin: 0 auto; }
867
+ header {
868
+ background: #1a73e8; color: white;
869
+ padding: 16px 20px; display: flex;
870
+ align-items: center; justify-content: space-between;
871
+ }
872
+ header h1 { margin: 0; font-size: 1.2rem; }
873
+ .path { font-size: 0.85rem; opacity: 0.8; }
874
+ main { padding: 0; }
875
+ </style>
876
+ <header>
877
+ <h1>SWC SPA Demo</h1>
878
+ <span class="path">{{currentPath}}</span>
879
+ </header>
880
+ <main><slot></slot></main>
881
+ `;
882
+ }
883
+ }
884
+
885
+ // ─── Bootstrap ───────────────────────────────────────────────────────────────
886
+
887
+ const swc = new Swc({ rootRouter: RootRouter, window: w });
888
+
889
+ window.addEventListener('DOMContentLoaded', async () => {
890
+ document.body.appendChild(swc.run());
891
+ await swc.routing('/app/home');
892
+ });
893
+ ```
894
+
895
+ ```html
896
+ <!-- index.html -->
897
+ <!DOCTYPE html>
898
+ <html lang="en">
899
+ <head>
900
+ <meta charset="UTF-8" />
901
+ <title>SWC SPA Demo</title>
902
+ </head>
903
+ <body>
904
+ <!-- root-router is appended by swc.run() -->
905
+ </body>
906
+ </html>
907
+ ```
908
+
909
+ ---
910
+
911
+ ## Complete Component Example
912
+
913
+ ```typescript
914
+ import 'reflect-metadata';
915
+ import {
916
+ elementDefine, innerHtml, state, attribute,
917
+ query, addEventListener, onAfterConnected,
918
+ emitCustomEvent, onAttributeChanged
919
+ } from '@dooboostore/simple-web-component';
920
+
921
+ @elementDefine({ name: 'user-profile', autoRemoveEventListeners: true })
922
+ class UserProfile extends HTMLElement {
923
+ @state @attribute name = 'Guest';
924
+ @state @attribute({ type: Number }) age = 0;
925
+ @state isEditing = false;
926
+ @state history: string[] = [];
927
+
928
+ @query('input[name="name"]') nameInput!: HTMLInputElement;
929
+
930
+ @emitCustomEvent({ type: 'profile-save', bubbles: true })
931
+ save() {
932
+ return { name: this.name, age: this.age };
933
+ }
934
+
935
+ @innerHtml({ useShadow: true })
936
+ render() {
937
+ return `
938
+ <style>
939
+ :host { display: block; font-family: sans-serif; }
940
+ .card { padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
941
+ button { cursor: pointer; margin: 4px; padding: 6px 12px; }
942
+ input { padding: 6px; margin: 4px; }
943
+ </style>
944
+
945
+ <div class="card">
946
+ <h2>{{name}} (age: {{age}})</h2>
947
+ <button id="edit-btn">Edit</button>
948
+ </div>
949
+
950
+ <div is="swc-if-children-div" swc-value="{{isEditing}}">
951
+ <div class="card">
952
+ <input name="name" type="text" value="{{name}}" placeholder="Name" />
953
+ <input name="age" type="number" value="{{age}}" placeholder="Age" />
954
+ <button id="save-btn">Save</button>
955
+ <button id="cancel-btn">Cancel</button>
956
+ </div>
957
+ </div>
958
+
959
+ <h4>Change History ({{history.length}})</h4>
960
+ <ul is="swc-for-of-children-ul" swc-value="{{history}}" as="log">
961
+ <li>{{log}}</li>
962
+ </ul>
963
+ `;
964
+ }
965
+
966
+ @addEventListener({ type: 'click', query: '#edit-btn' })
967
+ onEdit() { this.isEditing = true; }
968
+
969
+ @addEventListener({ type: 'click', query: '#cancel-btn' })
970
+ onCancel() { this.isEditing = false; }
971
+
972
+ @addEventListener({ type: 'click', query: '#save-btn' })
973
+ onSave() {
974
+ this.name = this.nameInput?.value ?? this.name;
975
+ this.isEditing = false;
976
+ this.save(); // dispatches CustomEvent automatically
977
+ }
978
+
979
+ @onAttributeChanged('name')
980
+ onNameChange(newVal: string, oldVal: string) {
981
+ if (oldVal && newVal !== oldVal) {
982
+ this.history = [...this.history, `${oldVal} → ${newVal}`];
983
+ }
984
+ }
985
+
986
+ @onAfterConnected
987
+ init() {
988
+ console.log('UserProfile ready');
989
+ }
990
+ }
991
+ ```
992
+
993
+ ```html
994
+ <user-profile name="Kim" age="30"></user-profile>
995
+
996
+ <script>
997
+ document.querySelector('user-profile').addEventListener('profile-save', (e) => {
998
+ console.log('Saved:', e.detail); // { name: '...', age: ... }
999
+ });
1000
+ </script>
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ## License
122
1006
 
123
- ## 📜 License
124
1007
  MIT