# 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=${...}>...${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`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${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`3 unread
${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`Signed out
`, children: (user) => html`Hi ${user.name}
`, })}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${() => 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${() => 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 ComponentHello ${() => 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`Row bindings run after the first render: 0
| (this.isSelected(row.id).value ? "font-weight: 700" : "")}>${row.label} |
${() => 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()}"
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`${() => 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`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`3 unread messages
${Card}> `; } } 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 = createRefSearching 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`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 = createContextThe 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 `${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 `${Card}>` 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` (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: ``, `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
${Show}> ` ``` 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`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`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`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${Link}>`; html`<${Link} to=${{ path: '/search', query: { q: 'signals' } }}>Search${Link}>`; ``` 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