Skip to content

Instantly share code, notes, and snippets.

@JordanMartinez
Last active March 20, 2020 02:00
Show Gist options
  • Save JordanMartinez/67160b479ef02c4c83ce4f5f07e7a599 to your computer and use it in GitHub Desktop.
Save JordanMartinez/67160b479ef02c4c83ce4f5f07e7a599 to your computer and use it in GitHub Desktop.
How to Wrap HTML elements using Halogen's HTML DSL

This has come up enough that I'm writing a guide for someone else to create these things. If you have a need, then you're likely motivated enough to get this going. This stuff is straight forward and can be inferred by looking at the source code. It is, however, a bit tedious.

How to Wrap HTML in Halogen's HTML DSL

HTML has 3 parts to it: the tag, the tag's attributes/properties/events, and whether or not it has children. In other words:

<elementName attribute="value" property="value" onevent="doStuff();">
  <child></child>
  <child></child>
</elementName>

Outline of guide:

  1. Show how to wrap HTML attributes/properties
  2. Show how to wrap HTML events
  3. Combine attributes, properties, and events into 1 row
  4. Show how to wrap HTML elements using these above
  5. Applying this explanation to SVG.

Wrapping HTML Attributes and Properties

For context, see this explanation on the difference between attributes and properties.

Looking at Halogen's source code, we can see how to write them:

In other words, we follow this pattern:

-- Attribute
attributeName :: forall r i. String -> IProp (attributeName :: AttributeType | r) i
attributeName = attr (AttrName "attributeName")

-- Property
propertyName :: forall r i. Boolean -> IProp (propertyName :: PropertyType | r) i
propertyName = prop (PropName "propertyName")

Note: I have no idea what the difference is between attr and attrNS. Someone more familiar with HTML likely does?

Wrapping HTML Events

Events aren't much trickier. When the event in question does not have a type for the event, you define a plain event handler. When it does have an event type, you define a type-specific event handler.

In other words, it follows this pattern:

onPlainEvent :: forall r i. (Event -> Maybe i) -> IProp (onPlainEvent :: Event | r) i
onPlainEvent = handler ET.plainEvent

specificTypeHandler :: forall i. (SpecificTypeEvent -> Maybe i) -> Event -> Maybe i
specificTypeHandler = unsafeCoerce

onSpecificTypeEvent :: forall r i. (Event -> Maybe i) -> IProp (onPlainEvent :: Event | r) i
onSpecificTypeEvent = handler SpecificType.event <<< specificTypeHandler

Combining Attributes, Properties, and Events

There are some attributes/properties/events that all HTML elements can handle. There are others that most can handle. Then there are a few events that are specific to those particular HTML elements. So that we don't duplicate ourselves, purescript-dom-indexed uses type aliases and open rows to define an event handler property once and reuse it in multiple places. For example:

Finally, we define a closed row of attributes/properties/events for a given HTML element by closing one of these foundational open rows with the ones specific to that element. For example, the HTMLa row.

Wrapping HTML Elements

HTML elements either have children (e.g. <p>) or don't (e.g. <br />). Those that have children have the Node type. Those that don't have the Leaf type:

Everything follows this pattern:

-- <elementName>children</elementName>
elementName :: forall w i. Node HTMLelementName w i
elementName = element (ElemName "elementName") 

-- <elementName />
elementName :: forall w i. Leaf HTMLelementName w i
elementName props = element (ElemName "elementName") props []

-- where `HTMLelementName`
-- refers to the closed row that stores all 
-- attributes/properties/events for that HTML element

Wrapping SVG

If you look at a file in the purescript-ocelot library that uses SVG, you can see that they are trying to write this HTML:

<svg class="circular" viewBox="25 25 50 50">
  <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="4" stroke-miterlimit="10"/>
</svg>

A few months ago, I was trying to update their library to Halogen 5 and remove their dependency on purescript-svg-parser-halogen. While I didn't complete that effort, I did end up writing this, which should guide you on what to do:

module Ocelot.Block.Loading where

import Prelude

import DOM.HTML.Indexed (HTMLdiv, Interactive)
import Data.Foldable (foldl)
import Halogen (AttrName(..), ElemName(..))
import Halogen.HTML (IProp, Leaf, Node, attr, element)
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Ocelot.HTML.Properties ((<&>))

type HTMLsvg = Interactive ( viewBox :: String )

svg :: forall p i. Node HTMLsvg p i
svg = element (ElemName "svg")

viewBox :: forall r i. Int -> Int -> Int -> Int -> IProp (viewBox :: String | r) i
viewBox x y w h = attr (AttrName "viewBox")
  (foldl showIntercalateSpace {init: true, val: ""} [x, y, w, h]).val
  where
    showIntercalateSpace acc next =
      if acc.init
        then { init: false, val: show next }
        else acc { val = acc.val <> " " <> show next }

type HTMLcircle = Interactive
  ( cx :: String
  , cy :: String
  , r :: String
  , fill :: String
  , strokeWidth :: String
  , strokeMiterLimit :: String
  )

circle :: forall w i. Leaf HTMLcircle w i
circle props = element (ElemName "circle") props []

-- Note: `cx` and below should probably be properties, not attributes.
cx :: forall r i. Int -> IProp (cx :: String | r) i
cx = attr (AttrName "cx") <<< show

cy :: forall r i. Int -> IProp (cy :: String | r) i
cy = attr (AttrName "cy") <<< show

r :: forall rest i. Int -> IProp (r :: String | rest) i
r = attr (AttrName "r") <<< show

fillNone :: forall r i. IProp (fill :: String | r) i
fillNone = attr (AttrName "fill") "none"

strokeWidth :: forall r i. Int -> IProp (strokeWidth :: String | r) i
strokeWidth = attr (AttrName "stroke-width") <<< show

strokeMiterLimit :: forall r i. Int -> IProp (strokeMiterLimit :: String | r) i
strokeMiterLimit = attr (AttrName "stroke-width") <<< show

spinner ::  p i. Array (HH.IProp HTMLdiv i) -> HH.HTML p i
spinner props =
  HH.div
    ( [ HP.class_ $ HH.ClassName "loader" ] <&> props )
    [ svg
      [ HP.class_ $ HH.ClassName "circular"
      , viewBox 25 25 50 50
      ]
      [ circle
        [ HP.class_ $ HH.ClassName "path"
        , cx 50, cy 50, r 20, fillNone, strokeWidth 4, strokeMiterLimit 10
        ]
      ]
    ]

spinner_ ::  p i. HH.HTML p i
spinner_ = spinner []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment