# @dooboostore/simple-web-component

A lightweight UI library built on top of the native Web Components API with a **TypeScript decorator-based reactive system**.  
Updates only the specific text nodes and attributes bound to data — no Virtual DOM, no diffing.

---

## Installation

```bash
pnpm add @dooboostore/simple-web-component reflect-metadata
```

Add this at the very top of your entry point:

```typescript
import 'reflect-metadata';
```

> **Safari Support** — The `is` attribute requires a polyfill:
> ```bash
> pnpm add @ungap/custom-elements
> ```
> ```typescript
> import '@ungap/custom-elements';
> import 'reflect-metadata';
> ```

---

## Quick Start

```typescript
import { elementDefine, innerHtml, state, addEventListener } from '@dooboostore/simple-web-component';

@elementDefine('my-counter')
class MyCounter extends HTMLElement {
  @state count = 0;

  @innerHtml({ useShadow: true })
  render() {
    return `<button id="btn">Clicked: {{count}} times</button>`;
  }

  @addEventListener({ type: 'click', query: '#btn' })
  onClick() {
    this.count++;
  }
}
```

```html
<my-counter></my-counter>
```

---

## Decorator API

### `@elementDefine(config)`

Registers a Custom Element.

```typescript
// Tag name only
@elementDefine('my-component')
class MyComponent extends HTMLElement {}

// With options
@elementDefine({
  name: 'my-component',
  extends: 'div',                  // Extend a built-in tag (used with `is` attribute)
  observedAttributes: ['data-id'], // Additional observed attributes
  autoRemoveEventListeners: true,  // Auto-remove listeners on disconnect (default: true)
  window: w,                       // Custom window injection (SSR, multi-window)
})
class MyComponent extends HTMLElement {}
```

---

### `@innerHtml(options?)`

Defines the component template. Use `{{varName}}` syntax for data binding.

```typescript
@elementDefine('my-card')
class MyCard extends HTMLElement {
  @state title = 'Hello';
  @state count = 0;

  // Light DOM (default)
  @innerHtml
  render() {
    return `<h1>{{title}}</h1><p>Count: {{count}}</p>`;
  }
}
```

```typescript
// Shadow DOM
@innerHtml({ useShadow: true })
render() {
  return `
    <style>h1 { color: blue; }</style>
    <h1>{{title}}</h1>
  `;
}
```

```typescript
// Shadow DOM + Light DOM simultaneously
@innerHtml({ useShadow: true })
shadowContent() {
  return `<style>:host { display: block; }</style><slot></slot>`;
}

@innerHtml
lightContent() {
  return `<p>Light DOM: {{name}}</p>`;
}
```

Async templates are supported:

```typescript
@innerHtml
async render() {
  const data = await fetch('/api/data').then(r => r.json());
  return `<p>${data.message}</p>`;
}
```

---

### `@state(alias?)`

Declares a reactive property. When the value changes, only the bound DOM nodes are surgically updated.

```typescript
@elementDefine('user-card')
class UserCard extends HTMLElement {
  @state name = 'Kim';
  @state age = 30;
  @state('userName') displayName = 'Kim'; // Access as {{userName}} in template

  @innerHtml
  render() {
    return `
      <p>Name: {{name}}</p>
      <p>Age: {{age}}</p>
      <p>Display: {{userName}}</p>
    `;
  }
}
```

Objects and arrays are also reactive:

```typescript
@state user = { name: 'Kim', age: 30 };
@state items = ['a', 'b', 'c'];

// Nested property changes are detected
this.user.name = 'Lee';   // DOM auto-updates
this.items.push('d');     // DOM auto-updates
```

---

### `@attribute(options?)`

Syncs an HTML attribute with a JS property bidirectionally.

```typescript
@elementDefine('my-input')
class MyInput extends HTMLElement {
  @attribute() label = '';                     // attribute name: label
  @attribute('data-count') count = 0;          // attribute name: data-count
  @attribute({ type: Number }) size = 10;      // type conversion
  @attribute({ type: Boolean }) disabled = false;
  @attribute({ type: Object }) config = {};
  @attribute({ disableReflect: true }) secret = ''; // JS→HTML reflection disabled

  @innerHtml
  render() {
    return `<label>{{label}}: {{count}}</label>`;
  }
}
```

