Model-View-Update II

Lecture 19 — CS 12 (Computer Programming II)
Second Semester, AY 2024-2025
Department of Computer Science
University of the Philippines Diliman

Lecture 19 outline

Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture

Motivating example

Task: Create an MVU web app that has two textboxes A and B on screen with text saying Please enter numbers.
  • Whenever text is entered into either A or B:
    • If the current text in both A and B can be parsed into integers by parseInt:
      • Sum their integer equivalents and replace the text on screen with the sum
    • Else:
      • Show Please enter numbers on screen

Lecture 19 outline

Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture

Motivating example

Task: Create an MVU web app that has the text (joke placeholder) and a button saying Fetch joke.
  • When the button is pressed:
    • The text on screen is replaced with Fetching joke…
    • The joke from the URL below is fetched
    • If fetching the joke is successful:
      • The joke should be displayed in place of Fetching joke…
    • Else:
      • The exception should be shown on screen
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?

ModelCmd

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>
    }

Lecture 19 outline

Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture

Example

  • New counter button: Creates new counter
    • Counters must be independent of each other

  • For each counter:
    • Count text: Shows current count
    • + button: Increments count by 1
    • - button: Decrements count by 1
    • Reset button: Sets count to 0
    • Delete button: Removes counter

Lecture 19 outline

Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture

Multiplication game

  • Two players
    • Player 1 first; alternating turns

  • $k = 1$ initially
  • Set $n$ to integer within $[100, 10000]$

  • During each turn:
    • Current player clicks a button
    • Integer of button gets multiplied to $k$
    • Product is assigned to $k$
    • If $k \ge n$, current player wins
  • When game is ongoing, screen should show:
    • Current player, $k$, $n$
    • One button for integers from 2 to 9
    • History of player choices made so far

  • When a player wins:
    • Remove all elements on screen
    • Show winning player
    • Add a Play again button
      • Starts a new game
        • Data should be cleared
        • $n$ should be randomized again

Lecture 19 outline

Dispatching on text input
Asynchronous operations
Dynamic DOM updates
Practice
Drawings and code from lecture

Code from HRU (8:30 AM) session

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)

Code from HRU (8:30 AM) session

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)

Code from HUV (10 AM) session

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)

Code from HUV (10 AM) session

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)

Code from HUV (10 AM) session

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)

Code from HWX-1 (1 PM) session

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)

Code from HWX-1 (1 PM) session

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)

Code from HWX-1 (1 PM) session

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)

Code from HYZ (4 PM) session

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)

Code from HYZ (4 PM) session

// Missing?

Code from HYZ (4 PM) session

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)

Drawing from lecture


Drawing from lecture


Drawing from lecture


Drawing from lecture


Drawing from lecture