# Signy documentation Every Signy 1.x docs page in one file, for AI tools. Source: https://signy.dev/documentation/1/ --- ## 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=${...}>...`). - 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`yes` : 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` `; } } application(Counter).mount('#app'); ``` Without a build step, the global build defines `Signy`: ```html
``` ### 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 { 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`

${this.props.label}: ${() => this.now.value.toLocaleTimeString()}

`; } } ``` 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`

${() => this.props.title()}

${this.props.children}
`; } } class Page extends Component { private title = new Signal('Inbox'); protected render() { return html` <${Card} title=${() => this.title.value}>

3 unread

`; } } ``` 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`
${Show({ when: () => this.user.value, fallback: html`

Signed out

`, children: (user) => html`

Hi ${user.name}

`, })}
`; ``` 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; } class TodoList extends Component { private todos = new Signal([]); private remove(id: number) { this.todos.set((list) => list.filter((t) => t.id !== id)); } protected render() { return html`
    ${For({ each: () => this.todos.value, keyBy: 'id', fallback: html`
  • Nothing to do
  • `, children: (todo, index) => html`
  • (todo.done.value ? 'done' : '')}> ${() => index() + 1}. ${todo.text} todo.done.set((d) => !d)} />
  • `, })}
`; } } ``` `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`

Idle

`, children: [ Match({ when: () => this.status.value === 'loading', children: () => html`

Loading

` }), Match({ when: () => this.status.value === 'error', children: () => html`

Failed

` }), ], })} `; ``` 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(); private clear() { this.query.value = ''; if (this.input.current) this.input.current.value = ''; } protected render() { return html`
e.preventDefault()}> (this.query.value = (e.currentTarget as HTMLInputElement).value)} />

Searching for ${() => this.query.value}

`; } } ``` `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`

${() => cart.state.items.length} items

`; ``` 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`

Loading

` })} ${Show({ when: () => user.value, children: (u) => html`

${u.name}

` })} `; ``` Shared values down the tree: `createContext`. Provide in `setup()`, inject in a descendant. Provide a signal when the value changes. ```typescript const ThemeContext = createContext>(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`${() => theme.value}`; } } ``` 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`

User ${() => params.value.id}

