@kontsedal/olas-svelte
The Svelte adapter for Olas. An Olas ReadSignal already satisfies Svelte's store contract: its subscribe(run) calls run with the current value at once and on every change, and returns the unsubscribe. So $count works on a signal with no wrapper. This package adds the root context and one store per multi-signal object: a query, an infinite query, a field and a mutation.
Install
pnpm add @kontsedal/olas-svelte @kontsedal/olas-core @preact/signals-core sveltesvelte >= 4 is a peer dependency. The tests run on Svelte 5, and the examples below use Svelte 5 syntax: on Svelte 4, write on:click where they write onclick.
Example
// root.ts: the controller knows nothing about Svelte
import { createRoot, defineController, signal } from '@kontsedal/olas-core'
export const root = createRoot(
defineController(() => {
const count = signal(0)
return { count, inc: () => count.set(count.peek() + 1) }
}),
{ deps: {} },
)
// Register the root's type once, so `getRoot()` needs no type argument.
declare module '@kontsedal/olas-svelte' {
interface Register {
root: typeof root
}
}<!-- App.svelte: provide the root to the tree below -->
<script lang="ts">
import { setRoot } from '@kontsedal/olas-svelte'
import Counter from './Counter.svelte'
import { root } from './root'
setRoot(root)
</script>
<Counter /><!-- Counter.svelte: a signal is a store -->
<script lang="ts">
import { getRoot } from '@kontsedal/olas-svelte'
const api = getRoot()
const count = api.count
</script>
<button onclick={api.inc}>{$count}</button>API
| Export | Purpose |
|---|---|
setRoot(root) | Provide a root to the component tree below. Call it while a component initializes. |
getRoot<Api>() | The nearest root's api. Register types it; a type argument is an unchecked cast. Throws when no ancestor called setRoot. |
queryStore(subscription) | One store over a query's state: $q.data, $q.isLoading and the rest update together. Carries refetch, reset and cancel. |
infiniteQueryStore(subscription) | The same, plus pages, flat and the paging flags, and fetchNextPage / fetchPreviousPage. |
fieldStore(field) | One store over a field's value and validation state, plus the field's actions. Binds as bind:value={$state.value}. |
mutationStore(mutation) | One store over a mutation's state, plus mutate, run and reset. |
Register and RegisteredApi type getRoot(). Each store's type is exported: QueryStore, InfiniteQueryStore, FieldStore and MutationStore, with the matching …State.
How it behaves
- A field binds as it is.
Fieldhasset, which makes it a writable store, so<input bind:value={$name} />writes throughfield.set. Reach forfieldStorewhen the input also shows errors, and bind its member:<input bind:value={$state.value} />. Svelte writes a member binding by assigning the member on the value it holds and passing that whole value toset.fieldStorehands each subscriber its own copy of the state, and itssetwrites a copy'svalueto the field. - An object-valued field binds by member. Bind
<input bind:value={$person.value.first} />throughfieldStore, or<input bind:value={$person.first} />on the raw field. Svelte assigns the member in place and passes the object back toset. That counts as a change even when it is the object the field holds, so validators run andisDirtyfollows, at any depth. The field keeps its baseline as a copy of its own, soreset()restores the value from before the edit.fieldStorehands Svelte a shallow copy, so a top-level member lands on the copy. A deeper member, or any member on a raw field, lands on the object the field holds. When that object is shared, say query data seated through a form'sinitial, bind aForm's leaf fields instead. - Bind a form's leaf fields, not members of
$form. AFormand aFieldArrayhavesettoo, sobind:value={$form.name}compiles. Svelte then assignsnameon the form's current value object in place before it callsform.set. The form ends right, but a value object you held earlier, such as a last-saved snapshot, changes under you. Bind the field:<input bind:value={$name} />withconst name = form.fields.name. - One store per object, one update per change.
queryStorederives its value from every signal on the query, so abatchof writes reaches the component once. - Svelte manages the subscriptions. A
$storeread subscribes when the component mounts and unsubscribes when it is destroyed. mutateis fire-and-forget. It returns nothing, and a failure lands on$save.errorand$save.status. The adapter catches the rejection, so it does not become an unhandled one.runreturns the promise, and the caller owns its rejection.
Testing
A controller tests without Svelte, through createTestController from @kontsedal/olas-core/testing. Component tests need the Svelte compiler plugin and the browser resolve condition, or svelte resolves to its server build and mount throws. The root vitest.config.ts runs them as their own project for that reason. packages/integration/tests/adapter-parity/ runs the same scenarios through this adapter, React, preact/compat and Vue, and asserts the same DOM.