Alternative syntax for Halogen


#1

The only thing I don’t like about Halogen, is the fact that there are too many arrays when bulding html and sometimes it gets a little hard to keep track of the parentheses.
The other problem is that it’s a bit unpleasant to handle optional html elements.

Both these problems can be fixed ofc by splitting code in smaller functions, but I was thinking of something different.

Is it possible to use MonadWriter with the current implementation of Halogen? I’m asking before starting to try as maybe there are issues that will make me waste hours trying to do something that’s impossible :smiley:

My idea would be to have something like this (also using a record for props):

runHtml $ do
  HH.div { classes: [ columns, isDesktop ] } $ do
    HH.div { class_: column } $ do
      HH.h1 { class_: title } $ HH.text "TITLE"
      HH.button { disabled: loading
                , onClick: HE.input_ Click
                } $ HH.text "CLICK ME"
      when foo $ do
        HH.p_ $ HH.text "SURPRISE"

Which for me is much more readable than:

HH.div [ HP.classes [ columns, isDesktop ] ]
[ HH.div [ HP.class_ column ]
  [ HH.h1 [ HP.class_ title ] ] [ HH.text "TITLE" ]
  , HH.button [ HP.disabled: loading
              , HE.onClick $ HE.input_ Click
              ]
    [ HH.text "CLICK ME" ]
  , someSolutionForFoo
] 

Thanks for reading!


#2

It’s definitely possible to do alternative things with the HTML syntax, but anything other than what we have will have a performance penalty. It’s unlikely we’ll support it in Halogen itself, but it could always be a library.

For what it’s worth, I think the theoretical example is no better at all than the bottom personally :smile:, and the bottom one can be much improved by alternative formatting:

HH.div 
  [ HP.classes [ columns, isDesktop ] ]
  [ HH.div 
      [ HP.class_ column ]
      [ HH.h1
          [ HP.class_ title ] 
          [ HH.text "TITLE" ]
      , HH.button 
          [ HP.disabled: loading
          , HE.onClick $ HE.input_ Click
          ]
          [ HH.text "CLICK ME" ]
      , someSolutionForFoo
      ]
  ] 

Formatting this way takes a bit more space vertically but makes it much easier to see the structure.

Regarding foo, this kind of thing would be one option:

HH.div 
  [ HP.classes [ columns, isDesktop ] ]
  [ HH.div 
      [ HP.class_ column ]
      $ join
          [ pure $ HH.h1
              [ HP.class_ title ] 
              [ HH.text "TITLE" ]
          , pure $ HH.button 
              [ HP.disabled: loading
              , HE.onClick $ HE.input_ Click
              ]
              [ HH.text "CLICK ME" ]
          , guard foo $> HH.p_
              [ HH.text "SURPRISE" ]
          ]
  ]

catMaybes would work here too, but with join you can have a guarded chunk of elements if you use *> instead of $>.

Or in this instance, since it’s the last element that is optional, you could just use a <> guard ... to avoid having to pure the join, etc.


#3

Thanks for the feedback!

So far I have been using an operator with type Array a -> Array (Tuple a Boolean) -> Array to handle optional elements, but , while it’s decent for props, can get very annoying for html subsections.

Your solution is much cleaner, but it suffers from the same problem: the need to change big part of the structure to handle a single optional element.

Adding the extra indentation is surely helpful, I wonder if I can change some setting so that my editor uses that by default.


#4

Any sort of Monadic syntax is going to either be wildly stack-unsafe or extremely inefficient. You can’t use naive Writer because you will very quickly blow the stack, so lets say you trampoline it through Free. You’ll end up at least paying for 1) accumulating the Monoid 2) the spine of Free 3) All the heap allocated closures incurred from Monadic binds. This just gets very expensive for anything non-trivial.

For optional elements I’d define:

when :: Boolean -> (Unit -> HTML ...) -> HTML ...
when b k = if b then k unit else H.text ""

It’s true that this inserts a dummy text node rather than nothing, but in many cases this ends up being more efficient in the long run. If you are not using keyed elements and you are toggling an element that isn’t at the end of the list, you can end up paying to destroy and rerender subsequent siblings. But if you use an empty text node, then the DOM position of nodes stays stable and they can be diffed normally.


#5

FWIW, I hide single optional elements, error messages in my case, by setting their CSS display property to none when they should be hidden. They are always present in the DOM, but it’s a relatively simple solution and it works.


#6

Nice to know!
Why are you passing Unit -> HTML instead of just HTML? Is it to postpone evaluation of something until the component needs to be rendered?


#7

It can work sometimes, but in other cases just hiding an element is not the best solution :frowning:


#8

I’m using maybeH :: forall a p i. Maybe a -> (a -> HTML p i) -> HTML p i variant of your when and it works pretty great in making code clearer!


#9

Unfortunately, Halogen HTML is not a Monoid, otherwise that would just be flip foldMap :slight_smile: I use that with the React bindings often.