`; } } class Layout extends Component { protected render() { return html`
<${Outlet} />
`; } } 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`. --- ## Installation Source: https://signy.dev/documentation/1/overview/installation/ From npm with a bundler, or one script tag with no build at all. ### npm Install the package and import what you need. ```shell npm install signy ``` ```js import { Component, Signal, application, html } from 'signy'; ``` Signy also works with `yarn add signy` and `pnpm add signy`. ### CDN The global build defines `Signy` on `window`, so a page needs no bundler and no build step. ```html ``` ### Entry points The `signy` import has everything. The smaller entry points let a bundler leave out the parts you do not use. | Import | What you get | | --- | --- | | `signy` | Everything below | | `signy/core` | `Signal`, `Computed`, `effect`, `batch`, `untrack` and the other primitives. No DOM. | | `signy/web` | Core plus `html`, `Component`, control flow, context and the DOM helpers | | `signy/store` | `store`, `ReactiveStore`, `produce`, `unwrap` | | `signy/router` | `router`, `Link`, `Outlet` and the route helpers | | `signy/app` | `application` and `Application` | | `signy/jsx-runtime` | The JSX runtime, see [JSX and TypeScript](https://signy.dev/documentation/1/overview/jsx-and-typescript/index.html.md) | ### Other builds The package also ships ES2017 and ES5 builds in `dist`, for older browsers, and a global build per entry point, such as `signy.core.global.min.js`. --- ## Quick start Source: https://signy.dev/documentation/1/overview/quick-start/ A component, a signal and one line to mount it. ### Starter page Save this as an `.html` file and open it in a browser. ```html Hello Signy!
``` ### A counter State lives in a `Signal`. The template binds a function, `${() => this.count.value}`, so that text updates when the signal changes. The button never renders again. ```js const { Component, Signal, application, html } = Signy; class Counter extends Component { count = new Signal(0); render() { return html` `; } } application(Counter).mount("#quick-counter"); ``` ### Key ideas - **Signals hold state**: Read with `.value`, write with `.value = x` or `.set(prev => next)`. - **render() runs once**: It builds the DOM a single time. Signy never calls it again when state changes. - **Only functions update**: `${() => count.value}` is live. `${count.value}` is the value at render time and stays frozen. - **Control flow is explicit**: Conditionals and lists go through `Show`, `For`, `Switch` and `Match`. - **Components own what they create**: Bindings, effects and child components are disposed on unmount. There is nothing to clean up by hand. ### Next steps - [How Signy works](https://signy.dev/documentation/1/overview/how-signy-works/index.html.md) explains the model behind these rules. - [Signals](https://signy.dev/documentation/1/reactivity/signals/index.html.md) and [components](https://signy.dev/documentation/1/components/components/index.html.md) cover what you will use most. - [Examples](https://signy.dev/examples/index.html.md) show whole apps you can copy. --- ## How Signy works Source: https://signy.dev/documentation/1/overview/how-signy-works/ Render once, then let signals update the exact nodes that read them. ### Render once A component's `render()` runs one time and returns DOM nodes. There is no virtual DOM and no second render. What changes later is wired up during that first render, as small effects bound to the exact text nodes and attributes that need them. ### Automatic tracking When a function bound in a template reads `signal.value`, the signal remembers it. A write to the signal runs only the functions that read it. You never list dependencies. ```js html`

${() => this.count.value}

${this.count.value}

`; ``` ### Updates in a microtask Signal writes are collected and applied together in a microtask, so five writes in a row cause one DOM update. Use [`batch()`](https://signy.dev/documentation/1/reactivity/batching/index.html.md) to group writes explicitly, and `flushAll()` in tests to apply them at once. ### Ownership Everything a component creates while rendering belongs to it: bindings, effects created in `setup()`, control flow branches and child components. Unmounting the component disposes all of it. ### Not React Signy looks familiar, but some React habits do not apply. - There is no `useState`, `useEffect` or `setState`. State is a signal, side effects are [`effect()`](https://signy.dev/documentation/1/reactivity/effects/index.html.md). - Do not expect `render()` to run again. Bind a function where a value must update. - Do not `.map()` a signal array in a template. It renders once. Use [`For`](https://signy.dev/documentation/1/templates/for-and-index/index.html.md). - Use `class` and `onClick`, not `className` or `onclick`. --- ## JSX and TypeScript Source: https://signy.dev/documentation/1/overview/jsx-and-typescript/ Typed components, and JSX when you have a build step. ### TypeScript Signy is written in TypeScript and ships its types. Signals infer their type from the first value. A component takes its props as a type parameter and marks `render()` and `setup()` as `protected`. ```js import { Component, Signal, html } from 'signy'; interface BadgeProps { label: string; } class Badge extends Component { private clicks = new Signal(0); protected render() { return html` `; } } ``` Give a signal an explicit type when the first value does not say enough. ```js const user = new Signal(null); ``` ### JSX JSX is compiled by `babel-plugin-jsx-dom-expressions`, the same compiler Solid uses. Calling `jsx()` at runtime throws, so setting `jsxImportSource` alone is not enough. ```js // 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 in a function. The other rules hold: `render()` runs once and lists go through `For`. ```js class Counter extends Component { private count = new Signal(0); protected render() { return ( ); } } ``` --- ## AI assistants Source: https://signy.dev/documentation/1/overview/ai-assistants/ Rules and Markdown docs so assistants write Signy, not React. AI assistants know React far better than Signy, so without help they reach for hooks and re-renders. Signy ships a rules file that fixes that. ### Usage rules [signy-usage.md](https://signy.dev/documentation/1/ai/signy-usage.md) covers how Signy works, the patterns for components, lists, forms, effects, stores and the router, and the mistakes to avoid. It is also in the npm package and in the repo at `ai/signy-usage.md`. Point your assistant at it from its instructions file, such as `AGENTS.md` or `CLAUDE.md`: ``` This project uses Signy. Read https://signy.dev/documentation/1/ai/signy-usage.md before you write a component. ``` ### llms.txt - [llms.txt](https://signy.dev/documentation/1/llms.txt) lists every docs page with a short description. - [llms-full.txt](https://signy.dev/documentation/1/llms-full.txt) has the usage rules and every docs page in one file. ### Markdown pages Every docs page has a Markdown copy at its URL plus `index.html.md`, such as [/documentation/1/reactivity/signals/index.html.md](https://signy.dev/documentation/1/reactivity/signals/index.html.md). Each live example becomes a JavaScript code block. --- ## Signals Source: https://signy.dev/documentation/1/reactivity/signals/ A value that tells the code reading it when it changes. ### Create a signal Use the class or the factory function. Both make the same thing. ```js const count = new Signal(0); const name = signal('Ada'); ``` ### Read and write ```js count.value; // read, and track it count.value = 5; // write count.set((c) => c + 1); // write from the previous value count.peek(); // read without tracking ``` A write with a value equal to the current one, compared with `Object.is`, notifies nobody. Pass `equals` to compare another way. ```js const point = new Signal({ x: 0, y: 0 }, { equals: (a, b) => a.x === b.x && a.y === b.y, }); ``` ### In templates Bind a function to keep a value live. Here the input writes the signal and the paragraph reads it. ```js const { Component, Signal, application, html } = Signy; class Greeting extends Component { name = new Signal("Ada"); render() { return html`

Hello ${() => this.name.value}!

`; } } application(Greeting).mount("#signal-name"); ``` ### Objects and arrays A signal tracks its value, not what is inside it. Replace an object or array to change it, do not mutate it in place. ```js todos.set((list) => [...list, newTodo]); // notifies todos.value.push(newTodo); // does not ``` For nested state you change in place, use a [store](https://signy.dev/documentation/1/reactivity/store/index.html.md). --- ## Computed Source: https://signy.dev/documentation/1/reactivity/computed/ A cached value derived from other signals. ### Create a computed ```js const count = new Signal(2); const double = new Computed(() => count.value * 2); const label = computed(() => `${count.value} items`); double.value; // 4 ``` A computed reads like a signal but you cannot write it. It recomputes lazily: only when it is read after a signal it depends on changed. Like a signal, `double.peek()` reads the current value without tracking it. ### Example ```js const { Component, Signal, Computed, application, html } = Signy; class Order extends Component { quantity = new Signal(1); price = 12.5; total = new Computed(() => (this.quantity.value * this.price).toFixed(2)); render() { return html`

Total: ${() => this.total.value} EUR

`; } } application(Order).mount("#computed-total"); ``` ### Chains Computed values can read other computed values. Each one recomputes at most once per change, however many readers it has. ```js const items = new Signal([3, 8, 1]); const sorted = computed(() => [...items.value].sort((a, b) => a - b)); const smallest = computed(() => sorted.value[0]); ``` --- ## Selectors Source: https://signy.dev/documentation/1/reactivity/selectors/ Change the selection, update two rows. A list that highlights the selected row usually reads the selection in every row. Then each change runs every row's binding. A selector keeps one boolean per key instead, so a change only runs the row that lost the selection and the row that gained it. ```js const selectedId = new Signal(null); const isSelected = createSelector(() => selectedId.value); // In each row html` (isSelected(row.id).value ? 'selected' : '')}>...`; ``` `isSelected(key)` returns a signal that is true while the source equals the key. Read it with `.value` in a binding. Pass a comparator as the second argument to match another way, such as `(id, user) => user?.id === id`. ### Example ```js const { Component, Signal, application, html, For, createSelector, createRef } = Signy; const ROWS = Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, label: `Row ${i + 1}` })); class Rows extends Component { selected = new Signal(null); isSelected = createSelector(() => this.selected.value); counter = createRef(); runs = 0; // Counts how often a row binding runs selectedIn(row) { if (this.counter.current) this.counter.current.textContent = ++this.runs; return this.isSelected(row.id).value; } render() { return html`

