# Signy: rules for AI assistants

Read this before you write an app or a component with Signy. It covers the `html` tagged template, which needs no build step. JSX is at the end.

## How Signy works

- State lives in signals. `new Signal(0)` or `signal(0)`. Read with `.value`, write with `.value = x` or `.set(prev => next)`. `.peek()` reads without tracking.
- Derived state is `new Computed(() => ...)` or `computed(...)`. It is cached and read with `.value`, or `.peek()` without tracking, like a signal.
- A component is a class that extends `Component`. `render()` runs once. It never runs again when state changes.
- Only functions update. `${() => count.value}` is a live binding. `${count.value}` is the value at render time and stays frozen.
- The same rule holds for attributes: `class=${() => ...}`, `disabled=${() => ...}`, `style=${() => ({ ... })}`.
- Events are `on` plus the event name: `onClick`, `onInput`, `onSubmit`, `onKeyDown`. The name is lowercased, so `onDblClick` listens to `dblclick`.
- Conditionals and lists use the control flow functions `Show`, `For`, `Index`, `Switch` and `Match`. Call them as functions inside `${}`, or use them as tags (`<${Show} when=${...}>...</${Show}>`).
- Everything a component creates while rendering belongs to it: bindings, control flow, child components. Unmounting disposes all of it. There is nothing to clean up by hand.
- Updates apply in a microtask, not synchronously. `batch(() => ...)` groups several writes into one update.
- An app is `application(RootComponent).mount('#app')`. Plugins such as the router go in with `app.use(...)` before `mount`.

## Do not

- Do not write React. There is no `useState`, `useEffect`, `setState`, virtual DOM or re-render. Do not expect `render()` to run again.
- Do not interpolate a bare signal value where it must update. `${this.count.value}` renders once. Write `${() => this.count.value}`.
- Do not return markup from a function child. `${() => ok ? html`<b>yes</b>` : null}` renders an empty text node. Use `Show`.
- Do not call `.map()` over a signal array in a template for a list that changes. It renders once. Use `For`.
- Do not bind `value=${() => ...}` on an input and expect it to reset the field. It sets the attribute, and the browser ignores the attribute once the user has typed. Use a ref and set `ref.current.value`.
- Do not call `component.mount()` yourself or create the root with `new`. Go through `application()`.
- Do not use `className`, `onclick` (all lowercase) or `@click`. Use `class` and `onClick`.
- Do not rely on `flushSync()` in tests. It only delivers signal notifications and does not run effects or DOM updates. Call `flushAll()` instead (see Tests).
- Do not use `console.log` in framework code. App code may.

## Starter

With npm:

```bash
npm install signy
```

```typescript
import { Component, Signal, Computed, application, html } from 'signy';

class Counter extends Component {
  private count = new Signal(0);
  private double = new Computed(() => this.count.value * 2);

  protected render() {
    return html`
      <button onClick=${() => this.count.set((c) => c + 1)}>
        ${() => this.count.value} (double ${() => this.double.value})
      </button>
    `;
  }
}

application(Counter).mount('#app');
```

Without a build step, the global build defines `Signy`:

```html
<div id="app"></div>
<script src="https://unpkg.com/signy/dist/signy.global.min.js"></script>
<script>
  const { Component, Signal, application, html } = Signy;

  class Hello extends Component {
    name = new Signal('world');
    render() {
      return html`<p>Hello ${() => this.name.value}</p>`;
    }
  }

  application(Hello).mount('#app');
</script>
```

## Imports

| Import | What you get |
|---|---|
| `signy` | Everything below |
| `signy/core` | `Signal`, `signal`, `Computed`, `computed`, `effect`, `on`, `batch`, `untrack`, `onCleanup`, `createRoot`, `createSelector`. No DOM |
| `signy/web` | Core plus `html`, `h`, `Component`, `Show`, `For`, `Index`, `Switch`, `Match`, `Portal`, `ErrorBoundary`, `Suspense`, `lazy`, DOM helpers |
| `signy/store` | `store`, `ReactiveStore`, `produce`, `unwrap` |
| `signy/router` | `router`, `Link`, `NavLink`, `Outlet`, `lazyRoute`, `useRouter`, `useRoute`, `useParams`, `useQuery`, `useRouteData` |
| `signy/app` | `application`, `Application` |

## Patterns

Component with props, setup and lifecycle. `setup()` runs once before `render()`. `onMount` runs after the DOM exists and may return a cleanup. `onUnmount` runs before removal.

