The first program sets how many minutes a timer lasts: "+" adds a minute, "−" takes one away, never below one.
Its code is examples/timer/duration.ts.
The state is everything the program knows, and a message is something that happened to it, here a click on either button:
export type State = Readonly<{ minutes: number }>
export type Message = 'More' | 'Less'
The state is always an object, { minutes } rather than a number, so that any program can later become a child
as it is (see anatomy of a program).
update takes a message, then the state, and returns the next state, as { state }. When a message does not
apply in the state, it returns reject instead: "−" at one minute.
export const update: Update<State, Message> = (message) => ({ minutes }) => {
if (message === 'More') return { state: { minutes: minutes + 1 } }
if (minutes === 1) return reject
return { state: { minutes: minutes - 1 } }
}
update is pure: it never runs an effect and never changes the state it is given. The run ignores a rejected
message and keeps the state as it was, so a click that comes late, from a screen drawn before, is harmless (see
rejections and defects).
The view does not read the state. It draws a model that makeModel makes from it, with the messages its
controls send. At one minute the model has no less: the view cannot offer what update rejects.
export type Model = Readonly<{
minutes: number
more: Message
less: Opt.Option<Message>
}>
export const duration: Program<{ state: State; model: Model; message: Message }> = {
init: { state: { minutes: 25 } },
update,
makeModel: ({ minutes }) => ({
minutes,
more: 'More',
less: minutes > 1 ? Opt.some('Less') : Opt.none(),
}),
}
A program is a plain object, and its type names its types: Program<{ state; model; message }>. init is the
state it starts from (see model).
A view is (model, dispatch) => node. It draws the model and sends what the model holds through dispatch; it
decides nothing on its own:
// snabbdom's props module never removes a prop, so both cases of "−" set `disabled`.
export const durationView: View<Duration.Model, Duration.Message, VNode> = (model, dispatch) =>
h('main.timer', [
Opt.match(model.less, {
onNone: () => h('button', { props: { disabled: true } }, '−'),
onSome: (less) => h('button', { props: { disabled: false }, on: onClick(less, dispatch) }, '−'),
}),
h('output', `${model.minutes} min`),
h('button', { on: onClick(model.more, dispatch) }, '+'),
])
run(duration, renderInto(root, durationView)) starts the program and hands the renderer a stream of frames,
each a model and the dispatch to send its messages with. renderInto draws each frame into the element root.
In a browser, the run is started with BrowserRuntime.runMain from @effect/platform-browser, as
step 3 does, so that a run that dies is reported (see
running).
Next, the timer itself.