2. Interaction
Add delegated click events, conditional content, and a keyed list. Then bring shared state and its changes together in named PulseStore operations so you can trace how each interaction reaches the view.
Table of contents
Stage 2. Add interaction with PulseStore
- Starting state
- Complete Stage 1 so TaskboardApp is mounted with serializable TaskboardState data.
- Applied change
- Handle clicks through onclick, render details with v-if and tasks with v-for plus key, then replace the plain state instance with createTaskboardStore and apply changes through named pulses.
- Observable result
- Toggle details changes the v-if content, and Add task appends the next keyed v-for item when PulseStore delivers its debounced subscriber update.
- Next step
- Continue with Stage 3 and keep this store instance while adding routes.
Build interaction before shared state
- Delegated events
- onclick participates in the Valyrian.js delegated event flow. Here each handler calls a PulseStore operation, so PulseStore takes control of update timing for that event.
- v-if
- v-if includes the details paragraph only while showDetails is true.
- v-for and key
- v-for renders each task through its callback child, and key preserves each task's stable identity.
- Why PulseStore
- Taskboard now needs one shared state object and named operations that routing, data loading and forms can reuse in later stages.
- PulseStore update flow
- store.state is read-only outside the store. Each named operation receives a mutable working copy as its first argument. During a delegated event, PulseStore uses preventUpdate() to suppress the event runtime update and delivers the committed state to readers through a debounced subscriber pass.
src/app.tsx
tsximport type { Task, TaskboardStore } from "./state";
export function TaskboardApp({ store }: { store: TaskboardStore }) {
const state = store.state;
return (
<main>
<h1>Taskboard</h1>
<button onclick={() => store.toggleDetails()}>Toggle details</button>
<p v-if={state.showDetails}>Tasks stay in one shared PulseStore.</p>
<button
onclick={() =>
store.addTask({
id: state.nextId,
title: "Task " + state.nextId,
})
}
>
Add task
</button>
<ul v-for={state.tasks}>
{(task: Task) => <li key={task.id}>{task.title}</li>}
</ul>
</main>
);
}
src/client-entry.tsx
tsximport { mount } from "valyrian.js";
import { TaskboardApp } from "./app";
import { createTaskboardStore } from "./state";
const store = createTaskboardStore();
mount("body", <TaskboardApp store={store} />);
src/state.ts
tsimport { createPulseStore } from "valyrian.js/pulses";
export type Task = { id: number; title: string };
export type TaskboardState = {
tasks: Task[];
showDetails: boolean;
nextId: number;
loaded: boolean;
};
export function createTaskboardStore(
initial: Partial<TaskboardState> = {},
) {
return createPulseStore(
{
tasks: (initial.tasks ?? [{ id: 1, title: "Learn Valyrian.js" }]).map(
(task) => ({ ...task }),
),
showDetails: initial.showDetails ?? false,
nextId: initial.nextId ?? 2,
loaded: initial.loaded ?? false,
},
{
toggleDetails(state) {
state.showDetails = !state.showDetails;
},
addTask(state, task: Task) {
state.tasks.push(task);
state.nextId = Math.max(state.nextId, task.id + 1);
},
replaceTasks(state, tasks: Task[]) {
state.tasks = tasks;
state.loaded = true;
state.nextId = tasks.reduce(
(nextId, task) => Math.max(nextId, task.id + 1),
1,
);
},
},
);
}
export type TaskboardStore = ReturnType<typeof createTaskboardStore>;