Html.fragment : List (Html msg) -> Html msg

Hello folks! React has “fragments” with roughly the type annotation that I put into the title.

I feel like these would sometimes be useful when writing Elm views, in a similar vein to how Html.Extra.viewIf and Html.Attributes.Extra.attributeIf are useful.

Motivating example

I have (after SSCCE-fication):

buttonView : String -> Msg -> Html Msg

buttonsView : Html Msg
buttonsView =
  Html.div []
    [ buttonView "Foo" DoFoo
    , buttonView "Bar" DoBar
    , buttonView "Save" Save
    ]

and I’ve just received an extra requirement from my project manager: if the entity is saved, show buttons "Save as new" and "Save changes" instead of the "Save" button.

So, ideally I’d love to do

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView {isSaved} =
  let
    saveButtons : Html Msg
    saveButtons =
        if isSaved then
            Html.fragment
                [ buttonView "Save as new" SaveAsNew
                , buttonView "Save changes" SaveChanges
                ]
        else
            buttonView "Save" Save
  in
  Html.div []
    [ buttonView "Foo" DoFoo
    , buttonView "Bar" DoBar
    , saveButtons
    ]

but since I don’t have Html.fragment, my (IMHO) best solution is to do:

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView {isSaved} =
  Html.div []
    [ buttonView "Foo" DoFoo
    , buttonView "Bar" DoBar
    , Html.Extra.viewIf isSaved <| buttonView "Save as new" SaveAsNew
    , Html.Extra.viewIf isSaved <| buttonView "Save changes" SaveChanges
    , Html.Extra.viewIf (not isSaved) <| buttonView "Save" Save
    ]

(or put the if in the top level and duplicate the Foo and Bar cases in both branches of the if.)
(or worse, meddle with :: and ++ which personally I don’t like in my views because of how it messes up the otherwise regular formatting.)
(or even worse, wrap things in another Html.div, which has, let’s say, CSS side-effects.)

That is to say, we have many workarounds, but it seems to me that our view code might be nicer if we had Html.fragment : List (Html msg) -> Html msg.