Row bindings run after the first render: 0

${For({ each: ROWS, keyBy: "id", children: (row) => html` String(this.selectedIn(row))}> `, })}
(this.isSelected(row.id).value ? "font-weight: 700" : "")}>${row.label}
`; } } application(Rows).mount("#selector-rows"); ``` Each Select runs two of the 1,000 row bindings, the row that lost the selection and the row that got it. The first runs one, since no row had it before. --- ## Effects Source: https://signy.dev/documentation/1/reactivity/effects/ Run code when the signals it reads change. ### effect() An effect runs once, tracks the signals it reads, and runs again when one of them changes. Its first run happens in a microtask. ```js effect(() => { document.title = `${this.count.value} items`; }); ``` Create effects in a component's `setup()`, so the component owns them and disposes them on unmount. ```js const { Component, Signal, application, html, effect } = Signy; class Logger extends Component { count = new Signal(0); log = new Signal([]); setup() { effect(() => { const count = this.count.value; this.log.set((lines) => [...lines.slice(-2), `count is ${count}`]); }); } render() { return html`

${() => this.log.value.join(", ")}

`; } } application(Logger).mount("#effect-log"); ``` ### onCleanup() Call `onCleanup()` inside an effect to undo its previous run, such as a timer or a listener. It runs before the next run and when the effect is disposed. ```js effect(() => { const delay = this.delay.value; const timer = setInterval(() => this.tick(), delay); onCleanup(() => clearInterval(timer)); }); ``` ### on() `on(source, fn)` tracks only the source and passes the new and previous values. `defer: true` skips the first run. ```js effect( on(() => this.page.value, (page, previous) => { console.log(previous, '->', page); }, { defer: true }) ); ``` ### untrack() Read a signal inside an effect without depending on it. ```js effect(() => { save(this.draft.value, untrack(() => this.user.value)); }); ``` --- ## Batching Source: https://signy.dev/documentation/1/reactivity/batching/ When updates run, and how to group them. ### Updates run in a microtask A signal write does not touch the DOM right away. Signy collects the changes and applies them in a microtask, so several writes in a row cause one update. ### batch() `batch()` groups writes explicitly. Readers run once, after the function returns, and see the final values. ```js batch(() => { this.firstName.value = 'Ada'; this.lastName.value = 'Lovelace'; }); ``` ```js const { Component, Signal, application, html, batch } = Signy; class Swap extends Component { left = new Signal("🍎"); right = new Signal("🍐"); swap() { batch(() => { const left = this.left.value; this.left.value = this.right.value; this.right.value = left; }); } render() { return html`

${() => this.left.value} and ${() => this.right.value}

`; } } application(Swap).mount("#batch-swap"); ``` ### flushAll() `flushAll()` runs every pending notification, effect and DOM update right away. Use it in tests after a write. See [Testing](https://signy.dev/documentation/1/application/testing/index.html.md). ```js count.value = 2; flushAll(); expect(root.textContent).toBe('2'); ``` `flushSync()` only delivers signal notifications. It does not run effects or DOM updates, so prefer `flushAll()`. --- ## Transitions Source: https://signy.dev/documentation/1/reactivity/transitions/ Keep input fast while slow work catches up. Some updates are urgent, such as the text in an input, and some can wait, such as filtering a long list. Transitions let the urgent part happen first. Signy transitions defer work to a later task and batch it. They do not split rendering into slices or interrupt it, as React's concurrent mode does. ### createDeferred() `createDeferred(source)` returns a getter for a copy of the source that updates after it. With `timeoutMs`, it waits until the source has been still for that long, like a debounce. ```js const { Component, Signal, Computed, application, html, For, createDeferred } = Signy; const WORDS = Array.from({ length: 2000 }, (_, i) => `item ${i} ${(i * 7919).toString(36)}`); class Filter extends Component { query = new Signal(""); deferred = createDeferred(() => this.query.value, { timeoutMs: 150 }); results = new Computed(() => WORDS.filter((word) => word.includes(this.deferred())).slice(0, 50)); render() { return html` (this.query.value = e.currentTarget.value)} />

String(this.query.value !== this.deferred())}> ${() => this.results.value.length} shown for "${() => this.deferred()}"

    ${For({ each: () => this.results.value.slice(0, 5), children: (word) => html`
  • ${word}
  • ` })}
`; } } application(Filter).mount("#deferred-filter"); ``` Comparing the source with the deferred copy tells you whether the results are stale, as the busy state above does. ### useTransition() `useTransition()` returns a pending signal and a start function. Writes inside `start()` are deferred and batched, and the pending signal is true until they are applied. ```js const [isPending, start] = useTransition(); start(() => { this.tab.value = 'reports'; }); html`

String(isPending.value)}>...

`; ``` Each call returns its own pending signal. `startTransition(fn)` is the same without one, and returns a promise for when the work is done. --- ## Store Source: https://signy.dev/documentation/1/reactivity/store/ Deep reactive state for nested objects and arrays. ### Create a store A store wraps an object. Reads through `.state` are tracked down to each property, so a binding updates only when the property it reads changes. ```js const cart = store({ items: [] as { id: number; name: string; qty: number }[], coupon: '', }); cart.state.items.length; // tracked ``` ### Write with set() `set()` takes a path and a value. A function as the last argument receives the previous value. ```js cart.set('coupon', 'SPRING'); cart.set('items', (items) => [...items, { id: 1, name: 'Tea', qty: 1 }]); cart.set('items', 0, 'qty', (qty) => qty + 1); ``` `merge(partial)` sets several top level keys, `update(fn)` replaces the whole state, and `unwrap()` returns a plain copy. ### Example ```js const { Component, application, html, store, For } = Signy; class Cart extends Component { cart = store({ items: [ { id: 1, name: "Tea", qty: 1 }, { id: 2, name: "Biscuits", qty: 2 }, ], }); add(index) { this.cart.set("items", index, "qty", (qty) => qty + 1); } render() { return html`
    ${For({ each: () => this.cart.state.items, children: (item, index) => html`
  • ${item.name}: ${() => this.cart.state.items[index()].qty}
  • `, })}

