The application shows a list of timers: "Add a timer" adds one, and "Remove" takes one away. Each timer is the
program of step 2, unchanged: any program can be a child as it is. Its code is
examples/timer/timers.ts.
The parent keeps each timer's state with its id, and the next id to give. Ids are never reused, so that a removed timer's id never comes back to another one (see children by id). Adding and removing are the parent's own messages:
/** Ids are numbers, never reused, so they compare by value and a removed timer's id never comes back. */
export type Entry = Readonly<{ id: number; timer: Timer.State }>
export type State = Readonly<{ nextId: number; entries: ReadonlyArray<Entry> }>
type AddClicked = Readonly<{ _tag: 'AddClicked' }>
type RemoveClicked = Readonly<{ _tag: 'RemoveClicked'; id: number }>
parent<{ state }>() gives the functions that declare children. each declares children by id: where each one
lives in the parent's state (get and set, by id), and which of them may listen (subscribed). The library does
the routing (see composition):
const { compose, each } = parent<{ state: State }>()
export const timers = compose({
init: (children) => {
const first = children.Timer.init(1)
return { state: { nextId: 2, entries: [{ id: 1, timer: first.state }] }, commands: first.commands }
},
children: {
Timer: each(Timer.timer, {
// A removed timer is `None`: a click on a frame drawn before its removal is rejected.
get: (state, id: number) =>
Opt.map(Arr.findFirst(state.entries, (entry) => entry.id === id), (entry) => entry.timer),
set: (state, id, timer) => ({
...state,
entries: Arr.map(state.entries, (entry) => entry.id === id ? { id, timer } : entry),
}),
// Every timer may listen: each one's own `subscriptions` say when it ticks.
subscribed: (state) => Arr.map(state.entries, ({ id, timer }) => [id, timer] as const),
}),
},
update: (message: AddClicked | RemoveClicked, children) => (state) => {
switch (message._tag) {
case 'AddClicked': {
const timer = children.Timer.init(state.nextId)
return {
state: { nextId: state.nextId + 1, entries: [...state.entries, { id: state.nextId, timer: timer.state }] },
commands: timer.commands,
}
}
// A second click on "Remove", from the same frame, finds the timer gone.
case 'RemoveClicked':
return Arr.some(state.entries, (entry) => entry.id === message.id)
? { state: { ...state, entries: Arr.filter(state.entries, (entry) => entry.id !== message.id) } }
: reject
}
},
makeModel: (state, children) => ({
add: { _tag: 'AddClicked' } as AddClicked,
timers: Arr.map(state.entries, ({ id }) => ({
id,
model: children.Timer(state, id),
remove: { _tag: 'RemoveClicked', id } as RemoveClicked,
})),
}),
})
export type Model = ModelOf<typeof timers>
/** The list's messages, made by the library: its own, and `{ _tag: 'Timer', id, message }`. */
export type Message = MessageOf<typeof timers>
What the parent gets from the declaration:
children.Timer.init(id) is a timer's first step, its state and its commands; the
parent's own init is a function of children for this.{ _tag: 'Timer', id, message } for each timer's. MessageOf names them.{ _tag: 'Timer', id, command }, run by the timer's runCommand.'ticks', under ['Timer', id, 'ticks'].children.Timer(state, id) in makeModel, the same object while the timer's state
is.A timer that is not there is None in get, so a click on a removed timer, from a frame drawn before, is
rejected. A parent never reads a child's state to decide: the parent here only adds and removes (see
splitting a program).
Each timer is drawn by timerView with a dispatch of its own, from dispatchTo, which wraps the timer's messages
as the parent receives them. It is the same function in every frame, so snabbdom's thunk draws again only the
timer whose model changed (see children's views):
// A timer's model is the same object while its state is, and `dispatchTo` gives it the same dispatch in every
// frame: `thunk` draws again only the timer that changed.
export const view: View<Timers.Model, Timers.Message, VNode> = (model, dispatch) =>
h('main', [
h('button', { on: onClick(model.add, dispatch) }, 'Add a timer'),
h(
'ul',
model.timers.map(({ id, model, remove }) =>
h('li', { key: id }, [
thunk('div.timer', String(id), timerView, [model, dispatchTo(dispatch, 'Timer', id)]),
h('button.secondary', { on: onClick(remove, dispatch) }, 'Remove'),
])
),
),
])
examples/timer/main.ts runs the list
in the page, with a bell that beeps. The services of every command are provided around run:
// A short beep, made with the Web Audio API.
const bell = Bell.of({
ring: Effect.sync(() => {
const audio = new AudioContext()
const tone = audio.createOscillator()
tone.connect(audio.destination)
tone.start()
tone.stop(audio.currentTime + 0.3)
tone.onended = () => void audio.close()
}),
})
BrowserRuntime.runMain(run(timers, renderInto(root, view)).pipe(Effect.provideService(Bell, bell)))
Next, testing it.