Skip to content

Model View Update (Elm Architecture)

Simon Fowler edited this page Jun 14, 2019 · 2 revisions

Model-View-Update

In spite of Links being a web language, its facilities for writing frontend web applications are dated. In particular, Links web programs have a rather imperative flavour, requiring explicit reading from- and writing to- the DOM.

In contrast, the Elm programming language pioneers the Model-View-Update pattern of web application development, which is particularly suited to functional programming languages. In a nutshell, the model is a representation of the application logic, which is rendered into an HTML DSL by the view function. The HTML produces messages which are handled by an update function, which in turn can update the model.

Consider a basic application consisting of a text box and a label, where the label should display the contents of the text box, but reversed.

In vanilla Links, we would write:

fun reverseString(str) {
  implode(reverse(explode(str)))
}

fun getProp(id, propName) { domGetPropertyFromRef(getNodeById(id), propName) }
fun setProp(id, propName, newVal) { domSetPropertyFromRef(getNodeById(id), propName, newVal) }

fun mainPage() {
  fun handleEvents() {
    receive {
      case UpdateLabel ->
        var value = getProp("toReverse", "value");
        setProp("label", "innerHTML", reverseString(value));
        handleEvents()
    }
  }

  var evtHandler = spawnClient { handleEvents() };

  page
    <html>
      <body>
        <form>
          <input type="text" id="toReverse"
            l:onkeyup="{evtHandler ! UpdateLabel}"></input>
        </form>
        <div id="label"></div>
      </body>
    </html>
}

fun main() {
  addRoute("/", fun(_) { mainPage() });
  servePages()
}

main()

The getProp and setProp functions access and mutate DOM properties respectively. We spawn a handler process, which receives messages from the DOM and performs the necessary mutations. The DOM is essentially one big chunk of mutable state.

With the new MVU library, we would write:

open import Mvu;
open import MvuAttrs;
open import MvuEvents;
open import MvuHTML;

fun reverseString(str) {
  implode(reverse(explode(str)))
}

typename Model = (contents: String);
typename Message = [| UpdateBox: String |];

fun updt(msg, model) {
  switch (msg) { case UpdateBox(newStr) -> (contents = newStr) }
}

var ae = MvuAttrs.empty;
var he = MvuHTML.empty;

fun view(model) {
  div(ae,
    form(ae,
      input(type("text") +@
            onKeyUp(fun(str) { UpdateBox(str) }), he)) +*
      div(ae, textNode(reverseString(model.contents)))
  )
}

fun mainPage() {
  runSimple("placeholder", (contents=""), view, updt);
  page
    <html><body><div id="placeholder"></div></body></html>
}

fun main() {
  addRoute("/", fun(_) { mainPage() });
  servePages()
}

main()

We define a Model type, which is a record with a single field containing the contents of the text box. We also define a Message type, which is fired whenever the user types a key, and contains the new value of the text box.

The updt function updates the model based on the message, and the view function takes the current model and generates the new HTML to display (including the new label value). The framework then uses a diffing algorithm to efficiently propagate changes to the DOM.

As described in the example, the key concept is the model-view-update loop:

  • Model: a representation of the program state
  • View: a function rendering the model as HTML, which can in turn produce messages in response to events
  • Update: a function taking a model and a message, and producing a new model

Subscriptions allow the program to react to events from outside the DOM, such as mouse movements, key presses, and animation frames. See examples/mvu/mousetest.links for an example.

HTML tags, attributes, and subscriptions are all monoidal, for ease of composition.

The framework is split into a Links part and a JS part.

The Links libraries can be found in lib/stdlib:

  • mvu.links: contains the FFI bindings to JS, and the main event loops.
  • mvuAttrs.links: monoidal representation of DOM attributes and event handlers
  • mvuEvents.links: event handler representation, used by mvuAttrs and mvuSubscriptions
  • mvuHTML.links: monoidal HTML DSL
  • mvuSubscriptions.links: monoidal subscriptions, including facilities for intervals and animation frames

The JS part consists of vdom.js and virtual-dom.js and can be found in lib/js.

virtual-dom.js is a compiled version of Matt Esch's excellent virtual-dom library (https://github.com/Matt-Esch/virtual-dom).

vdom.js is the main JS runtime for the framework, and is invoked via the FFI. It contains functions to evaluate monoidal elements, attributes, and subscriptions, to set up the appropriate event listeners, to generate the representation required by virtual-dom, and to ensure that messages are dispatched back into the event loop.

There are some example applications in the examples/mvu directory:

  • mousetest.links: showcases subscriptions using mouse events
  • keypress.links: showcases subscriptions using keyboard events
  • time.links: a stopwatch application, showing the use of subscriptions for timing intervals and key presses
  • pong.links: an implementation of Pong (!)
  • todomvc/todoMVC.links: an implementation of TodoMVC

Status

The MVU system was the result of @Buroni 's internship way back in time. I've been using this for my GtoPdb clone.