${() => this.cart.state.items.reduce((sum, item) => sum + item.qty, 0)} items in the cart

`; } } application(Cart).mount("#store-cart"); ``` ### produce() `produce()` lets you write a change as a mutation of a draft. ```js cart.set('items', produce((items) => { items.push({ id: 3, name: 'Jam', qty: 1 }); })); ``` --- ## Resource Source: https://signy.dev/documentation/1/reactivity/resource/ Async data with reactive loading and error state. ### Create a resource A resource runs a fetcher and exposes `value`, `loading` and `error`, all reactive. With a source function, it fetches again whenever the source changes. ```js const user = resource( () => this.userId.value, async (id) => (await fetch(`/api/users/${id}`)).json() ); user.value; // the data, or undefined user.loading; // true while fetching user.error; // the error of the last fetch user.refetch(); ``` `value` keeps the last data while a refetch runs and after a failed one, so a list does not flash empty. Read `loading` to show that newer data is on its way. Without a source, pass only the fetcher: `resource(() => fetchPosts())`. ### Example This fetcher waits half a second instead of calling a server. ```js const { Component, Signal, application, html, resource, Show } = Signy; const users = { 1: "Ada Lovelace", 2: "Grace Hopper", 3: "Alan Turing" }; const fetchUser = (id) => new Promise((resolve) => setTimeout(() => resolve({ id, name: users[id] }), 500)); class Profile extends Component { userId = new Signal(1); user = resource(() => this.userId.value, fetchUser); next() { this.userId.set((id) => (id % 3) + 1); } render() { return html` ${Show({ when: () => this.user.loading, fallback: html`

User: ${() => this.user.value?.name}

`, children: html`

Loading

