Alternative HTML eDSL compatible with Halogen

I’ve given it a fair try, but I must say that what appears to be the standard way of writing HTML in Haskell-like languages and frameworks (Yesod, Elm, Halogen…) doesn’t quite compute to me. I can imagine that I’m not the only one having a hard time processing that this:

  HH.div
    [ HP.id "root" ]
    [ HH.input
        [ HP.placeholder "Name" ]
    , HH.button
        [ HP.classes [ HH.ClassName "btn-primary" ]
        , HP.type_ HP.ButtonSubmit
        ]
        [ HH.text "Submit" ]
    ]

actually means

<div id="root">
  <input placeholder="Name" />
  <button class="btn-primary" type="submit">
    Submit
  </button>
</div>

The latter is universally readable and there’s a clear visual distinction between properties and contents (meaning that in the glimpse of an eye, one can understand whether something is a property value like btn-primary, or some content like Submit or even another nested tag).

The former is, at least to the untrained eye, quite cryptic, and more importantly there is absolutely no visual distinction between properties and content, both being enclosed in square brackets, which to me sounds like a net negative (and a massive one) compared to the regular HTML version.

Is there any alternative eDSL in PureScript that would either match more closely the natural HTML syntax (like JSX does for JavaScript), or at least would provide clear visual distinction between properties and contents?

EDIT

I stumbled across Smolder which does a better job at distinguishing between properties/content but it doesn’t seem to be maintained anymore (last commit from more than almost two years ago).

3 Likes

I think it’s mostly a matter of habbit. But to me, the former way is more appropriate and appealing than the latter. It’s all not just about “the glimpse of an eye”, but the correctness and overal simplicity.

one can understand whether something is a property value like btn-primary , or some content like Submit or even another nested tag

In the end, it’s all about just functions.

3 Likes

You could make an eDSL like so, but when you get it wrong the errors would be horrible:

import Unsafe.Coerce

foreign import data Boolean :: Type
foreign import data True :: Boolean
foreign import data False :: Boolean

foreign import data Nat :: Type
foreign import data Zero :: Nat
foreign import data Succ :: Nat -> Nat

data OpenEl = OpenEl
data CloseEl = CloseEl

foreign import data HTML :: Type

class Next :: Nat -> Boolean -> Type -> Type -> Constraint
class Next n attr i o | i n -> o

instance a ::
  ( Next (Succ n) False i o
  ) => Next (Succ n) True (Record r) (i -> o)

instance b ::
  ( Next (Succ n) False i o
  ) => Next (Succ n) attr String (i -> o)

instance c ::
  ( Next (Succ n) True i o
  ) => Next n attr OpenEl (String -> i -> o)

instance d :: Next (Succ Zero) attr CloseEl HTML

instance e ::
  ( Next (Succ n) False i o
  ) => Next (Succ (Succ n)) attr CloseEl (i -> o)

-- you'd actually implement a member for each of the instances, but it's not important here
-- so we use unsafeCoerce
parse :: forall i o. Next Zero False i o => i -> o
parse = unsafeCoerce unit

test = parse
  OpenEl "div"
    OpenEl "div" { attr: "Hello" }
      "Hello"
      OpenEl "button" CloseEl
      "World!"
    CloseEl
  CloseEl

The verbosity of OpenEl/CloseEl can be improved but it keeps the implementation here short

To be honest, this doesn’t really solve the original issue of it being easier to read, as I think in an actual implementation it wouldn’t use a record for the attributes? It’s mostly just a case of formatting attributes on the same line

2 Likes

Building on your example, about something that would give us

test username =
  Div { id: "root" }
    Input { placeholder: "Name" } Input'
    Button { class: "btn-primary", type: "submit" }
      "Your name is " <> username <> "."
    Button'
  Div'

Or even, closer to Halogen but replacing lists by a record-like syntax for attributes?

HH.div { id: "root" } [
  HH.input { placeholder: "name" },
  HH.button { classes: [ HH.ClassName "btn-primary" ], type: HP.ButtonSubmit } [
    HH.text "Submit"
  ]
]

Actually I’m guessing the latter example just above might not be too hard to implement on top of HalogenHTML, and (to me at least) it would already look much more readable.

1 Like

Eventually for a real-life project you probably still will end up with some kind of custom DSL that serves the project’s needs and personal preferences.

For example to have your custom text input widget you just make functions like that:

textInput :: String -> String -> Action -> H.ComponentHTML ...
textInput placeholder value action =
   HH.input
        [ HP.type_ HP.InputText
        , HP.placeholder placeholder
        , HP.value value
        , HE.onValueInput action
        ]

This way you may have whatever DSL you like and need. And it anyway will be much more expressive than relying on any kind of generic API.

3 Likes

very good point @wclr — I might have overthought it.

This is fairly similar to how React in PureScript is done FWIW. In react-basic-hooks you could write something like

DOM.div { id: "root", children: [
  DOM.input { placeholder: "name" }
  DOM.button { className: "btn-primary", type: "submit", children: [
    DOM.text "Submit"
  ]
]

So I think it’s more a matter of library design that something inherent to PureScript/Haskell.

2 Likes