Browse learning paths

Taskboard 2. Interaction

Add delegated click events, conditional content and a keyed list. Move shared Taskboard state behind named PulseStore operations and observe its debounced delivery to the mounted 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

tsx
import 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

tsx
import { mount } from "valyrian.js";
import { TaskboardApp } from "./app";
import { createTaskboardStore } from "./state";

const store = createTaskboardStore();
mount("body", <TaskboardApp store={store} />);

src/state.ts

ts
import { 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>;