`, })} `; } } application(Profile).mount("#resource-user"); ``` ### mutate() `mutate()` sets the value right away, without fetching. Use it for optimistic updates: show the change first, then roll it back if the server says no. ```js const previous = comments.value; comments.mutate((list) => [...list, draft]); try { await save(draft); } catch { comments.mutate(previous); } ``` ### Suspense Inside a `Suspense` boundary, pending resources and `lazy()` components show the boundary's fallback until they resolve. ```js Suspense({ fallback: html`

Loading

`, children: () => html`<${Dashboard} />`, }); ``` --- ## Components Source: https://signy.dev/documentation/1/components/components/ Classes with signals as fields and a render() that runs once. ### Define a component Extend `Component`, keep state in signals and return markup from `render()`. Methods are plain methods. ```js const { Component, Signal, application, html } = Signy; class Toggle extends Component { on = new Signal(false); toggle() { this.on.set((on) => !on); } render() { return html` `; } } application(Toggle).mount("#component-toggle"); ``` ### Mount Mount the root component through [`application()`](https://signy.dev/documentation/1/application/application/index.html.md). Do not call `mount()` on a component or create the root with `new` yourself. ```js application(Toggle).mount('#app'); ``` ### Nest components Use a component class as a tag inside `html`. ```js const { Component, application, html } = Signy; class Avatar extends Component { render() { return html`${this.props.initials}`; } } class Team extends Component { render() { return html`
<${Avatar} initials="AL" /> <${Avatar} initials="GH" /> <${Avatar} initials="AT" />
`; } } application(Team).mount("#component-nest"); ``` ### Reserved names `Component` already defines `id`, `name`, `props`, `app`, `parent`, `container`, `options`, `isMounted`, `shadowRoot` and the lifecycle methods. A field with one of those names fails or hides the original, so pick another, such as `userName`. ### setup() `setup()` runs once, before `render()`. Create effects, provide context and register lifecycle callbacks there. See [Lifecycle](https://signy.dev/documentation/1/components/lifecycle/index.html.md). --- ## Props and children Source: https://signy.dev/documentation/1/components/props-and-children/ Attributes in, nodes out. ### Props Attributes on a component tag arrive in `this.props`. Static props are plain values. ```js html`<${Greeting} name="Ada" />`; class Greeting extends Component<{ name: string }> { protected render() { return html`

Hello ${this.props.name}

`; } } ``` ### Props that change `render()` runs once, so a value read from props is read once. For a prop that changes, pass a function and call it inside a binding in the child. ```js const { Component, Signal, application, html } = Signy; class Meter extends Component { render() { return html` this.props.value()}>`; } } class Panel extends Component { level = new Signal(4); render() { return html` (this.level.value = Number(e.currentTarget.value))} /> <${Meter} value=${() => this.level.value} /> `; } } application(Panel).mount("#props-live"); ``` ### Children Content between the tags arrives as `this.props.children`. ```js const { Component, application, html } = Signy; class Card extends Component { render() { return html`
${this.props.title}
${this.props.children}
`; } } class Page extends Component { render() { return html` <${Card} title="Inbox">

3 unread messages

`; } } application(Page).mount("#props-children"); ``` ### Helpers - `mergeProps(defaults, props)` fills in defaults. - `splitProps(props, ['a', 'b'])` splits props in two, such as your own and the ones you pass on. --- ## Lifecycle Source: https://signy.dev/documentation/1/components/lifecycle/ setup, mount and unmount. ### Order 1. `setup()` runs once, before rendering. 2. `render()` runs once and returns the DOM. 3. The `onMount` callbacks run once the DOM is in the page. 4. On unmount, the cleanup returned by `onMount` and the `onUnmount` callbacks run, then everything the component owns is disposed. ### onMount() Register it in `setup()`. It may return a cleanup function, which runs on unmount. ```js const { Component, Signal, application, html } = Signy; class Clock extends Component { now = new Signal(new Date()); setup() { this.onMount(() => { const timer = setInterval(() => (this.now.value = new Date()), 1000); return () => clearInterval(timer); }); } render() { return html`

${() => this.now.value.toLocaleTimeString()}

`; } } application(Clock).mount("#lifecycle-clock"); ``` ### onUnmount() ```js protected setup(): void { const socket = new WebSocket(url); this.onUnmount(() => socket.close()); } ``` ### What you do not clean up Bindings, effects created in `setup()`, control flow branches and child components are owned by the component and disposed for you. Only clean up what Signy cannot see, such as timers, sockets and listeners on `window`. --- ## Refs and forms Source: https://signy.dev/documentation/1/components/refs-and-forms/ Read input in events, write to fields through refs. ### Events An attribute named `on` plus the event name adds a listener: `onClick`, `onInput`, `onSubmit`, `onKeyDown`. The name is lowercased, so `onDblClick` listens to `dblclick`. ### Refs `createRef()` gives an object whose `current` is the element after render. `ref` also takes a callback. ```js private input = createRef(); html``; html` this.draw(el)}>`; ``` ### Forms Keep state in signals and read the field in its event. To reset a field, set its `value` through a ref. Binding `value=${() => ...}` sets the attribute, which the browser ignores once someone has typed. ```js const { Component, Signal, application, html, createRef } = Signy; class Search extends Component { query = new Signal(""); input = createRef(); clear() { this.query.value = ""; this.input.current.value = ""; this.input.current.focus(); } render() { return html`
e.preventDefault()}> (this.query.value = e.currentTarget.value)} />

Searching for: ${() => this.query.value || "nothing yet"}

`; } } application(Search).mount("#forms-search"); ``` ### Attributes - A function binds the attribute: `disabled=${() => busy.value}`. - `true` sets an empty attribute and `false` removes it. For ARIA states, pass a string: `aria-expanded=${() => String(open.value)}`. - `style` takes a string or an object. --- ## DOM helpers Source: https://signy.dev/documentation/1/components/dom-helpers/ Listeners on window, document or any element, removed for you. Events on elements inside a template use `onClick` and friends. For targets outside it, such as `window`, `document` or a click anywhere on the page, use the `DOM` helpers. Called in `setup()`, the listener belongs to the component and is removed when it unmounts. ### DOM.on() ```js protected setup(): void { DOM.on(window, 'resize', () => (this.width.value = innerWidth)); } ``` It takes a target, an event name, a handler and the usual listener options, and returns a function that removes the listener early. The target can be a function, such as one that returns a ref's element. The listener then moves when the function returns another element. ### DOM.onKeydown() A keydown listener for one key or a list of keys, with optional modifiers. ```js DOM.onKeydown(document, 'Escape', () => this.close()); DOM.onKeydown(document, ['z', 'Z'], () => this.undo(), { ctrl: true, shift: false }); ``` `ctrl`, `alt`, `shift` and `meta` must match when set and are ignored when left out. ### DOM.onClickOutside() Runs the handler on a click anywhere except on the element or inside it. ```js const { Component, Signal, application, html, createRef, DOM, Show } = Signy; class Menu extends Component { open = new Signal(false); box = createRef(); setup() { DOM.onClickOutside(() => this.box.current, () => (this.open.value = false)); DOM.onKeydown(document, "Escape", () => (this.open.value = false)); } render() { return html`
${Show({ when: () => this.open.value, children: html`
  • Rename
  • Duplicate
  • Delete
`, })}

Open the menu, then click outside it or press Escape.

`; } } application(Menu).mount("#dom-dropdown"); ``` ### Other helpers - `DOM.query()` and `DOM.queryAll()` find elements, `DOM.getElement()` takes a selector or an element. - `DOM.addClass()`, `DOM.removeClass()`, `DOM.toggleClass()` and `DOM.hasClass()` change classes. - `DOM.setAttributes()` and `DOM.removeAttributes()` change several attributes at once. Each helper is also exported on its own, such as `createClickOutside` for `DOM.onClickOutside`. --- ## Context Source: https://signy.dev/documentation/1/components/context/ Share a value with every component below. ### Create a context `createContext(defaultValue, name)` returns a context. A component provides a value, and any component below it injects it. The default value is used when no component provides one. ```js const ThemeContext = createContext>(undefined, 'Theme'); ``` ### Provide and inject Provide in `setup()`. Provide a signal when the value changes. ```js const { Component, Signal, application, html, createContext } = Signy; const ThemeContext = createContext(undefined, "Theme"); class ThemeLabel extends Component { render() { const theme = ThemeContext.inject(this); return html`

The theme is ${() => theme.value}

`; } } class App extends Component { theme = new Signal("light"); setup() { ThemeContext.provide(this, this.theme); } render() { return html` <${ThemeLabel} /> `; } } application(App).mount("#context-theme"); ``` ### Required context `createRequiredContext(name)` has no default. Injecting it with no provider above throws, which catches a missing provider early. --- ## Shadow DOM Source: https://signy.dev/documentation/1/components/shadow-dom/ Scoped styles for one component. Set `shadow = true` and the component renders into its own shadow root. `styles` apply inside it only, and page styles do not leak in. ```js const { Component, application, html } = Signy; class Pill extends Component { shadow = true; styles = ` span { padding: 0.25rem 0.75rem; border-radius: 999px; color: #fff; background: linear-gradient(90deg, #c040f0, #7a1fc0); } `; render() { return html`Scoped styles`; } } application(Pill).mount("#shadow-badge"); ``` In TypeScript, declare both as `protected`. `styles` also takes an array of strings or of `CSSStyleSheet` objects, and `shadow` takes options such as `{ mode: 'closed' }`. --- ## Custom events and slots Source: https://signy.dev/documentation/1/components/events-and-slots/ Talk to the page around a Shadow DOM component. A Shadow DOM component mounted into an element of a page, such as an embeddable widget, cannot take props from that page. It talks back with custom events, and the page passes content in through slots. ### emit() `this.emit(name, detail)` dispatches a custom event from the component's host element. It bubbles and is `composed`, so it leaves the shadow root and the page can listen to it like any event. ```js // In the component this.emit('rate', { stars: 4 }); // On the page host.addEventListener('rate', (event) => console.log(event.detail.stars)); ``` Between Signy components without Shadow DOM, pass a callback prop instead, such as `onChange=${...}`. ### Slots Children of the host element in the page show where the component renders a ``. A child with `slot="name"` goes to ``. `getSlottedElements(name)` lists them, `hasSlottedContent(name)` checks for any, and `onSlotChange(name, callback)` follows changes. Call `onSlotChange()` in `setup()`, it is removed on unmount. ```js const { Component, Signal, application, html } = Signy; class Rating extends Component { shadow = true; styles = ` button { font-size: 1.5rem; border: 0; background: none; cursor: pointer; } ::slotted(*) { display: block; font-weight: 700; } `; stars = new Signal(0); rate(stars) { this.stars.value = stars; this.emit("rate", { stars }); } render() { return html`
${[1, 2, 3, 4, 5].map((n) => html` `)}
`; } } // The host's own children fill the slots const host = document.querySelector("#events-and-slots"); host.innerHTML = 'How was your coffee?'; const output = document.createElement("output"); host.after(output); host.addEventListener("rate", (event) => (output.textContent = `The page heard ${event.detail.stars} stars.`)); application(Rating).mount(host); ``` --- ## html templates Source: https://signy.dev/documentation/1/templates/html-templates/ Markup in a tagged template, turned into real DOM nodes. `html` is a tagged template built on [htm](https://github.com/developit/htm). It returns DOM nodes, not strings, and runs in the browser with no build step. ### Values ```js html`