```html
<my-input label="Name" data-count="5" disabled></my-input>
```

Combine with `@state` for reactive attribute handling:

```typescript
@state @attribute({ type: Number }) count = 0;
```

---

### `@query(selector?)` / `@queryAll(selector?)`

Lazily binds DOM elements.

```typescript
@elementDefine('form-component')
class FormComponent extends HTMLElement {
  // Property getter
  @query('input[name="email"]') emailInput!: HTMLInputElement;
  @queryAll('li') listItems!: NodeListOf<HTMLLIElement>;

  // Method — called once after connectedCallback
  @query('input')
  onInputReady(el: HTMLInputElement) {
    el.focus();
  }

  // Search in Shadow DOM
  @query({ selector: 'p', root: 'shadow' }) shadowP!: HTMLElement;

  // Search in Light DOM
  @query({ selector: 'slot', root: 'light' }) slotEl!: HTMLSlotElement;

  @innerHtml({ useShadow: true })
  render() {
    return `<input name="email" type="email" /><p>Status</p>`;
  }
}
```

---

### `@addEventListener(options)`

Declaratively registers event listeners. Auto-removed on disconnect when `autoRemoveEventListeners: true` (default).

```typescript
@elementDefine('event-demo')
class EventDemo extends HTMLElement {
  @state value = '';

  @innerHtml({ useShadow: true })
  render() {
    return `
      <input id="input" type="text" />
      <button id="submit-btn">Submit</button>
    `;
  }

  @addEventListener({ type: 'input', query: '#input' })
  onInput(event: InputEvent) {
    this.value = (event.target as HTMLInputElement).value;
  }

  @addEventListener({
    type: 'click',
    query: '#submit-btn',
    stopPropagation: true,
    preventDefault: true,
  })
  onSubmit() {
    console.log('Submitted:', this.value);
  }

  // Host element event (no query)
  @addEventListener({ type: 'click' })
  onHostClick(event: MouseEvent) {
    console.log('Component clicked');
  }
}
```

---

### `@emitCustomEvent(options)`

Automatically dispatches a `CustomEvent` with the method's return value as `detail`.

```typescript
@elementDefine('data-emitter')
class DataEmitter extends HTMLElement {
  @emitCustomEvent('data-change')
  updateData() {
    return { value: 42, timestamp: Date.now() };
    // → CustomEvent('data-change', { detail: { value: 42, ... } })
  }

  @emitCustomEvent({ type: 'user-select', bubbles: true, composed: true })
  selectUser(id: number) {
    return { id, name: 'Kim' };
  }
}
```

```typescript
// Listen from parent
document.querySelector('data-emitter')?.addEventListener('data-change', (e: CustomEvent) => {
  console.log(e.detail); // { value: 42, timestamp: ... }
});
```

---

## Lifecycle Hooks

### Decorator Hooks

```typescript
@elementDefine('lifecycle-demo')
class LifecycleDemo extends HTMLElement {
  @onBeforeConnected
  beforeConnect() { console.log('Before template injection'); }

  @onAfterConnected  // alias: @onConnected
  afterConnect() { console.log('After template injection and binding'); }

  @onBeforeDisconnected
  beforeDisconnect() { console.log('Before removal from DOM'); }

  @onAfterDisconnected  // alias: @onDisconnected
  afterDisconnect() { console.log('After removal and cleanup'); }

  // Single attribute
  @onAttributeChanged('name')
  onNameChange(newVal: string, oldVal: string, attrName: string) {
    console.log(`name: ${oldVal} → ${newVal}`);
  }

  // MutationSubscriber
  @onMutationSubscribe({root: 'all'})
  onMutationSubscribe(mutations: MutationRecord[], observer: MutationObserver, root: 'light' | 'shadow') {
    console.log('mutations', mutations, observer, root);
  }
  
  // Wildcard: any attribute change
  @onAttributeChanged
  onAnyChange(newVal: string, oldVal: string, attrName: string) {
    console.log(`${attrName} changed`);
  }

  // Multiple attributes
  @onAttributeChanged(['name', 'age'])
  onNameOrAgeChange(newVal: string, oldVal: string, attrName: string) {
    console.log(`${attrName} changed`);
  }

  @onAddEventListener
  onListenerAdded(el: Element, type: string, handler: EventListener) {
    console.log(`Event registered: ${type}`);
  }
}
```

