Better svg support in halogen

So, this was already discussed in another thread on a basic level, so I thought we could continue here:

So, in the previous issue someone shared a fork of the deprecated halogen-svg package which is supposed to work with the latest version of halogen. I tried using that package and it worked okish for the basics (still don’t know why there’s no classes prop like in normal halogen and I have to concatenate them manually), but after a while I discovered some props like the border prop were missing (and yes I tried setting the border from css and it doesn’t seem to have any effect on the svg element)

Edit: So aparently in svg instead of border It’s called stroke, so that part was my bad lol
Edit 2: So I get a random error when trying to use the strokeWidth attribute, so it seems like the package is still a little bit broken

I cannot open an issue on the fork because for some reason the issue tracker is disabled. I was also thinking about opening a pr, but there seem to be 2 prs opened back in September which haven’t been merged so idk if the mainainer is up to merging other changes.

I would probably have to create my own fork of the fork and use that, but some official support would be really nice.

Edit 3: I just saw the open issue on the deprecated package but nobody seemed to have noticed it in like 2.5 weeks so idk if we can rely on that

What do you think?

It needs someone willing to step up and maintain such a package who has a good knowledge of SVG. If the question is why don’t the current Halogen maintainers make this for us, it’s probably because they don’t have the need or knowledge to maintain it properly.

This has come up enough that I’m writing a guide for someone else to create these things. @Adrielus, 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.

If someone else understand this better, feel free to correct me.

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 []
2 Likes

Thanks, I’ll look into it!

Suspecting that I might be the ‘someone’ who shared ‘a fork’, I enabled issues here: https://github.com/statebox/purescript-halogen-svg/issues.

It’s great to see other people using Halogen and SVG as well. We haven’t had much time to do maintenance on this, but we’d welcome efforts in the direction of consolidating the various forks of purescript-halogen-svg under some community stewardship.

Oh, ur probably right, that’s the fork I’m usint. Probably this sunday I’ll open an issue with the problems I encounter and propose possible fixes