```typescript
interface ClockProps {
  label: string;
}

class Clock extends Component<ClockProps> {
  private now = new Signal(new Date());

  protected setup(): void {
    this.onMount(() => {
      const timer = setInterval(() => (this.now.value = new Date()), 1000);
      return () => clearInterval(timer);
    });
  }

  protected render() {
    return html`
      <p>${this.props.label}: ${() => this.now.value.toLocaleTimeString()}</p>
    `;
  }
}
```

Child components: use the class as a tag. Static props are plain values. For a prop that changes, pass a function and call it in the child. Content between the tags arrives as `this.props.children`.

```typescript
class Card extends Component<{ title: () => string; children?: Node }> {
  protected render() {
    return html`
      <section class="card">
        <h2>${() => this.props.title()}</h2>
        ${this.props.children}
      </section>
    `;
  }
}

class Page extends Component {
  private title = new Signal('Inbox');

  protected render() {
    return html`
      <${Card} title=${() => this.title.value}>
        <p>3 unread</p>
      </${Card}>
    `;
  }
}
```

Conditional: `Show` with an accessor in `when`. The `children` function receives the truthy value. It runs once per branch, untracked: the branch is rebuilt only when the truthy value changes (a different object, or falsy to truthy), not when a signal read inside it changes. Bind those signals with `${() => ...}` like anywhere else. A node child is moved in and out, so it keeps its listeners.

```typescript
html`
  <div>
    ${Show({
      when: () => this.user.value,
      fallback: html`<p>Signed out</p>`,
      children: (user) => html`
        <p>Hi ${user.name}</p>
        <button onClick=${() => (this.user.value = null)}>Sign out</button>
      `,
    })}
  </div>
`;
```

Lists: `For` with `keyBy` or `key` so rows are reused when the array changes. `index` is an accessor. The item is a plain value, so a field that changes on an item with the same key does not re-render. Keep such fields in a `Signal` on the item, or use a store.

```typescript
interface Todo {
  id: number;
  text: string;
  done: Signal<boolean>;
}

class TodoList extends Component {
  private todos = new Signal<Todo[]>([]);

  private remove(id: number) {
    this.todos.set((list) => list.filter((t) => t.id !== id));
  }

  protected render() {
    return html`
      <ul>
        ${For({
          each: () => this.todos.value,
          keyBy: 'id',
          fallback: html`<li>Nothing to do</li>`,
          children: (todo, index) => html`
            <li class=${() => (todo.done.value ? 'done' : '')}>
              ${() => index() + 1}. ${todo.text}
              <input
                type="checkbox"
                onChange=${() => todo.done.set((d) => !d)}
              />
              <button onClick=${() => this.remove(todo.id)}>Delete</button>
            </li>
          `,
        })}
      </ul>
    `;
  }
}
```

`Index` is the other way round: the item is an accessor and the index is a number. Use it for lists of primitives edited in place. Rows are kept and only their item changes, so read `item()` inside a binding: `${() => item()}`, not `${item()}`.

Several branches: `Switch` with `Match` children, first match wins.

```typescript
html`
  ${Switch({
    fallback: html`<p>Idle</p>`,
    children: [
      Match({ when: () => this.status.value === 'loading', children: () => html`<p>Loading</p>` }),
      Match({ when: () => this.status.value === 'error', children: () => html`<p>Failed</p>` }),
    ],
  })}
`;
```

Forms: read input in the event, keep state in signals, use a ref to write to the field.

```typescript
class Search extends Component {
  private query = new Signal('');
  private input = createRef<HTMLInputElement>();

  private clear() {
    this.query.value = '';
    if (this.input.current) this.input.current.value = '';
  }

  protected render() {
    return html`
      <form onSubmit=${(e: SubmitEvent) => e.preventDefault()}>
        <input
          ref=${this.input}
          type="search"
          onInput=${(e: Event) =>
            (this.query.value = (e.currentTarget as HTMLInputElement).value)}
        />
        <button type="button" onClick=${() => this.clear()}>Clear</button>
        <p>Searching for ${() => this.query.value}</p>
      </form>
    `;
  }
}
```

`ref` also takes a callback: `ref=${(el) => ...}`.

Effects: `effect(fn)` reruns when a signal it read changes. Its first run is in a microtask. Call `onCleanup` inside to undo the previous run. `onCleanup` also works inside bindings and control flow children. Create effects in `setup()` so they are owned by the component and disposed on unmount. `on(dep, fn, { defer: true })` skips the first run and passes the previous value.

```typescript
protected setup(): void {
  effect(() => {
    document.title = `${this.count.value} items`;
  });

  effect(
    on(() => this.page.value, (page, prev) => console.log(prev, '->', page), {
      defer: true,
    })
  );
}
```

Store: deep reactive state. Read through `.state`, write with a path in `.set()`. A function as the last argument receives the previous value.