### HTML Attribute Hooks (Zero-JS)

Execute scripts directly in HTML without any JS class definition.

```html
<my-component
  swc-on-constructor="console.log('created', this)"
  swc-on-connected="console.log('connected', $host)"
  swc-on-disconnected="console.log('removed')"
  swc-on-attribute-changed="console.log($name, $old, '→', $new)"
></my-component>
```

---

## Template Binding & Expressions

```typescript
@state user = { name: 'Kim', age: 30 };
@state items = ['apple', 'banana'];
@state isActive = true;
@state count = 0;
```

```html
<!-- Simple value -->
<p>{{name}}</p>

<!-- Object path -->
<p>{{user.name}} (age: {{user.age}})</p>

<!-- Expressions -->
<p>Item #{{count + 1}}</p>
<p class="{{isActive ? 'active' : 'inactive'}}">Status</p>
<p>{{items.length}} items</p>
```

---

## Inline Event Handler Variables

Available variables in all `swc-on-*` attribute scripts:

| Variable | Description |
|---|---|
| `this` | The DOM element where the event fired |
| `event` | The event object |
| `$data` | `event.detail` (CustomEvent) |
| `$host` / `$lastHost` | Nearest SWC parent component |
| `$firstHost` | Top-most SWC ancestor component |
| `$hosts` | All SWC ancestors array `[root, ..., parent]` |
| `$q(sel, root?)` | `querySelector` shorthand |
| `$qa(sel, root?)` | `querySelectorAll` → Array |
| `$qi(id, root?)` | `getElementById` shorthand |
| `$d` | `document` |
| `$w` | `window` |

```html
<!-- Call host component method -->
<button swc-on-click="$host.save();">Save</button>

<!-- Access top-most component in nested structure -->
<button swc-on-click="$firstHost.logout();">Logout</button>

<!-- DOM query shorthands -->
<button swc-on-click="$qi('modal')?.show();">Open Modal</button>
<button swc-on-click="$q('.panel', $host.shadowRoot)?.classList.toggle('open');">Panel</button>

<!-- Event object -->
<input swc-on-input="$host.name = event.target.value" />

<!-- Run once on connect -->
<div swc-on-connected="console.log('connected', $host, $firstHost)"></div>
```

---

## Built-in Logic Components

All standard HTML tags support logic via the `is` attribute.  
Pass data with `swc-value`, item alias with `as`, index alias with `as-index`.

### `swc-for-of` — Loop (removes element when array is empty)

```html
<!-- Standalone component -->
<swc-for-of swc-value="{{users}}" as="u">
  <div>{{u.name}}</div>
</swc-for-of>

<!-- is attribute (keeps semantic HTML) -->
<ul is="swc-for-of-ul" swc-value="{{users}}" as="u" as-index="i">
  <li>{{i + 1}}. {{u.name}} — {{u.active ? 'Active' : 'Inactive'}}</li>
</ul>
```

```typescript
const list = document.querySelector('ul[is="swc-for-of-ul"]') as SwcForOfUl;
list.swcValue = [{ name: 'Kim', active: true }, { name: 'Lee', active: false }];
list.addRow({ name: 'Park', active: true });  // add row
list.removeRow(0);                             // remove first row
```

### `swc-for-of-children` — Loop (keeps wrapper element)

```html
<ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item" as-index="idx">
  <li class="{{item.done ? 'done' : ''}}">
    {{idx + 1}}. {{item.text}}
    <button swc-on-click="$firstHost.removeItem({{idx}});">Delete</button>
  </li>
</ul>
```

### `swc-if` — Conditional rendering (removes from DOM when false)

```html
<swc-if swc-value="{{isLoggedIn}}">
  <p>Welcome!</p>
</swc-if>

<div is="swc-if-div" swc-value="{{isVisible}}" style="background: yellow;">
  <b>Conditional content</b>
</div>
```

```typescript
const el = document.querySelector('[is="swc-if-div"]') as SwcIfDiv;
el.swcValue = true;   // insert into DOM
el.swcValue = false;  // remove from DOM
```

### `swc-if-children` — Conditional rendering (keeps wrapper, swaps children)

```html
<section is="swc-if-children-section" swc-value="{{showContent}}">
  <p>Shown only when showContent is true.</p>
</section>
```

