Defining views

In the application record

type Application model message = {
      init :: model,
      view :: model -> Html message,
      update :: model -> message -> model,
      subscribe :: Array (Subscription message)
}

view maps the current state to markup. Whenever the model is updated, Flame patches the DOM by calling the view function with the new state.

A custom DSL, defined by the type Html, is used to write markup. You will likely need to qualify imports, e.g., prefix HE for HTML elements and HA for HTML attributes and events

import Flame.Html.Element as HE
import Flame.Html.Attribute as HA

Attributes and events

The module Flame.Html.Attribute exports

See the API reference for a complete list of attributes. In the case you need to define your own attributes/events, Flame provides the combinators

HA.createAttibute
HA.createProperty --for DOM properties
HA.createEvent
HA.createRawEvent

Elements

The module Flame.Html.Element exports HTML elements, such as div, body, etc, following the convention

HE.div [HA.id "my-div"] [HE.text "text content"] -- renders <div id="my-div">text content</div>
HE.div_ [HE.text "text content"] -- renders <div>text content</div>
HE.div' [HA.id "my-div"] -- renders <div id="my-div"></div>

(a few elements that usually have no children like br or input have element behave as element')

Attributes and children elements are passed as arrays

HE.div [HA.id "my-div", HA.disabled False, HA.title "div title"] [
      HE.span' [HA.id "special-span"],
      HE.br,
      HE.span_ [HE.text "I am regular"],
]
{- renders
<div id="my-div" title="div title">
      <span id="special-span"></span>
      <br>
      <span>I am regular</span>
</div>
-}

But for some common cases, the markup DSL also defines convenience type classes so we can write

Flame also offers a few special elements for cases where finer control is necessary

HE.managed takes user supplied functions to manipulate an element’s DOM node

type NodeRenderer arg = {
      createNode :: arg -> Effect Node,
      updateNode :: Node -> arg -> arg -> Effect Node
}

managed :: forall arg nd message. ToNode nd message NodeData => NodeRenderer arg -> nd -> arg -> Html message

On rendering, Flame calls createNode only once and from then on updateNode. These functions can check on their local state arg to decide whether/how to change a DOM node. For easy of use, the elements attributes and events are still automatically patched – otherwise, HE.managed_ should be used

managed_ :: forall arg message. NodeRenderer arg -> arg -> Html message

Lazy elements are only re-rendered if their local state arg changes

lazy :: forall arg message. Maybe Key -> (arg -> Html message) -> arg -> Html message

This is useful to avoid recomputing potentially expensive views such as large lists.

Fragments are wrappers

fragment :: forall children message. ToNode children message Html => children -> Html message

meaning that only their children elements will be rendered to the DOM. Fragments are useful in cases where having an extra parent element is unnecessary, or wherever DocumentFragment could be used.

See the API reference for a complete list of elements. In the case you need to define your own elements, Flame provides a few combinators as well

HE.createElement
HE.createElement_
HE.createElement'
HE.createEmptyElement

Combining attributes and events

For most attributes, later declarations overwrite previous ones

HE.div' [HA.title "title", HA.title "title 2"]
{- renders
<div title="title 2">
</div>
-}
HE.input [HA.type' "input", HA.value "test", HA.value "not a test!"]
{- renders
<input type="text">
with value set to "not a test!"
-}

However, classes, inline styles and events behave differently:

All uses of HE.class' on a single element are merged

HE.div' [HA.class' "a b", HA.class' { c: true }]
{- renders
<div class="a b c">
</div>
-}

So is HE.style

HE.div' [HA.style { display: "flex", color: "red" }, HA.style { order: "1" }]
{- renders
<div style="display: flex; color: red; order:1">
</div>
-}

Different messages for the same event on a single element are raised in the order they were declared. For example, clicking on a div similar to

HE.div' [HA.onClick Message1, HA.onClick Message2]

will result on the update function being called with Message1 and after once again with Message2.

View logic

A view is just a regular PureScript function: we can compose it or pass it around as any other value. For example, we can use the model in attributes

type Model = {
      done :: Int,
      enabled :: Boolean
}

data Message = Do

view :: Model -> Html Message
view model = HE.div [HA.class' { usefulClass: model.enabled }] $ HE.input [HA.type' "button", HA.value "Do thing number " <> show $ model.done, HA.onClick Do]

or to selective alter the markup

type Name = String

type Model = Maybe Name

data Message = Update Name | Greet

view :: Model -> Html Message
view = case _ of
      Nothing -> HE.div_ [
            HE.input [HA.type' "text", HA.onInput Update],
            HE.input [HA.type' "button", HA.value "Greet!", HA.onClick Greet]
      ]
      Just name -> "Greetings, " <> name <> "!"

as well create “partial views” without the need for any special syntax

header :: forall model message. model -> Html message
header = ...

footer :: forall model message. model -> Html message
footer = ...

view :: Model -> Html Message
view model = HE.content' [
      header,
      ...
      footer
]

Html is also a Functor so mapping messages works as expected. For instance, the counters example markup is a list of counters

view :: Model -> Html Message
view model = HE.main "main" [
      HE.button [HA.onClick Add] "Add",
      HE.div_ $ DA.mapWithIndex viewCounter model
]
      where viewCounter index model' = HE.div [HA.style { display: "flex" }] [
                  CounterMessage index <$> ECM.view model',
                  HE.button [HA.onClick $ Remove index] "Remove"
            ]

Next: Handling events