Are there any common patters for dealing with conditionally including markup?

I’m rather new to Elm, and while I am I’m absolutely loving it, this was definitely a pain point for me.
The standard way to conditionally include an HTML element, as far as I can tell is

div []
        [ element1
        , if condition then
            element2

          else
            text ""
        , element3
        ]

Personally I don’t like this. the “do nothing”text "" is not expressive, and, when doing this for attributes, is not even trivial (classList [] is the best solution I know). I’ve created tooling to make this, IMHO, nicer. It’s still a WIP but I’m using it in my current project right now. I’m curious if there’s already different common approaches to this that people think are nicer.

2 Likes

There are some nice solutions in this reddit thread - it pertains to attributes but the same rules apply for element lists.

I think text "" is an alright solution provided it’s wrapped in something like this to make it more expressive:

module Main exposing (main)

import Html exposing (Html, div, text)


htmlIf : Html msg -> Bool -> Html msg
htmlIf el cond =
    if cond then
        el

    else
        text ""


main : Html msg
main =
    div []
        [ div [] [ text "Always here" ]
        , htmlIf (div [] [ text "Conditionally drawn" ]) True
        , div [] [ text "Always here" ]
        ]

View on Ellie

2 Likes

This is more or less the approach I’m taking currently.

Instead of replacing your element with a dummy element, you can leave it out of the child list.

view condition =
    div []
        ([ element1 ]
            ++ (if condition then
                    [ element2 ]

                else
                    []
               )
            ++ [ element3 ]
        )

For elements, it seems it’s considered that text "" should be enough, although there is an open issue. [1]

For attributes there was this idea of a batch function [2]:
Html.Attributes.batch : List (Attribute msg) -> Attribute msg

But the idea was dismissed by Evan himself later. [3]

[1] https://github.com/elm/html/issues/53
[2] https://groups.google.com/d/topic/elm-dev/6hmLG0iNm90/discussion
[3] https://github.com/elm/html/issues/103#issuecomment-313605792

normally I define some simple helpers:

empty : Html msg
empty =
    Html.text ""


viewJust : (a -> Html msg) -> Maybe a -> Html msg
viewJust fn maybe =
    case maybe of
        Just a ->
            fn a

        Nothing ->
            empty


viewIf : (() -> Html msg) -> Bool -> Html msg
viewIf fn shouldDisplay =
    if shouldDisplay then
        fn ()

    else
        empty

These usually end up in a module like Html.Styled.Extra if I’m using elm-css (which is almost always) or Html.Extra if I’m not.

I’m a big fan of pulling conditionals out of markup. It’s a bit more verbose but is much more readable.

expandable : Model -> Html Msg
expandable model =
  if model.isExpanded then
    expandedView model.items
  else
    collapsedView

expandedView : List Item -> Html Msg
expandedView items =
  div [ class "expanded" ] <| List.map itemView items

collapsedView : Html a
collapsedView =
  div [ class "collapsed" ] []
3 Likes

I use something close to what @brian posted, the only difference is the function name.

empty : Html msg
empty =
    Html.text ""


when : Bool -> Html msg -> Html msg
when shouldRender view =
    if shouldRender then
        view

    else
        empty

I’d prefer when because it seems nice when you do pipes:


div [ class "d-flex flex-column flex-fill p-3" ]
    [ errorView
        |> when (RemoteData.isFailure form.submissionStatus)
    , myCoolView
        |> when somethingElse
    ]
2 Likes

I use a couple of helper methods like the other examples, but which operate on List instead of Html.

concatMaybe : Maybe (List a) -> List a -> List a
concatMaybe maybe list =
    case maybe of
        Just toConcat ->
            list ++ toConcat

        Nothing ->
            list


add : a -> List a -> List a
add value list =
    value :: list


addMaybe : Maybe a -> List a -> List a
addMaybe maybe list =
    case maybe of
        Just toAdd ->
            toAdd :: list

        Nothing ->
            list


addIf : Bool -> a -> List a -> List a
addIf doAdd value list =
    if doAdd then
        value :: list

    else
        list


addIfMatches : (a -> Bool) -> a -> List a -> List a
addIfMatches predicate value list =
    if predicate value then
        value :: list

    else
        list

Then build lists of things with pipelines.

let
       attributes =
            [ Css.tile ]
                |> Build.concatMaybe maybeClickAttributes
                |> Build.concatMaybe (getLinkAttributes (config.linkTo data))
                |> Build.addIf shouldTransistion Css.transition

       children =
           []
                |> Build.add (Text.heading "Sponsor Information")
                |> Build.add (Text.paragraph sponsorDetails)
                |> Build.addMaybe (Maybe.map viewContactInfo sponsorContactInfo)
                |> Build.addMaybe (Maybe.map viewAdminInfo sponsorAdminInfo)
                |> Build.addIf showMoreLink (Element.link (Route.SponsorInfo sponsorId) "More Info")
                |> List.reverse
in
Html.div attributes children

This is nice because it looks basically the same as if you had written out a list - just imagine replacing |> with , and you nearly have a list literal.

Downside is you have to build things backwards because :: prepends, or remember to use List.reverse. Doesn’t matter for attributes because their order doesn’t make a difference.

This was mentioned in the thread I linked before, and the order of attributes does matter.
https://groups.google.com/d/msg/elm-dev/6hmLG0iNm90/_PIUjr0SCwAJ

I have found text "" to be the most straight-forward an clear approach in most cases. So that is what we use.
Dealing with element lists (for the purpose of returning []) usually leads to code that is harder to work with and understand.

For empty view we define:

empty : Html msg
empty = Html.text ""

For empty attribute we define:

empty : Html.Attribute msg
empty = Html.Attributes.property "" Encode.null

Both are harmless and very usefull.

Html.Attributes.property "" Encode.null does actually set a property on the DOM object though. Html.Attributes.classList [] has absolutely no effect af far as I can tell, that’s why I prefer to use that.

I think it does if no class is already defined, see:

1 Like

oh wow, you’re right. I thought I had tested that but I guess not :confused:

In that case, text "" would also have an effect on the DOM - it actually creates the empty text node!

div [] [ text "", text "Hello, World!", text ""]

produces

Yes it does. And that property is "". So unless one wants to define a unnamed property of a node it should be fine. In practice that’s a very edge case. Thanks for pointing that out, it maybe useful for folks to learn.

My world is falling apart!

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.