### `swc-choose` / `swc-when` / `swc-other` — Switch/Case

```html
<!-- Removes element if no case matches -->
<div is="swc-choose-div" swc-value="{{status}}">
  <span is="swc-when-span" test="'loading'">⏳ Loading...</span>
  <span is="swc-when-span" test="'error'">❌ Error</span>
  <span is="swc-when-span" test="'success'">✅ Done</span>
  <span is="swc-other-span">⚪ Idle</span>
</div>
```

```typescript
const el = document.querySelector('[is="swc-choose-div"]') as SwcChooseDiv;
el.swcValue = 'loading';  // shows "⏳ Loading..."
el.swcValue = 'success';  // shows "✅ Done"
el.swcValue = 'unknown';  // shows "⚪ Idle" (swc-other)
```

### `swc-choose-children` — Switch/Case (keeps wrapper)

```html
<div is="swc-choose-children-div" swc-value="{{tab}}">
  <div is="swc-when-div" test="'home'">Home tab content</div>
  <div is="swc-when-div" test="'profile'">Profile tab content</div>
  <div is="swc-other-div">Default tab content</div>
</div>
```

### `swc-object` — Object binding

```html
<div is="swc-object-div" swc-value="{{user}}" as="u">
  <p>Name: {{u.name}}</p>
  <p>Age: {{u.age}}</p>
</div>
```

```typescript
const el = document.querySelector('[is="swc-object-div"]') as SwcObjectDiv;
el.swcValue = { name: 'Kim', age: 30 };
el.swcValue.name = 'Lee';  // Proxy reactive — DOM auto-updates
```

---

## Hierarchical Context (`$host` System)

Access parent components from nested components.

```typescript
@elementDefine('parent-comp')
class ParentComp extends HTMLElement {
  @state count = 0;
  @state items = [{ name: 'Item 1' }, { name: 'Item 2' }];

  removeItem(index: number) {
    this.items.splice(index, 1);
  }

  @innerHtml({ useShadow: true })
  render() {
    return `
      <p>Count: {{count}}</p>
      <!-- Call parent method from child event -->
      <button swc-on-click="$host.count++;">Increment</button>

      <!-- In nested for-of: $host = swc-for-of-children-ul, $firstHost = parent-comp -->
      <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item" as-index="i">
        <li>
          {{item.name}}
          <button swc-on-click="$firstHost.removeItem({{i}});">Delete</button>
        </li>
      </ul>
    `;
  }
}
```

### Accessing parent `@state` from nested components

```typescript
@elementDefine('app-root')
class AppRoot extends HTMLElement {
  @state currentUser = { name: 'Kim', role: 'admin' };
  @state items = [{ id: 1, text: 'Item 1' }, { id: 2, text: 'Item 2' }];

  @innerHtml({ useShadow: true })
  render() {
    return `
      <ul is="swc-for-of-children-ul" swc-value="{{items}}" as="item">
        <li>
          {{item.text}}
          <!-- Access parent's currentUser.name directly in child template -->
          <small>(by: {{currentUser.name}})</small>
        </li>
      </ul>
    `;
  }
}
```

---

## Shadow DOM + Slot Pattern

```typescript
@elementDefine('card-layout')
class CardLayout extends HTMLElement {
  @state title = '';

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; border: 1px solid #ccc; border-radius: 8px; }
        .header { background: #f5f5f5; padding: 12px; font-weight: bold; }
        .body { padding: 16px; }
      </style>
      <div class="header">{{title}}</div>
      <div class="body">
        <slot></slot>
        <slot name="footer"></slot>
      </div>
    `;
  }
}
```

```html
<card-layout title="User Info">
  <p>Name: Kim</p>
  <p>Email: kim@example.com</p>
  <div slot="footer">
    <button>Edit</button>
  </div>
</card-layout>
```

---

## File Separation Pattern

When splitting a component into its own file, use `export default (w: Window) => {...}` to inject `window`:

```typescript
// MyComponent.ts
import { elementDefine, innerHtml, state } from '@dooboostore/simple-web-component';

export default (w: Window) => {
  @elementDefine({ name: 'my-component', window: w })
  class MyComponent extends HTMLElement {
    @state name = '';

    @innerHtml({ useShadow: true })
    render() {
      return `<p>Hello, {{name}}!</p>`;
    }
  }
  return MyComponent;
};
```

```typescript
// index.ts — inject window at the entry point
import createMyComponent from './MyComponent';

const MyComponent = createMyComponent(window);
```

> For browser-only SPAs, you can omit the `window` option entirely.  
> `@elementDefine('my-component')` works fine — `buildEnv` falls back to the global `window` automatically.

---

## SPA Routing — Full Working Example

SWC provides two ways to bootstrap a SPA. Choose the one that fits your setup.

---

### Option A — `<swc-app>` Custom Element (HTML-first, no JS bootstrap)

The simplest approach. Drop `<swc-app>` into your HTML and point it at a config function via `swc-get-application-config`. No manual `DOMContentLoaded` wiring needed.

**`index.html`**
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>SWC SPA</title>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <!--
    swc-get-application-config: a global function name that returns SwcAttributeConfigType.
    SwcApp calls this function after the script is evaluated.
  -->
  <swc-app swc-get-application-config="applicationConfig(this)"></swc-app>
</body>
</html>
```

**`index.ts`**
```typescript
import 'reflect-metadata';
import {
  elementDefine, innerHtml, state, attribute,
  query, addEventListener, onAfterConnected, onMutationSubscribe,
  SwcApp, SwcAppInterface, SwcAttributeConfigType
} from '@dooboostore/simple-web-component';
import { SimpleApplication } from '@dooboostore/simple-boot/SimpleApplication';
import { Sim } from '@dooboostore/simple-boot/decorators/SimDecorator';
import { Router as WebRouter } from '@dooboostore/core-web/routers/Router';
import { Router } from '@dooboostore/simple-boot/decorators/route/Router';
import { RouterAction } from '@dooboostore/simple-boot/route/RouterAction';
import { RoutingDataSet } from '@dooboostore/simple-boot/route/RouterManager';
import createMyComponent from './MyComponent';

const w = window;

// ─── Pages ───────────────────────────────────────────────────────────────────

// MyComponent is defined in a separate file and receives `window` via factory
const MyComponent = createMyComponent(w);

@Sim
@elementDefine({ name: 'my-component2', window: w })
class MyComponent2 extends HTMLElement {
  say() { console.log('hello MyComponent2'); }

  @innerHtml
  render() {
    return `
      <div style="padding: 10px; border: 1px solid #ccc;">
        <h3>My Component2</h3>
      </div>
    `;
  }
}

// ─── Sub Router ──────────────────────────────────────────────────────────────

@Sim
@Router({
  path: '/hello',
  route: {
    '/good': MyComponent,
    '/good2': MyComponent2,
  }
})
@elementDefine('my-router')
class MyRouter extends HTMLElement implements RouterAction.CanActivate {
  @query('#my-route') myRoute!: HTMLElement;

  @innerHtml
  render() {
    return `
      <div style="padding: 10px; border: 1px solid #ccc;">
        <h3>My Router</h3>
        <div id="my-route"></div>
      </div>
    `;
  }

  async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
    if (data instanceof Node && !this.myRoute.contains(data)) {
      this.myRoute.replaceChildren(data);
    }
  }
}

// ─── Root Router ─────────────────────────────────────────────────────────────

@Sim
@Router({ routers: [MyRouter] })
@elementDefine({ name: 'root-router', window: w })
class RootRouter extends HTMLDivElement implements RouterAction.CanActivate {
  @state route = '';
  @state name = 'Root Name';

  constructor(
    private router: WebRouter,
    private sim: SimpleApplication
  ) {
    super();
  }

  async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
    this.route = url.intent.pathname;
    if (data instanceof Node && !this.contains(data)) {
      this.replaceChildren(data);
    }
  }

  @addEventListener({ type: 'click', query: '#route-btn' })
  onRouteBtnClick() {
    this.name = 'Changed Root ' + new Date().getSeconds();
    this.router.go('/hello/good');
  }

  // @onMutationSubscribe — fires when MutationObserver detects DOM changes
  @onMutationSubscribe({ root: 'all' })
  onDomChanged(mutations: MutationRecord[], observer: MutationObserver) {
    console.log('DOM changed:', mutations);
  }

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        .container { padding: 10px; border: 1px solid #ccc; }
      </style>
      <div class="container">
        <h3>Root Router</h3>
        <p>Current Route: <strong>{{route}}</strong></p>
        <p>Current name: <strong>{{name}}</strong></p>
        <button id="route-btn">Go to /hello/good</button>
        <slot></slot>
      </div>
    `;
  }
}