${text}

` // text, set once html`

${() => count.value}

` // text, kept up to date html`

${html`nested`}

` // nodes ``` ### Attributes and events ```js html` (invalid.value ? 'invalid' : '')} disabled=${() => busy.value} onInput=${(e) => (name.value = e.currentTarget.value)} /> ` ``` A function value binds the attribute. An `on` attribute with a function adds a listener. `ref` captures the element, see [Refs and forms](https://signy.dev/documentation/1/components/refs-and-forms/index.html.md). ### Components A component class goes in the tag name position, closed with `` or self closed with `<${Card} />`. ### SVG Write SVG in a template as you would in HTML. `svg`, `circle`, `path` and the other SVG tags are created in the SVG namespace, and bindings work on their attributes. ```js const { Component, Signal, application, html } = Signy; class Dial extends Component { value = new Signal(60); render() { return html` 314 - (314 * this.value.value) / 100} transform="rotate(-90 60 60)" /> ${() => this.value.value}% (this.value.value = Number(e.currentTarget.value))} /> `; } } application(Dial).mount("#svg-example"); ``` `a`, `title`, `style` and `script` exist in HTML and SVG and are created as HTML. Inside an SVG, give them `xmlns="http://www.w3.org/2000/svg"`. ### Pitfalls - Close void elements: ``, `
`, ``. Without the slash, the parser treats the rest of the template as their children and rendering fails. - `${this.count.value}` renders once. Bind `${() => this.count.value}`. - A function child that returns markup, such as ``${() => ok ? html`...` : null}``, renders an empty text node. Use [`Show`](https://signy.dev/documentation/1/templates/show/index.html.md). - `.map()` over a signal array renders once. Use [`For`](https://signy.dev/documentation/1/templates/for-and-index/index.html.md). ### Hyperscript `h(tag, props, ...children)` is the function under `html`, for code that builds nodes without a template. --- ## Show Source: https://signy.dev/documentation/1/templates/show/ Render one branch or the other. `Show` takes an accessor in `when`. While it is truthy, the children render, otherwise the fallback. ```js const { Component, Signal, application, html, Show } = Signy; class Session extends Component { user = new Signal(null); render() { return html` ${Show({ when: () => this.user.value, fallback: html` `, children: (user) => html`

Hi ${user.name}

`, })} `; } } application(Session).mount("#show-login"); ``` ### Children as a function A function in `children` receives the truthy value. It runs once per branch, untracked: the branch is built again only when the truthy value changes, such as another object or falsy to truthy. Bind signals read inside it with `${() => ...}` as usual. ### As a tag ```js html` <${Show} when=${() => this.open.value}>

Now you see me

` ``` A node child is moved in and out, not rebuilt, so it keeps its listeners and bindings. --- ## For and Index Source: https://signy.dev/documentation/1/templates/for-and-index/ Lists that update row by row. ### For `For` renders one row per item. With `keyBy` or `key`, rows are reused and moved when the array changes. The item is a plain value and `index` is an accessor. ```js const { Component, Signal, application, html, For } = Signy; let nextId = 3; class Todos extends Component { todos = new Signal([ { id: 1, text: "Learn signals", done: new Signal(true) }, { id: 2, text: "Write a component", done: new Signal(false) }, ]); add(form) { const text = form.elements.text.value.trim(); if (!text) return; this.todos.set((list) => [...list, { id: nextId++, text, done: new Signal(false) }]); form.reset(); } remove(id) { this.todos.set((list) => list.filter((todo) => todo.id !== id)); } render() { return html`
(e.preventDefault(), this.add(e.currentTarget))}>
    ${For({ each: () => this.todos.value, keyBy: "id", fallback: html`
  • Nothing to do
  • `, children: (todo, index) => html`
  • `, })}
`; } } application(Todos).mount("#for-todos"); ``` A field that changes on an item with the same key does not render again. Keep such fields in a signal on the item, like `done` above, or use a [store](https://signy.dev/documentation/1/reactivity/store/index.html.md). ### Index `Index` is the other way round: the item is an accessor and the index a number. Rows stay in place and only their item changes. Use it for lists of primitives edited in place, and read `item()` inside a binding. ```js const { Component, Signal, application, html, Index } = Signy; class Scores extends Component { scores = new Signal([3, 7, 5]); bump(i) { this.scores.set((list) => list.map((score, j) => (j === i ? score + 1 : score))); } render() { return html`
    ${Index({ each: () => this.scores.value, children: (score, i) => html`
  1. ${() => score()} points
  2. `, })}
`; } } application(Scores).mount("#index-scores"); ``` --- ## Switch and Match Source: https://signy.dev/documentation/1/templates/switch-and-match/ One of several branches. `Switch` takes `Match` children and renders the first one whose `when` is truthy. The fallback renders when none match. ```js const { Component, Signal, application, html, Switch, Match } = Signy; class Upload extends Component { status = new Signal("idle"); render() { return html`
${Switch({ fallback: html`

Idle

`, children: [ Match({ when: () => this.status.value === "loading", children: () => html`

Uploading

` }), Match({ when: () => this.status.value === "done", children: () => html`

Uploaded

` }), Match({ when: () => this.status.value === "error", children: () => html`

Upload failed

` }), ], })} `; } } application(Upload).mount("#switch-status"); ``` Like `Show`, a function in `children` receives the truthy value of `when`. --- ## Portal and ErrorBoundary Source: https://signy.dev/documentation/1/templates/portal-and-error-boundary/ Render elsewhere, and recover from errors. ### Portal `Portal` moves its children into another element, `document.body` by default. It is useful for dialogs and toasts that must escape an `overflow` or a stacking context. The content is removed when its owner is disposed. ```js html` ${Portal({ children: html`
Saved
`, })} ` ``` Pass `mount` to pick the target element. ### ErrorBoundary `ErrorBoundary` catches errors thrown while its children render and shows the fallback. The fallback receives the error and a `reset` function that tries again. Errors in event handlers are not caught. ```js const { Component, Signal, application, html, ErrorBoundary } = Signy; let attempts = 0; class Flaky extends Component { render() { attempts++; if (attempts === 1) throw new Error("The first render failed"); return html`

Rendered on attempt ${attempts}

`; } } class App extends Component { render() { return html` ${ErrorBoundary({ fallback: (error, reset) => html`

${error.message}

`, children: () => html`<${Flaky} />`, })} `; } } application(App).mount("#error-boundary-example"); ``` --- ## Lazy and Suspense Source: https://signy.dev/documentation/1/templates/lazy-and-suspense/ Load components when they are needed. ### lazy() `lazy(importer)` takes a function that returns a module promise, usually a dynamic `import()`, and gives back a component that loads the first time it renders. The module's default export is a component class, or a function component that takes props and returns nodes. ```js // reports.js export default class Reports extends Component { /* ... */ } // app.js const LazyReports = lazy(() => import('./reports.js')); html`<${LazyReports} year=${2026} />`; ``` `minLoadTime` keeps the fallback up for at least that many milliseconds, so a fast load does not flash. ### Suspense A lazy component must render inside a `Suspense` boundary. The boundary shows its fallback until every lazy component and resource inside it is ready. Pass `children` as a function. ```js const { Component, Signal, application, html, lazy, Suspense, Show } = Signy; class Chart extends Component { render() { return html`