```typescript
import { store } from 'signy';

const cart = store({ items: [] as { id: number; qty: number }[], coupon: '' });

cart.set('coupon', 'SPRING');
cart.set('items', (items) => [...items, { id: 1, qty: 1 }]);
cart.set('items', 0, 'qty', (q) => q + 1);

html`<p>${() => cart.state.items.length} items</p>`;
```

Async data: `resource`. `value`, `loading` and `error` are reactive. A source function refetches when it changes.

```typescript
import { resource } from 'signy';

const user = resource(
  () => this.userId.value,
  async (id) => (await fetch(`/api/users/${id}`)).json()
);

html`
  ${Show({ when: () => user.loading, children: () => html`<p>Loading</p>` })}
  ${Show({ when: () => user.value, children: (u) => html`<p>${u.name}</p>` })}
`;
```

Shared values down the tree: `createContext`. Provide in `setup()`, inject in a descendant. Provide a signal when the value changes.

```typescript
const ThemeContext = createContext<Signal<'light' | 'dark'>>(undefined, 'Theme');

class App extends Component {
  private theme = new Signal<'light' | 'dark'>('light');
  protected setup() {
    ThemeContext.provide(this, this.theme);
  }
}

class ThemeLabel extends Component {
  protected render() {
    const theme = ThemeContext.inject(this);
    return html`<span>${() => theme.value}</span>`;
  }
}
```

Router: define routes, `app.use(router)`, render `Outlet` where the page goes. `Link` sets `router-link-active` and `router-link-exact-active` on the active link, and `aria-current="page"` on the exact one (change it with `ariaCurrentValue`). `useParams(this)`, `useQuery(this)` and `useRoute(this)` return signals.

```typescript
import { application, Component, html, router, Link, Outlet, useParams, useRouter } from 'signy';

class UserPage extends Component {
  protected render() {
    const params = useParams(this);
    const nav = useRouter(this);
    return html`
      <h1>User ${() => params.value.id}</h1>
      <button onClick=${() => nav.push('/')}>Home</button>
    `;
  }
}

class Layout extends Component {
  protected render() {
    return html`
      <nav>
        <${Link} to="/">Home</${Link}>
        <${Link} to="/users/42">User 42</${Link}>
      </nav>
      <main><${Outlet} /></main>
    `;
  }
}

const appRouter = router({
  routes: [
    { path: '/', component: HomePage },
    { path: '/users/:id', component: UserPage },
    { path: '*', component: NotFound },
  ],
});

application(Layout).use(appRouter).mount('#app');
```

Guards: `appRouter.beforeEach(({ to }) => ...)` or `guards: [...]` on a route. Return `false` to cancel, a path to redirect, nothing to go on. They run on every navigation, the first one and back and forward included, and a route with guards, a redirect or a loader shows nothing until its first navigation is done. A cancelled back or forward steps back to the route on screen, so no history entry is lost. `history.listen((location, action, delta) => ...)` gets how many entries a pop moved.

Nested routes use `children` on a route and an `Outlet` in the parent component. `lazyRoute(() => import('./page'))` splits a route into its own chunk. `mode: 'hash'` or `mode: 'memory'` change the history.

SVG: write it in the template as in HTML. `svg`, `circle`, `path` and the other SVG tags are created in the SVG namespace. `a`, `title`, `style` and `script` inside an SVG need `xmlns="http://www.w3.org/2000/svg"`.

Code splitting: `lazy(() => import('./chart'))` takes a module whose default export is a component class or a function component. Render it inside `Suspense`, which shows its fallback while it loads.

Shadow DOM: set `protected shadow = true` and `protected styles = '...'` on the component for isolated CSS.

## Tests

Templates and control flow render synchronously, so the DOM is complete right after `mount()`. Updates after a signal write apply in a microtask. In tests, call `flushAll()` after a write. It runs pending notifications, effects and DOM updates until nothing is left:

```typescript
import { flushAll } from 'signy';

count.value = 2;
flushAll();
expect(root.textContent).toBe('2');
```

For async work such as route loaders and lazy imports, await a task first, then call `flushAll()`.

## JSX

JSX is compiled by `babel-plugin-jsx-dom-expressions`, the same compiler Solid uses. Calling `jsx()` at runtime throws, so `jsxImportSource` alone is not enough.

```javascript
// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-jsx-dom-expressions', {
      moduleName: 'signy/jsx',
      generate: 'dom',
      delegateEvents: true,
      wrapConditionals: true,
    }],
  ],
};
```

With the compiler, `{count.value}` in JSX is reactive because the compiler wraps it. The other rules above still hold: `render()` runs once and lists go through `For`.
