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.
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:
- Show how to wrap HTML attributes/properties
- Show how to wrap HTML events
- Combine attributes, properties, and events into 1 row
- Show how to wrap HTML elements using these above
- Applying this explanation to SVG.
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?
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.
onLoad
, a plain event handleronClick
, a special-event handler that uses mouseHandler to force type safety.
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
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:
- the Global Attributes row, the Global Events row, and the Global Properties row stores all attributes/properties/events that apply to all HTML elements.
- the Interactive Events row composes multiple open rows to get all interactive events
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.
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
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 []