The chart module loaded.

`; } } // A real app would pass () => import("./chart.js") const LazyChart = lazy(() => new Promise((resolve) => setTimeout(() => resolve({ default: Chart }), 800)), ); class Report extends Component { open = new Signal(false); render() { return html` ${Show({ when: () => this.open.value, children: () => Suspense({ fallback: html`

Loading the chart

`, children: () => html`<${LazyChart} />`, }), })} `; } } application(Report).mount("#lazy-suspense"); ``` ### Preloading `LazyChart.preload()` starts loading without rendering, and `preloadAll([a, b])` loads several. Call them on hover or when the browser is idle, and the component renders at once when it is needed. ```js link.addEventListener('pointerenter', () => LazyChart.preload()); requestIdleCallback(() => preloadAll([LazyChart, LazyTable])); ``` Each lazy component also exposes its `state` and `error` as signals, and `loading()`. --- ## Routes Source: https://signy.dev/documentation/1/router/routes/ Map paths to components. ### Define routes `router()` takes a list of routes. Install it with `app.use()` before mounting, and render an `Outlet` where the page goes. ```js import { application, Component, html, router, Link, Outlet } from 'signy'; class Layout extends Component { protected render() { return html`
<${Outlet} />
`; } } const appRouter = router({ routes: [ { path: '/', component: HomePage }, { path: '/about', component: AboutPage }, { path: '/users/:id', component: UserPage }, { path: '*', component: NotFound }, ], }); application(Layout).use(appRouter).mount('#app'); ``` ### A live router This one uses memory history, so it does not change the address of this page. ```js const { Component, application, html, router, Link, Outlet } = Signy; class Home extends Component { render() { return html`

Welcome home.

`; } } class About extends Component { render() { return html`

Signy has a router built in.

