Since Halogen’s HTML, properties and classes are all arrays, dealing with optional elments might not be immediatly obvious to the less experienced Purescript coder (like me!).
For example, in most templating languages it’s possibile to write something like this (here I’m using handlebars):
Obviously with Halogen we want to avoid something hideous like:
HH.div_ case maybeUsername of Nothing ->  Just n -> [ HH.text $ "Hello " <> n ]
This example might look ok here, but it quickly leads to unreadable code in more real life scenarios.
Classes and properties
Monads to the rescue!
To optionally render classes and properties, we can use these operators:
join :: forall a m. Bind m => m (m a) -> m a guard :: forall m. MonadZero m => Boolean -> m Unit ($>) :: forall f a b. Functor f => f a -> b -> f b
In our case,
m is actually
Array. So we can rewrite them as:
join :: forall a. Array (Array a) -> Array a guard :: Boolean -> Array Unit ($>) :: forall a b. Array a -> b -> Array b
We are going to use
($>) to return either an empty array in case
the guarding fails, or an array containing just the single element when the
This is because
empty when the condition fails, and
when it succeds. In our example, this is actually
( :: Array Unit) $> "hello" == ( :: Array String) [unit] $> "hello" == ["hello"]
From the example we can see that the penguin
($>) replaces the element in the
array if present.
The last step is
join which takes an
Array (Array a) and returns
We can put all this together like this:
HH.div [ HP.classes $ join [ pure $ ClassName "hello" , guard true $> ClassName "world" , guard false $> ClassName "nope" ] ]
Our div will have “hello” and “world” as classes.
pure in our example is
join [ [ClassName "hello"], [ClassName "world"], ] -- which becomes [ ClassName "hello", ClassName "world" ]
A better solution
Ok, it’s not better, but I like it more.
The problem with the previous approach is that when an optional elment has to be
added, one needs to wrap all the other elements in
pure and add a
We can abuse html to come up with a simpler solution:
dummyAttr :: forall r i. HP.IProp r i dummyAttr = HP.attr (HH.AttrName "data-dummy") "" whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i whenP b p = if b then p else dummyAttr maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i maybeP m p = maybe dummyAttr p m whenC :: Boolean -> ClassName -> ClassName whenC b c = if b then c else ClassName ""
We add either a fake attribute or an empty class when our conditions are not met.
HH.div [ HP.classes [ ClassName "hello" , whenC true $ ClassName "world" , whenC false $ ClassName "nope" ] , maybeP mTitle $ \t -> HP.title t ]
A good solution most of the time is just to extract the optional HTML to a separate function:
render state = HH.div_ [ HH.text "Hello" , adminSection state.isAdmin , HH.text "World"] adminSection false = HH.text "" adminSection true = HH.div ...
We can write a couple of helper methods like the one we had for classes and properties:
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i whenH b k = if b then k unit else HH.text "" maybeH :: forall a p i. Maybe a -> (a -> HH.HTML p i) -> HH.HTML p i maybeH m k = maybe (HH.text "") k m
render state = HH.div_ [ HH.h1_ [ HH.text "Big title" ] , whenH state.isAdmin $ \_ -> HH.h2_ [ HH.text "Hello admin!" ] , maybeH state.todo $ \t -> HH.p [ HH.text $ "Remember: " <> todo ] ]