Lecture 19 — CS 12 (Computer Programming II)
Second Semester, AY 2024-2025
Department of Computer Science
University of the Philippines Diliman
Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture
Task: Create an MVU web app that has two textboxes A and B on screen with text saying Please enter numbers.
Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture
Task: Create an MVU web app that has the text (joke placeholder) and a button saying Fetch joke.
URL: https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,racist,sexist,explicit&format=txt
How do we trigger a call to update when fetch resolves without violating separation of concerns?
import { startModelCmd } from "cs12242-mvu/src"
// initModel: ModelCmd<Model, Msg>
// update: (msg: Msg, model: Model) => ModelCmd<Model, Msg>
startModelCmd(root, initModel, update, view)
export type Cmd<Msg> = {
sub: (dispatch: (msg: Msg) => void) => Promise<void>
}
export namespace Cmd {
export const ofSub = <Msg>(
sub: (dispatch: (msg: Msg) => void) => Promise<void>,
): Cmd<Msg> => ({
sub,
})
}
export type ModelCmd<Model, Msg> =
| Model
| {
model: Model
cmd: Cmd<Msg>
}
Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture
Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture
Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture
import { Array, Match, HashMap, Schema as S, pipe } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
nextCounterId: S.Int,
counters: S.HashMap({
key: S.Int,
value: S.Int,
}),
})
const initModel: Model = Model.make({
nextCounterId: 0,
counters: HashMap.empty(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgNewCounter", {}),
S.TaggedStruct("MsgIncrement", {
id: S.Number,
}),
S.TaggedStruct("MsgDecrement", {
id: S.Number,
}),
S.TaggedStruct("MsgReset", {
id: S.Number,
}),
S.TaggedStruct("MsgDelete", {
id: S.Number,
}),
)
const [
MsgNewCounter, //
MsgIncrement,
MsgDecrement,
MsgReset,
MsgDelete,
] = Msg.members
const update = (msg: Msg, model: Model): Model =>
Match.value(msg).pipe(
Match.tag("MsgNewCounter", () =>
Model.make({
nextCounterId: model.nextCounterId + 1,
counters: HashMap.set(model.counters, model.nextCounterId, 0),
}),
),
Match.tag("MsgIncrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) + 1),
),
}),
),
Match.tag("MsgDecrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) - 1),
),
}),
),
Match.tag("MsgReset", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.set(id, 0)),
}),
),
Match.tag("MsgDelete", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.remove(id)),
}),
),
Match.orElse(() => model),
(x) => {
console.log(JSON.stringify(x, null, 2))
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"button",
{
on: {
click: () => dispatch(MsgNewCounter.make()),
},
},
"New counter",
),
drawCounters(model.counters, dispatch),
])
const drawCounters = (
counters: HashMap.HashMap<number, number>,
dispatch: (msg: Msg) => void,
) => {
const x = pipe(
counters,
HashMap.map(drawCounter(dispatch)),
HashMap.values,
Array.fromIterable,
)
return h("div", x)
}
const drawCounter =
(dispatch: (msg: Msg) => void) => (counter: number, key: number) =>
h("div", [
h("h3", `Counter ${key}`),
h(
"button",
{
on: {
click: () =>
dispatch(
MsgIncrement.make({
id: key,
}),
),
},
},
"+",
),
h("p", `${counter}`),
h(
"button",
{
on: {
click: () =>
dispatch(
MsgDecrement.make({
id: key,
}),
),
},
},
"-",
),
h("div", [
h(
"button",
{
on: {
click: () =>
dispatch(
MsgReset.make({
id: key,
}),
),
},
},
"Reset",
),
]),
h("div", [
h(
"button",
{
on: {
click: () => dispatch(MsgDelete.make({ id: key })),
},
},
"Delete",
),
]),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
import { Match, Schema as S } from "effect"
import { Cmd, h, startModelCmd } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
joke: S.String,
isFetching: S.Boolean,
})
const initModel = Model.make({
joke: "",
isFetching: false,
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgFetchJoke", {}),
S.TaggedStruct("MsgGotJoke", {
joke: S.String,
}),
)
const [MsgFetchJoke, MsgGotJoke] = Msg.members
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgFetchJoke", () => ({
model: Model.make({
...model,
isFetching: true,
}),
cmd: Cmd.ofSub(async (dispatch: (msg: Msg) => void) => {
try {
const resp = await fetch(
"https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,racist,sexist,explicit&format=txt",
//"https://flksdajf;lkadsjfk;ldsajf;lkadsjfa",
)
const data = await resp.text()
dispatch(MsgGotJoke.make({ joke: data }))
} catch (e) {
console.log(`Error occurred: ${e}`)
}
}),
})),
Match.tag("MsgGotJoke", ({ joke }) =>
Model.make({
...model,
isFetching: false,
joke,
}),
),
Match.exhaustive,
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"p",
model.isFetching ? "Fetching joke..."
: model.joke !== "" ? model.joke
: "(joke placeholder)",
),
h(
"button",
{
on: {
click: () => dispatch(MsgFetchJoke.make()),
},
},
"Fetch joke",
),
])
const root = document.getElementById("app")!
startModelCmd(root, initModel, update, view)
import { Cmd, h, startModelCmd } from "cs12242-mvu/src"
import { Match, Schema as S } from "effect"
type Model = typeof Model.Type
const Model = S.Struct({
joke: S.String,
isFetching: S.Boolean,
error: S.String,
text: S.String,
})
const initModel = Model.make({
joke: "",
isFetching: false,
error: "",
text: "",
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgFetchJoke", {}),
S.TaggedStruct("MsgGotJoke", {
data: S.String,
}),
S.TaggedStruct("MsgError", {
error: S.String,
}),
S.TaggedStruct("MsgType", {
text: S.String,
}),
)
const [MsgFetchJoke, MsgGotJoke, MsgError, MsgType] = Msg.members
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgFetchJoke", () => {
return {
model: Model.make({
...model,
isFetching: true,
}),
cmd: Cmd.ofSub(async (dispatch: (msg: Msg) => void) => {
//const resp = await fetch(
//"https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,racist,sexist,explicit&format=txt",
//)
try {
const resp = await fetch("https://hellofasfkljads;lfjaslk")
const data = await resp.text()
dispatch(
MsgGotJoke.make({
data,
}),
)
} catch (e) {
dispatch(
MsgError.make({
error: `${e}`,
}),
)
}
}),
}
}),
Match.tag("MsgGotJoke", ({ data }) =>
Model.make({
...model,
joke: data,
isFetching: false,
}),
),
Match.tag("MsgError", ({ error }) =>
Model.make({
...model,
error,
}),
),
Match.tag("MsgType", ({ text }) =>
Model.make({
...model,
text,
}),
),
Match.exhaustive,
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"p",
model.error !== "" ? `${model.error}`
: model.isFetching ? "Fetching joke..."
: model.joke === "" ? "(joke placeholder)"
: model.joke,
),
h(
"button",
{
on: {
click: () => dispatch(MsgFetchJoke.make()),
},
},
"Fetch joke",
),
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgType.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h("h2", model.text),
])
const root = document.getElementById("app")!
startModelCmd(root, initModel, update, view)
import { Schema as S, Match, Option } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
text1: S.String,
text2: S.String,
sum: S.Option(S.Number),
})
const initModel = Model.make({
text1: "",
text2: "",
sum: Option.none(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgTyped1", {
text: S.String,
}),
S.TaggedStruct("MsgTyped2", {
text: S.String,
}),
)
const [
MsgTyped1, //
MsgTyped2,
] = Msg.members
const computeSum = (text1: string, text2: string) => {
const a = parseInt(text1)
const b = parseInt(text2)
const sum = a + b
return Number.isNaN(sum) ? Option.none() : Option.some(sum)
}
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgTyped1", ({ text }) =>
Model.make({
...model,
text1: text,
sum: computeSum(text, model.text2),
}),
),
Match.tag("MsgTyped2", ({ text }) =>
Model.make({
...model,
text2: text,
sum: computeSum(model.text1, text),
}),
),
Match.exhaustive,
(x) => {
console.log(JSON.stringify(msg))
console.log(JSON.stringify(x))
console.log()
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTyped1.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTyped2.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h(
"h2",
Match.value(model.sum).pipe(
Match.tag("Some", ({ value }) => `${value}`),
Match.tag("None", () => "sum should be here"),
Match.exhaustive,
),
),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
import { HashMap, Array, Match, pipe, Schema as S } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
nextCounterId: S.Number,
counters: S.HashMap({
key: S.Number,
value: S.Number,
}),
})
const initModel = Model.make({
nextCounterId: 0,
counters: HashMap.empty(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgNewCounter", {}),
S.TaggedStruct("MsgIncrement", {
id: S.Number,
}),
S.TaggedStruct("MsgDecrement", {
id: S.Number,
}),
S.TaggedStruct("MsgReset", {
id: S.Number,
}),
S.TaggedStruct("MsgDelete", {
id: S.Number,
}),
)
const [
MsgNewCounter, //
MsgIncrement,
MsgDecrement,
MsgReset,
MsgDelete,
] = Msg.members
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgNewCounter", () =>
Model.make({
...model,
nextCounterId: model.nextCounterId + 1,
counters: HashMap.set(model.counters, model.nextCounterId, 0),
}),
),
Match.tag("MsgIncrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) + 1),
),
}),
),
Match.tag("MsgDecrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) - 1),
),
}),
),
Match.tag("MsgReset", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.set(id, 0)),
}),
),
Match.tag("MsgDelete", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.remove(id)),
}),
),
Match.exhaustive,
(x) => {
console.log(JSON.stringify(msg))
console.log(JSON.stringify(x))
console.log()
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"button",
{
on: {
click: () => dispatch(MsgNewCounter.make()),
},
},
"New counter",
),
...makeCounters(model.counters, dispatch),
])
const makeCounters = (
counters: HashMap.HashMap<number, number>,
dispatch: (msg: Msg) => void,
) =>
pipe(
counters,
HashMap.map(makeCounter(dispatch)),
HashMap.values,
Array.fromIterable,
)
const makeCounter =
(dispatch: (msg: Msg) => void) => (counter: number, id: number) =>
h("div", [
h("h2", `Counter ${id}`),
h(
"button",
{
on: {
click: () =>
dispatch(
MsgIncrement.make({
id,
}),
),
},
},
`+`,
),
h("p", `${counter}`),
h(
"button",
{
on: {
click: () =>
dispatch(
MsgDecrement.make({
id,
}),
),
},
},
`-`,
),
h(
"button",
{
on: {
click: () => dispatch(MsgReset.make({ id })),
},
},
"Reset",
),
h(
"button",
{
on: {
click: () => dispatch(MsgDelete.make({ id })),
},
},
"Delete",
),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
import { Match, Schema as S, Option } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
text1: S.String,
text2: S.String,
sum: S.Option(S.Number),
})
const initModel = Model.make({
text1: "",
text2: "",
sum: Option.none(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgTyped1", {
text: S.String,
}),
S.TaggedStruct("MsgTyped2", {
text: S.String,
}),
)
const [
MsgTyped1, //
MsgTyped2,
] = Msg.members
const computeSum = (text1: string, text2: string) => {
const a = parseInt(text1)
const b = parseInt(text2)
const sum = a + b
return isNaN(sum) ? Option.none() : Option.some(sum)
}
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgTyped1", ({ text }) =>
Model.make({
...model,
text1: text,
sum: computeSum(text, model.text2),
}),
),
Match.tag("MsgTyped2", ({ text }) =>
Model.make({
...model,
text2: text,
sum: computeSum(model.text1, text),
}),
),
Match.exhaustive,
(x) => {
console.log(x)
console.log(msg)
console.log()
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTyped1.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTyped2.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h(
"p",
Match.value(model.sum).pipe(
Match.tag("Some", ({ value }) => `${value}`),
Match.tag("None", () => "Please enter numbers"),
Match.exhaustive,
),
),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
import { Match, Schema as S } from "effect"
import { Cmd, h, startModelCmd } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
joke: S.String,
isFetching: S.Boolean,
error: S.String,
})
const initModel = Model.make({
joke: "",
isFetching: false,
error: "",
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgFetchJoke", {}),
S.TaggedStruct("MsgGotJoke", {
joke: S.String,
}),
S.TaggedStruct("MsgError", {
error: S.String,
}),
)
const [MsgFetchJoke, MsgGotJoke, MsgError] = Msg.members
const update = (
msg: Msg,
model: Model,
): Model | { model: Model; cmd: Cmd<Msg> } =>
Match.value(msg).pipe(
Match.tag("MsgFetchJoke", () => ({
model: Model.make({
...model,
isFetching: true,
}),
cmd: Cmd.ofSub(async (dispatch: (msg: Msg) => void) => {
try {
const resp = await fetch(
//"https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,racist,sexist,explicit&format=txt",
"https://;alksjfl;ksdajf;ldaksfjasdlk;fjadsl;k",
)
const data = await resp.text()
dispatch(
MsgGotJoke.make({
joke: data,
}),
)
} catch (e) {
dispatch(MsgError.make({ error: `Error: ${e}` }))
}
}),
})),
Match.tag("MsgGotJoke", ({ joke }) =>
Model.make({
...model,
isFetching: false,
joke,
}),
),
Match.tag("MsgError", ({ error }) =>
Model.make({
...model,
error,
}),
),
Match.exhaustive,
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"p",
model.error !== "" ? model.error
: model.isFetching ? "Fetching joke..."
: model.joke !== "" ? model.joke
: "(joke placeholder)",
),
h(
"button",
{
on: {
click: () => dispatch(MsgFetchJoke.make()),
},
},
"Fetch joke",
),
])
const root = document.getElementById("app")!
startModelCmd(root, initModel, update, view)
import { HashMap, Array, Match, Schema as S, pipe } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
nextCounterId: S.Number,
counters: S.HashMap({
key: S.Number,
value: S.Number,
}),
})
const initModel = Model.make({
nextCounterId: 0,
counters: HashMap.empty(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgNewCounter", {}),
S.TaggedStruct("MsgIncrement", {
id: S.Number,
}),
S.TaggedStruct("MsgDecrement", {
id: S.Number,
}),
S.TaggedStruct("MsgReset", {
id: S.Number,
}),
S.TaggedStruct("MsgDelete", {
id: S.Number,
}),
)
const [
MsgNewCounter, //
MsgIncrement,
MsgDecrement,
MsgReset,
MsgDelete,
] = Msg.members
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgNewCounter", () =>
Model.make({
...model,
nextCounterId: model.nextCounterId + 1,
counters: HashMap.set(model.counters, model.nextCounterId, 0),
}),
),
Match.tag("MsgIncrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) + 1),
),
}),
),
Match.tag("MsgDecrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) - 1),
),
}),
),
Match.tag("MsgReset", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.set(id, 0)),
}),
),
Match.tag("MsgDelete", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.remove(id),
),
}),
),
Match.orElse(() => model),
(x) => {
console.log(JSON.stringify(msg))
console.log(JSON.stringify(x))
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"button",
{
on: {
click: () => dispatch(MsgNewCounter.make()),
},
},
"New counter",
),
showCounters(model.counters, dispatch),
])
const showCounters = (
counters: HashMap.HashMap<number, number>,
dispatch: (msg: Msg) => void,
) =>
h(
"div",
pipe(
HashMap.map(counters, showCounter(dispatch)),
HashMap.values,
Array.fromIterable,
),
)
const showCounter =
(dispatch: (msg: Msg) => void) => (counter: number, id: number) =>
h("div", [
h("h2", `Counter ${id}`),
h(
"button",
{
on: {
click: () => dispatch(MsgIncrement.make({ id })),
},
},
"+",
),
h("p", `${counter}`),
h(
"button",
{
on: {
click: () => dispatch(MsgDecrement.make({ id })),
},
},
"-",
),
h(
"button",
{
on: {
click: () => dispatch(MsgReset.make({ id })),
},
},
"Reset",
),
h("button", {
on: {
click: () => dispatch(MsgDelete.make({ id })),
},
}, "Delete"),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
import { Schema as S, Match, Option } from "effect"
import { startSimple, h } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
textA: S.String,
textB: S.String,
sum: S.Option(S.Number),
})
const initModel = Model.make({
textA: "",
textB: "",
sum: Option.none(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgTextA", {
text: S.String,
}),
S.TaggedStruct("MsgTextB", {
text: S.String,
}),
)
const [MsgTextA, MsgTextB] = Msg.members
const computeSum = (textA: string, textB: string) => {
const a = parseInt(textA)
const b = parseInt(textB)
const c = a + b
return isNaN(c) ? Option.none() : Option.some(c)
}
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgTextA", ({ text }) =>
Model.make({
...model,
textA: text,
sum: computeSum(text, model.textB),
}),
),
Match.tag("MsgTextB", ({ text }) =>
Model.make({
...model,
textB: text,
sum: computeSum(model.textA, text),
}),
),
Match.exhaustive,
(x) => {
console.log(JSON.stringify(msg))
console.log(JSON.stringify(x))
console.log()
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTextA.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h("input", {
type: "text",
on: {
input: (e) =>
dispatch(
MsgTextB.make({
text: (e.target as HTMLInputElement).value,
}),
),
},
}),
h(
"p",
Match.value(model.sum).pipe(
Match.tag("Some", ({ value }) => `${value}`),
Match.tag("None", () => "Please enter numbers"),
Match.exhaustive,
),
),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)
// Missing?
import { HashMap, Array, Match, Schema as S, pipe } from "effect"
import { h, startSimple } from "cs12242-mvu/src"
type Model = typeof Model.Type
const Model = S.Struct({
nextCounterId: S.Number,
counters: S.HashMap({
key: S.Number,
value: S.Number,
}),
})
const initModel = Model.make({
nextCounterId: 0,
counters: HashMap.empty(),
})
type Msg = typeof Msg.Type
const Msg = S.Union(
S.TaggedStruct("MsgNewCounter", {}),
S.TaggedStruct("MsgIncrement", {
id: S.Number,
}),
S.TaggedStruct("MsgDecrement", {
id: S.Number,
}),
S.TaggedStruct("MsgReset", {
id: S.Number,
}),
S.TaggedStruct("MsgDelete", {
id: S.Number,
}),
)
const [
MsgNewCounter, //
MsgIncrement,
MsgDecrement,
MsgReset,
MsgDelete,
] = Msg.members
const update = (msg: Msg, model: Model) =>
Match.value(msg).pipe(
Match.tag("MsgNewCounter", () =>
Model.make({
nextCounterId: model.nextCounterId + 1,
counters: pipe(model.counters, HashMap.set(model.nextCounterId, 0)),
}),
),
Match.tag("MsgIncrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) + 1),
),
}),
),
Match.tag("MsgDecrement", ({ id }) =>
Model.make({
...model,
counters: pipe(
model.counters,
HashMap.set(id, HashMap.unsafeGet(model.counters, id) - 1),
),
}),
),
Match.tag("MsgReset", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.set(id, 0)),
}),
),
Match.tag("MsgDelete", ({ id }) =>
Model.make({
...model,
counters: pipe(model.counters, HashMap.remove(id)),
}),
),
Match.orElse(() => model),
(x) => {
console.log(JSON.stringify(msg))
console.log(JSON.stringify(x))
return x
},
)
const view = (model: Model, dispatch: (msg: Msg) => void) =>
h("div", [
h(
"button",
{
on: {
click: () => dispatch(MsgNewCounter.make()),
},
},
"New counter",
),
showCounters(model.counters, dispatch),
])
const showCounters = (
counters: HashMap.HashMap<number, number>,
dispatch: (msg: Msg) => void,
) =>
h(
"div",
pipe(
HashMap.map(counters, showCounter(dispatch)),
HashMap.values,
Array.fromIterable,
),
)
const showCounter =
(dispatch: (msg: Msg) => void) => (counter: number, id: number) =>
h("div", [
h("h2", `Counter ${id}`),
h(
"button",
{
on: {
click: () => dispatch(MsgIncrement.make({ id })),
},
},
"+",
),
h("p", `${counter}`),
h(
"button",
{
on: {
click: () => dispatch(MsgDecrement.make({ id })),
},
},
"-",
),
h(
"button",
{
on: {
click: () => dispatch(MsgReset.make({ id })),
},
},
"Reset",
),
h(
"button",
{
on: {
click: () => dispatch(MsgDelete.make({ id })),
},
},
"Delete",
),
])
const root = document.getElementById("app")!
startSimple(root, initModel, update, view)