`; } } class Layout extends Component { render() { return html` <${Outlet} /> `; } } const appRouter = router({ mode: "memory", routes: [ { path: "/", component: Home }, { path: "/about", component: About }, ], }); application(Layout).use(appRouter).mount("#router-basic"); ``` ### Paths - `/users/:id` captures a parameter. - `/users/:id?` makes it optional. - `*` matches anything, for a not found page. ### History modes `mode` is the browser history by default. `'hash'` keeps the route after a `#`, which works on any static host. `'memory'` keeps it in memory, for tests and widgets. ### Route options - `redirect` sends the route to another path. - `name` lets you navigate by name. - `meta` holds your own data, such as a page title. - `guards` and `loader` are covered in [Navigation](https://signy.dev/documentation/1/router/navigation/index.html.md) and [Params and query](https://signy.dev/documentation/1/router/params-and-query/index.html.md). --- ## Navigation Source: https://signy.dev/documentation/1/router/navigation/ Links, code and guards. ### Link `Link` renders an `a` that navigates without a page load. The active link gets `router-link-active`, and the exact match `router-link-exact-active`. ```js html`<${Link} to="/users/42">User 42`; html`<${Link} to=${{ path: '/search', query: { q: 'signals' } }}>Search`; ``` The exact match also gets `aria-current="page"`, so screen readers announce the current page and CSS such as Iris CSS can style it. `ariaCurrentValue` changes the value, such as `"step"` in a wizard. `replace` replaces the history entry, and `activeClass` and `exactActiveClass` rename the classes. `NavLink` also takes `isActive`, for your own matching. ### From code `useRouter(this)` returns the router. ```js class Logout extends Component { protected render() { const nav = useRouter(this); return html``; } } ``` The router also has `replace()`, `back()`, `forward()` and `go(delta)`. ### Guards A guard runs before every navigation, the first one when the app opens and the browser's back and forward buttons included. Return `false` to cancel it, a path to redirect, or nothing to go on. Add guards for every route with `beforeEach()`, or for one route with `guards`. When a guard cancels a back or forward move, the router steps back to the page on screen, so the page the user tried to open stays in the history. A guard that answers after the user has moved on is ignored. ```js appRouter.beforeEach(({ to }) => { if (to.meta.private && !session.value) return '/login'; }); appRouter.afterEach(() => window.scrollTo(0, 0)); ``` --- ## Params and query Source: https://signy.dev/documentation/1/router/params-and-query/ The current route, as signals. ### Params `useParams(this)`, `useQuery(this)` and `useRoute(this)` return signals, so bind them like any other. ```js const { Component, application, html, memoryRouter, Link, Outlet, useParams } = Signy; class User extends Component { render() { const params = useParams(this); return html`

Showing user ${() => params.value.id}

`; } } class Users extends Component { render() { return html` <${Outlet} /> `; } } const appRouter = memoryRouter({ initialEntries: ["/users/1"], routes: [{ path: "/users/:id", component: User }], }); application(Users).use(appRouter).mount("#router-params"); ``` ### Query ```js const query = useQuery(this); html`

Results for ${() => query.value.q}

`; ``` ### Loaders A route `loader` fetches data before the page shows. It gets the params, query, URL and an `AbortSignal`. Read the result with `useRouteData(this)`. ```js { path: '/users/:id', component: UserPage, loader: async ({ params, signal }) => (await fetch(`/api/users/${params.id}`, { signal })).json() } class UserPage extends Component { protected render() { const user = useRouteData(this); return html`

${() => user.value?.name}

`; } } ``` `useIsNavigating(this)` is true while a navigation and its loaders run. --- ## Nested and lazy routes Source: https://signy.dev/documentation/1/router/nested-and-lazy-routes/ Layouts inside layouts, and pages loaded on demand. ### Nested routes A route with `children` renders its component, and that component renders the matched child in its own `Outlet`. ```js const appRouter = router({ routes: [ { path: '/settings', component: SettingsLayout, children: [ { path: '', component: Profile }, { path: 'security', component: Security }, ], }, ], }); class SettingsLayout extends Component { protected render() { return html`
<${Outlet} />
`; } } ``` ### Lazy routes `lazyRoute()` loads a route's component the first time it is visited, so a bundler can split it into its own chunk. The module exports the component as `default`. ```js { path: '/reports', component: lazyRoute(() => import('./pages/reports')) } ``` `preloadRoutes()` loads lazy routes ahead of time, such as when the browser is idle. --- ## Application Source: https://signy.dev/documentation/1/application/application/ Create, configure, mount. ### Mount ```js const app = application(App); app.mount('#app'); ``` `mount()` takes a selector or an element, `#app` by default. `unmount()` disposes the whole tree. An app mounts once: create a new one to mount again. ### Plugins `use()` installs a plugin, such as the router, and returns the app, so calls chain. Install plugins before `mount()`. ```js application(App).use(appRouter).mount('#app'); ``` A plugin is a function that receives the app, or an object with an `install(app)` method. ```js const analytics = { install(app) { app.provide('analytics', createTracker()); }, }; ``` ### Several apps on a page Each `application()` is independent, so a page can mount several, as the examples on this site do. --- ## Testing Source: https://signy.dev/documentation/1/application/testing/ Synchronous checks with flushAll(). ### flushAll() Templates and control flow render synchronously, so the DOM is complete right after `mount()`. Updates after a signal write apply in a microtask. In a test, call `flushAll()` after a write to run them at once. ```js import { application, flushAll } from 'signy'; test('counts clicks', () => { const root = document.createElement('div'); const app = application(Counter).mount(root); root.querySelector('button').click(); flushAll(); expect(root.textContent).toContain('Clicked 1 times'); app.unmount(); }); ``` ### Async work For loaders, lazy imports and resources, wait for pending promises first, then call `flushAll()`. ```js await new Promise((resolve) => setTimeout(resolve)); flushAll(); ``` ### Router `memoryRouter()` keeps history in memory and takes the starting entries. ```js const testRouter = memoryRouter({ routes, initialEntries: ['/users/42'] }); application(Layout).use(testRouter).mount(root); ```