DEV Community

stevensonmt
stevensonmt

Posted on

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.

Top comments (3)

Collapse
 
jeremysorensen profile image
JeremySorensen

I am just (as of yesterday) trying to get started in elm-ui. When I saw the issues you were having I was genuinely concerned, because I need to make something almost exactly like what you did here. The main difference is that my menu needs to open on onMouseEnter, and close on onMouseLeave. Turns out responding to those events with the OpenMenu and CloseMenu messages you already have is very easy and gives a good user experience. So you might solve the problem that way (though it is a different behavior than the original "spec"). Also good to note that your solution (with the element in back) gives perfect behavior (I think) for making a combo-box/drop down list. In that case you want the menu to close when they click an item.
In any case it is difficult just to find enough examples of elm-ui to synthesize all the pieces from the docs into a working program so your example is very useful. Thanks!

Collapse
 
rolograaf profile image
Lourens • Edited

Girish Sonawane is using a CSS framework called BULMA to make a Modal pop-up in elm, as descibed in article medium.com/@girishso/displaying-mo.... I think by using a fullscreen container div as background it is toggling the modal state onClick. If that is any useful...

bulma.io/documentation/components/...

renderModal : Model -> Html Msg
renderModal model =
    div
        [ class "modal is-active", attribute "aria-label" "Modal title" ]
        [ div
            [ class "modal-background", onClick TogglePopup ]
            []
        , div
            [ class "modal-card" ]
            [ header
                [ class "modal-card-head" ]
                [ p
                    [ class "modal-card-title" ]
                    [ text "Modal title" ]
                , button
                    [ class "delete", onClick TogglePopup, attribute "aria-label" "close" ]
                    []
                ]
...
Collapse
 
mboren profile image
Marty Boren

This was very helpful. In my case, I was trying to display a menu on top of other content, so I had to use use Element.inFront in place of Element.behindContent because the content would cover up the clickable region.

Element.el
[ Element.inFront fullWidthBackgroundThatClosesMenuOnClick
, Element.inFront myMenu
]
(...content...)