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)
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 ononMouseEnter
, and close ononMouseLeave
. Turns out responding to those events with theOpenMenu
andCloseMenu
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!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/...
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 ofElement.behindContent
because the content would cover up the clickable region.