Working with menus in elm-ui

stevensonmt

stevensonmt

Posted on December 31, 2018

 Working with menus in elm-ui

Recently had to figure out how to get a modal sort of menu to open and close correctly using the mdgriffith/elm-ui package in Elm 0.19. For those unfamiliar, the elm-ui package allows you to create front-end interfaces without resorting to CSS or HTML (with occasional exceptions). If you are working in Elm, I highly recommend it. If you aren't working in Elm yet, I would point to elm-ui as one of its selling points and a reason to try it out.

Here is the very basic concept in an ellie.
Our simple model has two fields: count and menu. It is defined thus:

type alias Model =
    { count : Int, menu : Bool }


initialModel : Model
initialModel =
    { count = 0, menu = False }

Here is the view code:

view : Model -> Html Msg
view model =
    Element.layout [ Element.Background.color (Element.rgb255 30 30 30), padding 10]
    <| column [spacing 10, width <| px 200, Element.Background.color (Element.rgb255 200 30 10)] 
           [ el [ width fill
                , padding 8
                , Element.Font.center
                , Element.Background.color <| 
                      Element.rgb255 120 120 180
                ] <| 
                     Element.text (String.fromInt model.count)
           , el [ centerX
                , Element.Events.onClick OpenMenu
                , Element.below <| myMenu model
                ] <| 
                    Element.text "I'm a menu!"
           ]

And finally our Msgs and update function fill out the Elm Architecture for our demo app:

type Msg
    = Increment
    | Decrement
    | OpenMenu
    | CloseMenu


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        OpenMenu -> 
            { model | menu = True }

        CloseMenu -> 
            { model | menu = False }

If you try the ellie link you'll find that the menu opens to reveal the buttons for incrementing and decrementing the model count. Easy peasy. BUT the menu stays open. The first step to fix that is to change the menu element's onClick attribute from an OpenMenu action to a ToggleMenu action.
So now we have

type Msg
    = Increment
    ...
    | ToggleMenu

update : Msg -> Model -> Model
update msg model =
    case msg of
       ...
        ToggleMenu -> { model | menu = not model.menu }

view : Model -> Html Msg
view model =
    ...
    , el [ centerX
                , Element.Events.onClick ToggleMenu
                , Element.below <| myMenu model
                ] <| 
                    Element.text "I'm a menu!"
    ...

Sweet! Now the menu opens and closes when the triggering element is clicked. See the updated version here. But lots of users will probably expect the menu to close when any other part of the viewport is clicked. Here is the first way I thought of setting that up:

view : Model -> Html Msg
view model =
    Element.layout [ Element.Events.onClick CloseMenu
    ...

This should mean that clicking anywhere sends the CloseMenu Msg. See the problem with this approach here. Try to launch the menu and then open the debugger.

What you see is that clicking the menu sends the ToggleMenu Msg just like we want, but it is immediately followed by the CloseMenu Msg. This leads to the menu never opening. Ugggh.

The reason for this is that onClick events are propagated from child elements to parent elements. And we can't just use the ToggleMenu fix on the layout because that would open the menu anytime you click anywhere.

There are almost certainly many ways to get around this, but the way I hit upon was to put an empty element the size of layout behind the content of the layout using the cleverly named function Element.behindContent. Giving this element an onClick CloseMenu attribute works because it is not in a parent-child relationship with the menu elements.

This approach works okay but there's still a couple of glitches. See it in action here. The most obvious glitch in terms of the problem of getting the menu to close is that the element displaying the counter is not sending the CloseMenu Msg when clicked because it is in front and not a child element of the element we just added. My solution was to add the onClick CloseMenu attribute to that element. I'd be interested to hear more elegant solutions though!

The second obvious hiccup to me is that the menu closes each time you increment or decrement the count. I think most users would expect the menu to stay open until they were done incrementing/decrementing the count as many times as they wanted. I'll leave the solution to that as an exercise for anyone interested.

Thanks for reading. I hope you found it helpful. If you have questions about Elm or elm-ui I highly recommend the Elm Discourse and the Elm and elm-ui Slack channels.

💖 💪 🙅 🚩
stevensonmt
stevensonmt

Posted on December 31, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related