// ─── Bootstrap (Option A) ────────────────────────────────────────────────────
// Export a config factory function and expose it on window.
// <swc-app swc-get-application-config="applicationConfig(this)"> calls this.

export const applicationConfig = (app: SwcAppInterface): SwcAttributeConfigType => ({
  rootRouter: RootRouter,
  path: '/hello/good',
  window: w,
});

(w as any).applicationConfig = applicationConfig;
```

**`MyComponent.ts`** — separate file, receives `window` via factory
```typescript
import { elementDefine, innerHtml, state, attribute, onAfterConnected, addEventListener } from '@dooboostore/simple-web-component';
import { Sim } from '@dooboostore/simple-boot/decorators/SimDecorator';
import { Router as WebRouter } from '@dooboostore/core-web/routers/Router';

// export default (w: Window) => {...} pattern
// — keeps the file decoupled from the global window
export default (w: Window) => {
  @Sim
  @elementDefine({ name: 'my-component', window: w })
  class MyComponent extends HTMLElement {
    @state route = 'zz';
    @state @attribute name?: string | null;
    @state showExtra = false;
    @state showRealIf = true;

    constructor(private router: WebRouter) {
      super();
    }

    @innerHtml({ useShadow: true })
    render() {
      return `
        <div style="padding: 10px; border: 1px solid #ccc;">
          <h3>My Component</h3>
          <p>Route: <strong>{{route}}</strong></p>
          <p>Name: <strong>{{name}}</strong></p>
          <button id="change-name-btn">Change Name</button>
          <button id="toggle-extra-btn">Toggle IfChildren</button>
          <button id="toggle-real-if-btn">Toggle Real If</button>

          <!-- Inline event: this = button, $host = my-component -->
          <button swc-on-click="console.log('host:', $host);">Log Host</button>

          <!-- Two-way binding via inline event -->
          <input swc-on-input="$host.name = event.target.value" value="{{name}}" />

          <h4>IfChildren (wrapper stays)</h4>
          <div is="swc-if-children-div" swc-value="{{showExtra}}">
            <p style="color: blue;">Extra content!</p>
          </div>

          <h4>Real If (removed from DOM)</h4>
          <div is="swc-if-div" swc-value="{{showRealIf}}" style="background: yellow; padding: 5px;"
            swc-on-click="console.log(this, $host, $firstHost, 'clicked')"
            swc-on-connected="console.log('connected', $host)">
            <b>Toggle me to disappear from DOM.</b>
          </div>
        </div>
      `;
    }

    @onAfterConnected
    init() { console.log('MyComponent connected', this.name); }

    @addEventListener({ type: 'click', query: '#change-name-btn' })
    onChangeName() { this.name = 'Changed ' + new Date().getSeconds(); }

    @addEventListener({ type: 'click', query: '#toggle-extra-btn' })
    onToggleExtra() { this.showExtra = !this.showExtra; }

    @addEventListener({ type: 'click', query: '#toggle-real-if-btn' })
    onToggleRealIf() { this.showRealIf = !this.showRealIf; }
  }

  return MyComponent;
};
```

---

### Option B — Programmatic Bootstrap (`Swc` class)

Use this when you need full control over the bootstrap lifecycle.

```typescript
import 'reflect-metadata';
import { Swc } from '@dooboostore/simple-web-component';
// ... same component definitions as Option A ...

const swc = new Swc({ rootRouter: RootRouter, window: w });

window.addEventListener('DOMContentLoaded', async () => {
  document.body.appendChild(swc.run());
  await swc.routing('/hello/good');
});
```

---

### Routing Architecture

```
RootRouter (extends HTMLDivElement)
  └── implements RouterAction.CanActivate
  └── replaceChildren(data)  ← routed component docks here (Light DOM)
        │
        └── MyRouter  (@Router path: '/hello')
              └── implements RouterAction.CanActivate
              └── #my-route.replaceChildren(data)
                    ├── /good  → MyComponent
                    └── /good2 → MyComponent2