Questions

  • Do you think Html.fragment would be useful?
  • Are there some associated risks?
    (I"m only aware of the need to also have a Html.Keyed.fragment.)

Aside: I get unnecessarily excited by the thought that Html a would become a Semigroup and a Monoid (we already have an identity element, but we don’t have a well-behaving concat function… Html.div [] and friends don’t cut it :smile: )

9 Likes

It would be nice. I suppose you could implement this by defining your own Fragment type? Does that mean you’d have to replicate the entire Html API though? and also it might be less efficient than what Html.fragment operation could achieve?

1 Like

The best performance would probably be achieved if fragment was a primitive baked into elm/virtual-dom and handled by the reconciliator/patcher/… (I don’t know the nomeclature).

I guess having this be a type separate to Html would be possible; but as you imply, the Elm code would probably do a lot of List.concat or something similar under the hood, yes.

(And for me personally, if the choices are “wait for it forever and work around it in the meantime” vs “implement my own Html-like module”, I’m probably sticking with the status quo :sweat_smile: )

How about PM Evan, see if he likes the idea and would potentially be open to a Pull Request if you wrote the feature?

I think this can be done whilst retaining full backward compatability on elm/virtual-dom, that is, it could be done as a minor release?

Html.fragment is sort of equivalent to Cmd.batch and Sub.batch. Would you also define Html.none == Html.fragment [] in the API?

1 Like

I would probably do the following (or whatever is similar but compiles I haven’t checked the parsing of ::)

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView {isSaved} =
  let
    saveButtons : Html Msg
    saveButtons =
        if isSaved then
                [ buttonView "Save as new" SaveAsNew
                , buttonView "Save changes" SaveChanges
                ]
        else
            [ buttonView "Save" Save ]
  in
  Html.div []
    ( buttonView "Foo" DoFoo
      :: buttonView "Bar" DoBar
      :: saveButtons
    )

Mostly because I think it is the most similar to what you are wishing for. Which is why I would also prefer Html.fragment in particular your SSCCE-ified version is actually pretty kind, because it has the ‘list’ part last. This would become much more challenging if say you wanted the buttonView "Bar" DoBar button to be the last one.

As a further point in favour of Html.fragment it’s not just the pain of writing an example such as the one you’ve given, it’s the refactoring of an existing code that is painful. You’re forced to basically the entire enclosing element, whereas using Html.fragment you would only change that part that you wish to change.

I can’t think of any downsides off the top of my head, but, and I’m not sure why, I suspect there is a major gotcha that I’ve not thought of here.

1 Like

We already kinda cheat Html.none (btw, Html.Extra.nothing is a thing) with Html.text "". But having it sanctioned (and perhaps done using Html.fragment [] instead) would be a nice polishing of the API, yes.

1 Like

This should also be done with attributes. This package already does it:

https://package.elm-lang.org/packages/arowM/html/latest/

3 Likes

When I encounter this type of situation, the solution usually looks like this:

buttonsView : { isSaved : Bool } -> Html Msg
buttonsView { isSaved } =
    [ [ buttonView "Foo" DoFoo
      , buttonView "Bar" DoBar
      ]
    , if isSaved then
        [ buttonView "Save as new" SaveAsNew
        , buttonView "Save changes" SaveChanges
        ]

      else
        [ buttonView "Save" Save ]
    ]
        |> List.concat
        |> div []

But yeah, having a way to say “Insert these elements as siblings in this list of children” would help as I ended up using this pattern frequent enough.

2 Likes

I have been using an approach similar to this, which works for all types of lists:

    when: Bool -> List a -> List a -> List a
    when condition subList accumulator =
        if condition
        then (subList ++ accumulator)
        else accumulator

    always: List a -> List a -> List a
    always subList accumulator =
           when True

Then we can write something similar to this:

    buttonsView : { isSaved : Bool } -> Html Msg
    buttonsView {isSaved} =
       Html.div []
         (always
             [ buttonView "Foo" DoFoo
             , buttonView "Bar" DoBar
             ] <|
          when isSaved
             [ buttonView "Save as new" SaveAsNew
             , buttonView "Save changes" SaveChanges
             ] <|
          when (not isSaved)
             [ buttonView "Save" Save ] [])

You could also define never as when False, and also define variants that take a single element instead of a subList, using :: instead of ++. Honestly, when I write UI code that is many pages long with maybe twenty conditions, this style looks pretty easy to read IMHO.

ps. How do you all get your code syntax-highlighted?

7 Likes

you add “elm” after the first triple quote indicating the start of a code block quote like this

```elm
your code
```
3 Likes

Thanks!! I fixed it.

Actually I take that back. For this style, :: is not very useful because you would need ordinary parenthesis to do the work of keeping the single item together. So instead of writing this:

    ...
    whenFor1 isNotSaved
        ( buttonView "Save" Save ) <|
    ...

you might as well use square brackets, make it a list, and have only one set of definitions all based on ++. The only exception is when the single item you’re trying to include is actually just a single token to the lexer. But that is so rare anyway. Almost always the single item will be something applied to something.

I feel strongly that Elm does not need something like Fragment. In Elm (but not React), there is nothing stopping you from writing a view function that returns a List (Html msg) instead of Html msg. You have complete freedom in Elm to write a view function however you would like, whereas in React I think you are practically limited to view components.

Im speaking from experience. I often write view functions that return lists.

So, taking @Janiczek 's OP example, I would just embrace the List type.

buttonsView : List (Html Msg)
buttonsView =
    [ buttonView "Foo" DoFoo
    , buttonView "Bar" DoBar
    , buttonView "Save" Save
    ]
buttonsView : { isSaved : Bool } -> List (Html Msg)
buttonsView {isSaved} =
  List.concat
    [ buttonView "Foo" DoFoo
    , buttonView "Bar" DoBar
    , saveButtons isSaved
    ]

saveButtons : Bool -> List (Html msg)
saveButtons isSaved =
    if isSaved then
        [ buttonView "Save as new" SaveAsNew
        , buttonView "Save changes" SaveChanges
        ]

    else 
        [ buttonView "Save" Save ]

I think the ergonomics of the list type are great. But another point I would like to make, is that I think wrapping views in arbitrary <div> leads to bugs. A lot of css rules are dictating the interaction between parent and child html notes, and when you have an arbitrary div in between, you are either changing or complicating the relationship between the things you were trying to position on the screen.

In more concrete terms, heres an example of an unfortunate situation I have been stuck with many times, where there are like 10 levels of html between the <body> and the first part of what the UI is meant to render:

<div id="main">
    <div class="page-body">
        <div class="page-header">
        <div class="main-page-body-x"
            <div class="nav">
            <div class="page-container">
                <div class="page">
                    <div class="main-page-container">
                         <div class="main-page">
                            <div class="main-page-menu-bar">
                            ... 

when it could have been half the size, if developers werent in the routine of returning and wrapping divs.:

<div class="header">
<div class="body"
    <div class="nav">
     <div class="main-page"
         <div class="main-page-menu-bar">
                            ... 

Those sorts of super dom stacks inevitably lead to persistent problems with scroll bars being on the wrong element and areas of the page rendering either too short or beyond the page limits. Its just very difficult to manage the relationship between all those nodes. Its very difficult for developers to even conceptualize what the different layers are meant to do, which I think is reflected in the challenge of giving distinct names to several arbitrary layers (“main”, “root”, “container”, “page”, etc).

6 Likes

I think I understand the gist but the examples don’t typecheck in my head. What’s the type annotation of buttonView - does it also return a List? Then we’re missing some List.concats somewhere?

Could you please make an Ellie so that I’m sure I’m not misunderstanding the idea? :slight_smile:

Oh shoot. I made some type errors in my example.

Heres what I was aiming for, in this type-checked ellie: https://ellie-app.com/ctgbXkwCcp2a1

1 Like

It’s great to hear from someone with an opposing view. Proposal threads often turn into something like an echo chamber, where everyone agrees the proposed feature is great/not-useful.

I think this is the sort of state that @Janiczek is attempting to help prevent. I think the point is that these can occur because there is no Html.fragment. So people are in a situation where their current function returns an Html Msg, but they have to add an element to it. Maybe they are returning a checkbox and realise that they should also return an associated Html.label element. Now perhaps what they should do is change the return type of the function to be List (Html Msg), and then refactor at all the places that call it, but what they do instead is wrap it in a Html.div [ Html.Attributes.class "checkbox-container" ] and move on. What you end up with if you do that a few times is the kind of situation you describe where you have a bunch of divs with nothing in between the levels (like Chapter, sub-section, sub-sub-section headers with no text before inner most ‘sub-*section’) If Html.fragment were a thing, then what they could do instead is just wrap the checkbox and label inside a Html.fragment and then they would still appear at the outer level with no "checkbox-container’ div.

I cannot quite shake the feeling that there is some hidden gotcha here, like maybe this means that basically all (view) functions return an Html Msg and we end up losing some important typing distinctions.

1 Like

Okay I see. I think maybe I rushed into this topic too fast talking about the problems of overly layered html. We are all more or less aligned against that.

We’ve brought up batch functions in this thread. Sub.batch, Cmd.batch, and Css.batch seem to work pretty well in practice, so, maybe it wouldnt be so bad if there was an Html.batch. On the other hand, if Elm didnt have Sub.batch or Cmd.batch built in, then I think our next best alternative would be update functions returning List (Cmd msg) and subscriptions : List (Sub msg); and we would have to use ++ and List.concat to merge groups of different Cmd or Sub. Which in my opinion is only slightly worse than what we have today with batch.

3 Likes

I think display: contents is supposed to eliminate the CSS side-effects of wrapping things in a div. From the spec:

For the purposes of box generation and layout, the element must be treated as if it had been replaced in the element tree by its contents

That said, it looks like not all browsers support it entirely: CSS display: contents | Can I use... Support tables for HTML5, CSS3, etc

1 Like

Rather than “perhaps” I’d say “often”, and use the friendly compiler and fearlessly refactor the code to fit the problem.

This seems like a social problem, and even if you have Html.fragment, why wouldn’t people keep doing what you just mentioned?

Good coding practices come from working with others, reviewing code, and healthy discussion. Linters like elm-review can help with these things some times too. There is no way around it.

I think Chad brings a great point, it is all functions and changing the return type helps with this.


I don’t have anything against the proposal, the impact in the virtual dom library is probably the most important thing to assess.

It would definitely be handy to have fragment because if you have functions that return List (Html msg) you can’t map over them like you can with a Html msg, which is very useful.

In the previous example you’d have to saveButtons isSaved |> List.map (Html.map fn) which is not the end of the world but not very ergonomic if you have to do it in many places with the List.concats or ++s.

2 Likes