```

Key points:
- `canActivate(url, data)` receives the instantiated component node as `data`
- `replaceChildren(data)` swaps the routed component atomically — no flicker
- `RootRouter` can extend any built-in element (`HTMLDivElement`, `HTMLElement`, etc.)
- The `@Router` decorator wires path matching; `canActivate` decides where to mount

---

## Complete Component Example

@Sim
@elementDefine({ name: 'page-home', window: w })
class PageHome extends HTMLElement {
  @state count = 0;
  @state todos: { text: string; done: boolean }[] = [
    { text: 'Learn SWC', done: true },
    { text: 'Build something', done: false },
  ];

  addTodo() {
    this.todos = [...this.todos, { text: 'New todo ' + Date.now(), done: false }];
  }

  toggleTodo(index: number) {
    this.todos[index].done = !this.todos[index].done;
  }

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; padding: 20px; }
        .done { text-decoration: line-through; color: #999; }
        button { cursor: pointer; margin: 4px; padding: 6px 12px; }
        li { margin: 6px 0; }
      </style>

      <h2>🏠 Home Page</h2>

      <section>
        <h3>Counter: {{count}}</h3>
        <button swc-on-click="$host.count++;">+1</button>
        <button swc-on-click="$host.count--;">-1</button>
        <button swc-on-click="$host.count = 0;">Reset</button>
      </section>

      <hr/>

      <section>
        <h3>Todo List ({{todos.length}} items)</h3>
        <button id="add-btn">Add Todo</button>
        <ul is="swc-for-of-children-ul" swc-value="{{todos}}" as="todo" as-index="i">
          <li class="{{todo.done ? 'done' : ''}}">
            <input type="checkbox"
              swc-on-change="$firstHost.toggleTodo({{i}});"
              {{todo.done ? 'checked' : ''}}
            />
            {{todo.text}}
          </li>
        </ul>
      </section>
    `;
  }

  @addEventListener({ type: 'click', query: '#add-btn' })
  onAddTodo() {
    this.addTodo();
  }

  @onAfterConnected
  init() {
    console.log('PageHome connected');
  }
}

// ─── Page: About ─────────────────────────────────────────────────────────────

@Sim
@elementDefine({ name: 'page-about', window: w })
class PageAbout extends HTMLElement {
  @state @attribute name = 'World';

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; padding: 20px; }
        input { padding: 6px; margin-right: 8px; }
      </style>

      <h2>ℹ️ About Page</h2>
      <p>Hello, <strong>{{name}}</strong>!</p>
      <input
        type="text"
        placeholder="Enter your name"
        value="{{name}}"
        swc-on-input="$host.name = event.target.value"
      />

      <hr/>

      <h3>Status Panel</h3>
      <div is="swc-choose-children-div" swc-value="{{name}}">
        <p is="swc-when-p" test="'World'">👋 Default greeting</p>
        <p is="swc-when-p" test="'Admin'">🔑 Admin mode activated</p>
        <p is="swc-other-p">😊 Hello, {{name}}!</p>
      </div>
    `;
  }
}

// ─── Sub Router ──────────────────────────────────────────────────────────────

@Sim
@Router({
  path: '/app',
  route: {
    '/home': PageHome,
    '/about': PageAbout,
  }
})
@elementDefine({ name: 'app-sub-router', window: w })
class AppSubRouter extends HTMLElement implements RouterAction.CanActivate {
  @query('#outlet') outlet!: HTMLElement;

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; }
        nav { background: #f0f0f0; padding: 10px; }
        nav a { margin-right: 12px; cursor: pointer; color: #1a73e8; text-decoration: none; }
        nav a:hover { text-decoration: underline; }
        #outlet { min-height: 200px; }
      </style>
      <nav>
        <a id="nav-home">Home</a>
        <a id="nav-about">About</a>
      </nav>
      <div id="outlet"></div>
    `;
  }

  async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
    if (data instanceof Node && !this.outlet?.contains(data)) {
      this.outlet?.replaceChildren(data);
    }
  }

  @addEventListener({ type: 'click', query: '#nav-home', root: 'shadow' })
  goHome() { (this as any)._sim?.routing('/app/home'); }

  @addEventListener({ type: 'click', query: '#nav-about', root: 'shadow' })
  goAbout() { (this as any)._sim?.routing('/app/about'); }
}

// ─── Root Router ─────────────────────────────────────────────────────────────

@Sim
@Router({ routers: [AppSubRouter] })
@elementDefine({ name: 'root-router', window: w })
class RootRouter extends HTMLElement implements RouterAction.CanActivate {
  @state currentPath = '/';

  constructor(
    private router: WebRouter,
    private sim: SimpleApplication
  ) {
    super();
  }

  async canActivate(url: RoutingDataSet, data?: any): Promise<void> {
    this.currentPath = url.intent.pathname;
    if (data instanceof Node && !this.contains(data)) {
      this.replaceChildren(data);
    }
  }

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; font-family: sans-serif; max-width: 800px; margin: 0 auto; }
        header {
          background: #1a73e8; color: white;
          padding: 16px 20px; display: flex;
          align-items: center; justify-content: space-between;
        }
        header h1 { margin: 0; font-size: 1.2rem; }
        .path { font-size: 0.85rem; opacity: 0.8; }
        main { padding: 0; }
      </style>
      <header>
        <h1>SWC SPA Demo</h1>
        <span class="path">{{currentPath}}</span>
      </header>
      <main><slot></slot></main>
    `;
  }
}

// ─── Bootstrap ───────────────────────────────────────────────────────────────

const swc = new Swc({ rootRouter: RootRouter, window: w });

window.addEventListener('DOMContentLoaded', async () => {
  document.body.appendChild(swc.run());
  await swc.routing('/app/home');
});
```

```html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>SWC SPA Demo</title>
</head>
<body>
  <!-- root-router is appended by swc.run() -->
</body>
</html>
```

---

## Complete Component Example

```typescript
import 'reflect-metadata';
import {
  elementDefine, innerHtml, state, attribute,
  query, addEventListener, onAfterConnected,
  emitCustomEvent, onAttributeChanged
} from '@dooboostore/simple-web-component';

@elementDefine({ name: 'user-profile', autoRemoveEventListeners: true })
class UserProfile extends HTMLElement {
  @state @attribute name = 'Guest';
  @state @attribute({ type: Number }) age = 0;
  @state isEditing = false;
  @state history: string[] = [];

  @query('input[name="name"]') nameInput!: HTMLInputElement;

  @emitCustomEvent({ type: 'profile-save', bubbles: true })
  save() {
    return { name: this.name, age: this.age };
  }

  @innerHtml({ useShadow: true })
  render() {
    return `
      <style>
        :host { display: block; font-family: sans-serif; }
        .card { padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
        button { cursor: pointer; margin: 4px; padding: 6px 12px; }
        input { padding: 6px; margin: 4px; }
      </style>

      <div class="card">
        <h2>{{name}} (age: {{age}})</h2>
        <button id="edit-btn">Edit</button>
      </div>

      <div is="swc-if-children-div" swc-value="{{isEditing}}">
        <div class="card">
          <input name="name" type="text" value="{{name}}" placeholder="Name" />
          <input name="age" type="number" value="{{age}}" placeholder="Age" />
          <button id="save-btn">Save</button>
          <button id="cancel-btn">Cancel</button>
        </div>
      </div>

      <h4>Change History ({{history.length}})</h4>
      <ul is="swc-for-of-children-ul" swc-value="{{history}}" as="log">
        <li>{{log}}</li>
      </ul>
    `;
  }

  @addEventListener({ type: 'click', query: '#edit-btn' })
  onEdit() { this.isEditing = true; }

  @addEventListener({ type: 'click', query: '#cancel-btn' })
  onCancel() { this.isEditing = false; }

  @addEventListener({ type: 'click', query: '#save-btn' })
  onSave() {
    this.name = this.nameInput?.value ?? this.name;
    this.isEditing = false;
    this.save(); // dispatches CustomEvent automatically
  }

  @onAttributeChanged('name')
  onNameChange(newVal: string, oldVal: string) {
    if (oldVal && newVal !== oldVal) {
      this.history = [...this.history, `${oldVal} → ${newVal}`];
    }
  }

  @onAfterConnected
  init() {
    console.log('UserProfile ready');
  }
}
```

```html
<user-profile name="Kim" age="30"></user-profile>

<script>
  document.querySelector('user-profile').addEventListener('profile-save', (e) => {
    console.log('Saved:', e.detail); // { name: '...', age: ... }
  });
</script>
```

---

## License

MIT
