This is an alternative site for discovering Elm packages. You may be looking for the official Elm package site instead.
README

DropdownMenu

A production-grade dropdown menu.

If you need internal scrolling, you will need to wire in a port, check the examples to see how it is done.

The API is designed to minimise the boiler plate necessary to use dropdown menus.

We rewrote the whole module several times and every time we ended up with the same boilerplate and roughly the same way to abstract it away.

Common Features

type alias CommonFeatures = { downArrow : Html Never , clearButton : Html Never , scrollIntoView : String -> Cmd Never , classes : Classes }

This record describes the stuff that should be the same all over the app.

scrollIntoView is required only if you need internal scrolling: you should set it to the port provided in ports/.

defaultCommonFeatures : CommonFeatures
type alias Classes = { root : String , isOpen : String , isClosed : String , isDisabled : String -- selection box , selectBox : String , selection : String , placeholder : String , clearButton : String , downArrow : String -- menu , menu : String , menuOption : String , menuOptionSelected : String , menuOptionHighlighted : String }

These are the CSS class names that will be used for the dropdown menu elements.

defaultClasses : Classes
itemToHtml : (item -> String) -> Bool -> Bool -> item -> Html msg

In most cases where you declare a Config's itemToHtml field you don't really care about the isSelected and isHighlighted arguments, so you can use this convenience function to keep your config declaration cleaner.

myConfig =
    { ...
    , itemToHtml = DropdownMenu.itemToHtml .name
    ...
    }

Config

type alias Config model item msg = { hasClearButton : Bool , itemToHtml : Bool -> Bool -> item -> Html Never , itemToId : item -> String , itemToLabel : item -> String , modelToItems : model -> List item , modelToMaybeOpenState : model -> Maybe OpenState , modelToMaybeSelection : model -> Maybe item , msgWrapper : Msg -> msg , placeholder : Html Never }

This record holds the configuration for a specific dropdown menu.

The model and the msg types refer to the Elm Architecture Model and Msg of the parent, ie the types used by the update function that will call DropdownMenu.update.

item is just the type of the items that will appear in the menu.

The record fields are:

  • hasClearButton if set to True, will add a Clear icon that, when clicked, will set the selected item to Nothing.

  • itemToHtml is a function to render a menu item into Html. The first two arguments state whether the item is currently selected and highlighted, respectively.

  • itemToId This string is used for comparisons and as DOM id when scrolling.

  • itemToLabel This string is used for matching the item against character searches.

  • modelToItems Returns the list of items to be shown in the menu.

  • modelToMaybeOpenState Returns Nothing when the menu is closed, or Just the OpenState if it is open.

  • modelToMaybeSelection Returns the currently selected item.

  • msgWrapper Transforms a DropdownMenu.Msg into a parent msg.

  • placeholder is shown whenever the current selection is Nothing.

The Elm Architecture

type OpenState = OpenState PrivateOpenState

You can think of this as the Elm Architecture Model for the dropdown menu, with the difference that it is only needed when the menu is open.

Also, since you really want to have only a single dropdown menu open at any given time, you should structure your Model so that at most one OpenState can exist at any given time. Check the examples to see how this is done.

open : OpenState

Normally dropdown menus are initialised as closed and open only on user input, but if for any reason your code needs to open the menu, you can use this.

type Msg = NoOp | OnKey Key | OnBlur | OnClickCurrentSelection | OnClickItem String | OnMouseEnterItem String | OnClickClear | OnResetSearchString Int | OnTransitionEnd
update : CommonFeatures -> Config model item msg -> Setters model item msg -> Msg -> model -> ( model, Cmd msg )
type alias Setters model item msg = { closeAllDropdowns : model -> model , openOnlyThisDropdown : OpenState -> model -> ( model, Cmd msg ) , setCurrentSelection : Maybe item -> model -> ( model, Cmd msg ) }

This type is just a nicer way to declare the arguments for DropdownMenu.update.

All these functions modify the parent model.

view : CommonFeatures -> Config model item msg -> Bool -> model -> Html msg

Setting the third argument to True will disable the dropdown.

module DropdownMenu
    exposing
        ( CommonFeatures
        , defaultCommonFeatures
        , Classes
        , defaultClasses
        , itemToHtml
          --
        , Config
          --
        , OpenState
        , open
        , Msg
        , update
        , Setters
        , view
        )

{-| A production-grade dropdown menu.

If you need internal scrolling, you will need to wire in a [port](https://github.com/xarvh/elm-dropdown-menu/tree/master/ports),
check the [examples](https://github.com/xarvh/elm-dropdown-menu/tree/master/examples)
to see how it is done.

The API is designed to minimise the boiler plate necessary to use dropdown menus.

We rewrote the whole module several times and every time we ended up with the
same boilerplate and roughly the same way to abstract it away.


# Common Features

@docs CommonFeatures, defaultCommonFeatures, Classes, defaultClasses, itemToHtml


# Config

@docs Config


# The Elm Architecture

@docs OpenState, open, Msg, update, Setters, view

-}

import Char
import Html exposing (Html, div, span, text, ul, li)
import Html.Attributes exposing (class)
import Html.Events
import Json.Decode
import List.Extra
import Process
import Regex
import Task
import Time


-- Exposed (API) types


{-| This record describes the stuff that should be the same all over the app.

`scrollIntoView` is required only if you need internal scrolling: you should
set it to the port provided in [ports/](https://github.com/xarvh/elm-dropdown-menu/tree/master/ports).

-}
type alias CommonFeatures =
    { downArrow : Html Never
    , clearButton : Html Never
    , scrollIntoView : String -> Cmd Never
    , classes : Classes
    }


{-| These are the CSS class names that will be used for the dropdown menu elements.
-}
type alias Classes =
    { root : String
    , isOpen : String
    , isClosed : String
    , isDisabled : String

    -- selection box
    , selectBox : String
    , selection : String
    , placeholder : String
    , clearButton : String
    , downArrow : String

    -- menu
    , menu : String
    , menuOption : String
    , menuOptionSelected : String
    , menuOptionHighlighted : String
    }


{-| This record holds the configuration for a specific dropdown menu.

The `model` and the `msg` types refer to the [Elm Architecture](https://guide.elm-lang.org/architecture/)
`Model` and `Msg` of the *parent*, ie the types used by the `update` function
that will call `DropdownMenu.update`.

`item` is just the type of the items that will appear in the menu.

The record fields are:

  - `hasClearButton` if set to True, will add a Clear icon that, when clicked, will set the selected item to `Nothing`.

  - `itemToHtml` is a function to render a menu item into Html.
    The first two arguments state whether the item is currently selected and highlighted, respectively.

  - `itemToId` This string is used for comparisons and as DOM id when scrolling.

  - `itemToLabel` This string is used for matching the item against character searches.

  - `modelToItems` Returns the list of items to be shown in the menu.

  - `modelToMaybeOpenState` Returns `Nothing` when the menu is closed, or `Just` the `OpenState` if it is open.

  - `modelToMaybeSelection` Returns the currently selected `item`.

  - `msgWrapper` Transforms a `DropdownMenu.Msg` into a parent `msg`.

  - `placeholder` is shown whenever the current selection is `Nothing`.

-}
type alias Config model item msg =
    { hasClearButton : Bool
    , itemToHtml : Bool -> Bool -> item -> Html Never
    , itemToId : item -> String
    , itemToLabel : item -> String
    , modelToItems : model -> List item
    , modelToMaybeOpenState : model -> Maybe OpenState
    , modelToMaybeSelection : model -> Maybe item
    , msgWrapper : Msg -> msg
    , placeholder : Html Never
    }


{-| This type is just a nicer way to declare the arguments for `DropdownMenu.update`.

All these functions modify the parent `model`.

-}
type alias Setters model item msg =
    { closeAllDropdowns : model -> model
    , openOnlyThisDropdown : OpenState -> model -> ( model, Cmd msg )
    , setCurrentSelection : Maybe item -> model -> ( model, Cmd msg )
    }


type Outcome item
    = CloseAndSelect (Maybe item)
    | OpenWithState OpenState


{-| You can think of this as the [Elm Architecture](https://guide.elm-lang.org/architecture/)
`Model` for the dropdown menu, with the difference that it is only needed when
the menu is open.

Also, since you really want to have only a single dropdown menu open at any
given time, you should structure your `Model` so that at most one `OpenState`
can exist at any given time.
Check the [examples](https://github.com/xarvh/elm-dropdown-menu/tree/master/examples) to see how this is done.

-}
type OpenState
    = OpenState PrivateOpenState



-- Internal types


type alias PrivateOpenState =
    { maybeHighlightId : Maybe String
    , searchCounter : Int
    , searchString : String
    }


type Key
    = Esc
    | Enter
    | Space
    | ArrowUp
    | ArrowDown
    | PageUp
    | PageDown
    | Home
    | End
    | Searchable Char


{-| -}
type Msg
    = NoOp
    | OnKey Key
    | OnBlur
    | OnClickCurrentSelection
    | OnClickItem String
    | OnMouseEnterItem String
    | OnClickClear
    | OnResetSearchString Int
    | OnTransitionEnd



-- Defaults


openModel : PrivateOpenState
openModel =
    { maybeHighlightId = Nothing
    , searchCounter = 0
    , searchString = ""
    }


{-| Normally dropdown menus are initialised as closed and open only on user
input, but if for any reason your code needs to open the menu, you can use this.
-}
open : OpenState
open =
    OpenState openModel


namespace s =
    "ElmDropdownMenu-" ++ s


{-| -}
defaultCommonFeatures : CommonFeatures
defaultCommonFeatures =
    { downArrow = text "▼"
    , clearButton = text "×"
    , scrollIntoView = always Cmd.none
    , classes = defaultClasses
    }


{-| -}
defaultClasses : Classes
defaultClasses =
    { root = namespace "root"
    , isOpen = namespace "isOpen"
    , isClosed = namespace "isClosed"
    , isDisabled = namespace "isDisabled"

    -- selection box
    , selectBox = namespace "selectBox"
    , selection = namespace "selection"
    , placeholder = namespace "placeholder"
    , clearButton = namespace "clearButton"
    , downArrow = namespace "downArrow"

    -- menu
    , menu = namespace "menu"
    , menuOption = namespace "menuOption"
    , menuOptionSelected = namespace "menuOptionSelected"
    , menuOptionHighlighted = namespace "menuOptionHighlighted"
    }


{-| In most cases where you declare a `Config`'s `itemToHtml` field you don't
really care about the `isSelected` and `isHighlighted` arguments, so you
can use this convenience function to keep your config declaration cleaner.

    myConfig =
        { ...
        , itemToHtml = DropdownMenu.itemToHtml .name
        ...
        }

-}
itemToHtml : (item -> String) -> Bool -> Bool -> item -> Html msg
itemToHtml itemToLabel isSelected isHighlighted item =
    item
        |> itemToLabel
        |> Html.text



-- Update helpers


noCmd : outcome -> ( outcome, Cmd msg )
noCmd outcome =
    ( outcome, Cmd.none )


maybeFallback : Maybe a -> Maybe a -> Maybe a
maybeFallback replacement original =
    case original of
        Just _ ->
            original

        Nothing ->
            replacement


itemIdToDomId : String -> String
itemIdToDomId itemId =
    itemId
        |> Regex.replace Regex.All (Regex.regex "[^a-zA-Z0-9_-]") (\_ -> "_")
        |> namespace


itemToDomId : Config model item msg -> item -> String
itemToDomId config item =
    item
        |> config.itemToId
        |> itemIdToDomId


findItem : Config model item msg -> model -> Maybe String -> Maybe item
findItem config model maybeId =
    maybeId
        |> Maybe.andThen (\id -> List.Extra.find (\i -> config.itemToId i == id) (config.modelToItems model))


findNext : element -> List element -> Maybe element
findNext e items =
    case items of
        a :: b :: xs ->
            if a == e then
                Just b
            else
                findNext e (b :: xs)

        _ ->
            Nothing


maybeSelectionId : Config model item msg -> model -> Maybe String
maybeSelectionId config model =
    model |> config.modelToMaybeSelection |> Maybe.map config.itemToId


maybeOpenState : Config model item msg -> model -> Maybe PrivateOpenState
maybeOpenState config model =
    case config.modelToMaybeOpenState model of
        Just (OpenState openState) ->
            Just openState

        _ ->
            Nothing


type alias Picker =
    ( PrivateOpenState, List String ) -> Maybe String


pick : Config model item msg -> model -> Picker -> Maybe String
pick config model picker =
    case maybeOpenState config model of
        Nothing ->
            -- Closed. Open it with the current selection highlighted.
            maybeSelectionId config model

        Just openState ->
            model
                |> config.modelToItems
                |> List.map config.itemToId
                |> (,) openState
                |> picker


reversePicker : Picker -> Picker
reversePicker picker =
    Tuple.mapSecond List.reverse >> picker


pickerNext : Picker
pickerNext ( openState, ids ) =
    case openState.maybeHighlightId of
        Nothing ->
            List.head ids

        Just highlightId ->
            ids
                |> findNext highlightId
                |> maybeFallback (ids |> List.reverse |> List.head)


pickerSkip : Int -> Picker
pickerSkip skip ( openState, ids ) =
    case openState.maybeHighlightId of
        Nothing ->
            List.head ids

        Just highlightId ->
            ids
                |> List.Extra.dropWhile ((/=) highlightId)
                |> List.drop skip
                |> List.head
                |> maybeFallback (ids |> List.reverse |> List.head)


scrollToHighlight : CommonFeatures -> PrivateOpenState -> Cmd msg
scrollToHighlight commonFeatures openState =
    case openState.maybeHighlightId of
        Nothing ->
            Cmd.none

        Just highlightId ->
            highlightId
                |> itemIdToDomId
                |> commonFeatures.scrollIntoView
                |> Cmd.map never



-- Update partials


type alias PartialUpdate model item msg =
    CommonFeatures -> Config model item msg -> model -> ( Outcome item, Cmd Msg )


updateNoChange : PartialUpdate model item msg
updateNoChange commonFeatures config model =
    case config.modelToMaybeOpenState model of
        Nothing ->
            model |> config.modelToMaybeSelection |> CloseAndSelect |> noCmd

        Just openState ->
            OpenWithState openState |> noCmd


updateClose : PartialUpdate model item msg
updateClose commonFeatures config model =
    CloseAndSelect (config.modelToMaybeSelection model) |> noCmd


updateHighlight : Maybe String -> PartialUpdate model item msg
updateHighlight maybeHighlightId commonFeatures config model =
    let
        openState =
            { openModel
                | maybeHighlightId = maybeHighlightId
                , searchString = ""
            }

        cmd =
            scrollToHighlight commonFeatures openState
    in
        ( OpenWithState (OpenState openState), cmd )


updateSelect : Maybe String -> PartialUpdate model item msg
updateSelect maybeItemId commonFeatures config model =
    CloseAndSelect (findItem config model maybeItemId) |> noCmd


updateSearchStringTimeout : Char -> PartialUpdate model item msg
updateSearchStringTimeout searchChar commonFeatures config model =
    let
        oldOpenState =
            maybeOpenState config model
                |> Maybe.withDefault openModel

        searchCounter =
            oldOpenState.searchCounter + 1

        searchString =
            -- Manage backspace character
            if searchChar == '\x08' then
                String.dropRight 1 oldOpenState.searchString
            else
                oldOpenState.searchString ++ String.toLower (String.fromChar searchChar)

        matchesSearchString item =
            String.startsWith searchString (config.itemToLabel item |> String.toLower)

        maybeHighlightId =
            List.Extra.find matchesSearchString (config.modelToItems model)
                |> Maybe.map config.itemToId
                |> maybeFallback oldOpenState.maybeHighlightId

        openState =
            { oldOpenState
                | maybeHighlightId = maybeHighlightId
                , searchCounter = searchCounter
                , searchString = searchString
            }

        cmdTimeout =
            Process.sleep (1.0 * Time.second)
                |> Task.perform (\() -> OnResetSearchString searchCounter)

        cmdHighlight =
            scrollToHighlight commonFeatures openState

        cmd =
            Cmd.batch
                [ cmdHighlight
                , cmdTimeout
                ]
    in
        ( OpenWithState (OpenState openState), cmd )


updatePartial : Config model item msg -> model -> Msg -> PartialUpdate model item msg
updatePartial config model msg =
    let
        pickHighlight =
            pick config model >> updateHighlight
    in
        case msg of
            NoOp ->
                updateNoChange

            OnClickCurrentSelection ->
                case maybeOpenState config model of
                    Nothing ->
                        model |> config.modelToMaybeSelection |> Maybe.map config.itemToId |> updateHighlight

                    Just openState ->
                        updateClose

            OnClickItem itemId ->
                updateSelect (Just itemId)

            OnMouseEnterItem itemId ->
                case maybeOpenState config model of
                    Nothing ->
                        updateNoChange

                    Just openState ->
                        Just itemId |> updateHighlight

            OnClickClear ->
                updateSelect Nothing

            OnTransitionEnd ->
                case maybeOpenState config model of
                    Nothing ->
                        updateNoChange

                    Just openState ->
                        openState.maybeHighlightId |> updateHighlight

            OnResetSearchString searchCounter ->
                case maybeOpenState config model of
                    Nothing ->
                        updateNoChange

                    Just openState ->
                        if openState.searchCounter /= searchCounter then
                            updateNoChange
                        else
                            updateHighlight openState.maybeHighlightId

            OnBlur ->
                updateClose

            OnKey Esc ->
                updateClose

            OnKey Enter ->
                case maybeOpenState config model of
                    Nothing ->
                        maybeSelectionId config model |> updateHighlight

                    Just openState ->
                        updateSelect openState.maybeHighlightId

            OnKey Space ->
                maybeSelectionId config model |> updateHighlight

            OnKey ArrowUp ->
                pickHighlight (Tuple.mapSecond List.reverse >> pickerNext)

            OnKey ArrowDown ->
                pickHighlight pickerNext

            OnKey PageUp ->
                pickHighlight (Tuple.mapSecond List.reverse >> (pickerSkip 9))

            OnKey PageDown ->
                pickHighlight (pickerSkip 9)

            OnKey Home ->
                pickHighlight (Tuple.second >> List.head)

            OnKey End ->
                pickHighlight (Tuple.second >> List.reverse >> List.head)

            OnKey (Searchable char) ->
                updateSearchStringTimeout char



-- Update


{-| -}
update : CommonFeatures -> Config model item msg -> Setters model item msg -> Msg -> model -> ( model, Cmd msg )
update commonFeatures config setters msg model =
    let
        partial =
            updatePartial config model msg

        ( outcome, dropdownCmd ) =
            partial commonFeatures config model

        ( newParentModel, userCmd ) =
            case outcome of
                OpenWithState openState ->
                    setters.openOnlyThisDropdown openState model

                CloseAndSelect maybeItem ->
                    let
                        updateSelection =
                            if maybeItem /= config.modelToMaybeSelection model then
                                setters.setCurrentSelection maybeItem
                            else
                                noCmd
                    in
                        model
                            |> setters.closeAllDropdowns
                            |> updateSelection

        cmd =
            Cmd.batch
                [ dropdownCmd |> Cmd.map config.msgWrapper
                , userCmd
                ]
    in
        ( newParentModel, cmd )



-- Key decoder


keyDecoder : Config model item msg -> model -> Int -> Json.Decode.Decoder Key
keyDecoder config model keyCode =
    let
        -- This is necessary to ensure that the key is not consumed and can propagate to the parent
        pass =
            Json.Decode.fail ""

        key =
            Json.Decode.succeed
    in
        case keyCode of
            13 ->
                key Enter

            27 ->
                -- Consume Esc only if the Menu is open
                if maybeOpenState config model == Nothing then
                    pass
                else
                    key Esc

            32 ->
                key Space

            33 ->
                key PageUp

            34 ->
                key PageDown

            35 ->
                key End

            36 ->
                key Home

            38 ->
                key ArrowUp

            40 ->
                key ArrowDown

            _ ->
                let
                    char =
                        Char.fromCode keyCode

                    -- TODO should the user be able to search non-alphanum chars?
                    -- TODO add support for non-ascii alphas
                    isAlpha char =
                        (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
                in
                    -- Backspace is "searchable" because it can be used to modify the search string
                    if isAlpha char || Char.isDigit char || char == '\x08' then
                        key (Searchable char)
                    else
                        pass



-- View


htmlNeverToHtmlMsg : Html Never -> Html Msg
htmlNeverToHtmlMsg =
    Html.map (always NoOp)


viewItem : CommonFeatures -> Config model item msg -> model -> Maybe String -> item -> Html Msg
viewItem commonFeatures config model maybeHighlightId item =
    let
        isSelected =
            case maybeSelectionId config model of
                Just selectionId ->
                    config.itemToId item == selectionId

                Nothing ->
                    False

        isHighlighted =
            case maybeHighlightId of
                Just highlightId ->
                    config.itemToId item == highlightId

                Nothing ->
                    False

        classes =
            Html.Attributes.classList
                [ ( commonFeatures.classes.menuOption, True )
                , ( commonFeatures.classes.menuOptionSelected, isSelected )
                , ( commonFeatures.classes.menuOptionHighlighted, isHighlighted )
                ]
    in
        li
            [ classes
            , Html.Events.onClick <| OnClickItem <| config.itemToId item
            , Html.Events.on "mousemove" <| Json.Decode.succeed <| OnMouseEnterItem <| config.itemToId item
            , Html.Attributes.id <| itemToDomId config item
            ]
            [ config.itemToHtml isSelected isHighlighted item |> htmlNeverToHtmlMsg
            ]


viewSelection : CommonFeatures -> Config model item msg -> model -> Html Msg
viewSelection commonFeatures config model =
    let
        maybeSelId =
            maybeSelectionId config model

        currentSelection =
            case findItem config model maybeSelId of
                Nothing ->
                    div
                        [ class commonFeatures.classes.placeholder ]
                        [ config.placeholder ]

                Just item ->
                    div
                        [ class commonFeatures.classes.selection ]
                        [ config.itemToHtml False False item ]

        onClickNoBubble =
            Html.Events.onWithOptions "click" { stopPropagation = True, preventDefault = False } << Json.Decode.succeed

        clearIcon =
            if config.hasClearButton && maybeSelId /= Nothing then
                div
                    [ onClickNoBubble OnClickClear
                    , class commonFeatures.classes.clearButton
                    ]
                    [ commonFeatures.clearButton |> htmlNeverToHtmlMsg
                    ]
            else
                text ""
    in
        div
            [ class commonFeatures.classes.selectBox
            , Html.Events.onClick OnClickCurrentSelection
            ]
            [ currentSelection |> htmlNeverToHtmlMsg
            , clearIcon
            , div
                [ class commonFeatures.classes.downArrow ]
                [ commonFeatures.downArrow |> htmlNeverToHtmlMsg ]
            ]


{-| Setting the third argument to True will disable the dropdown.
-}
view : CommonFeatures -> Config model item msg -> Bool -> model -> Html msg
view commonFeatures config isDisabled model =
    if isDisabled then
        viewDisabled commonFeatures config model
    else
        viewEnabled commonFeatures config model


viewDisabled : CommonFeatures -> Config model item msg -> model -> Html msg
viewDisabled commonFeatures config model =
    div
        [ class commonFeatures.classes.root
        , class commonFeatures.classes.isClosed
        , class commonFeatures.classes.isDisabled
        ]
        [ viewSelection commonFeatures config model
        ]
        |> Html.map (\msg -> config.msgWrapper NoOp)


viewEnabled : CommonFeatures -> Config model item msg -> model -> Html msg
viewEnabled commonFeatures config model =
    let
        maybeHighlightId =
            model
                |> maybeOpenState config
                |> Maybe.andThen .maybeHighlightId

        menuItems =
            model
                |> config.modelToItems
                |> List.map (viewItem commonFeatures config model maybeHighlightId)

        classOpenOrClosed =
            if maybeOpenState config model == Nothing then
                commonFeatures.classes.isClosed
            else
                commonFeatures.classes.isOpen

        keyMsgDecoder =
            Html.Events.keyCode
                |> Json.Decode.andThen (keyDecoder config model)
                |> Json.Decode.map OnKey
    in
        div
            [ class commonFeatures.classes.root
            , class classOpenOrClosed
            , Html.Attributes.tabindex 0
            , Html.Events.onBlur OnBlur
            , Html.Events.onWithOptions "keydown" { stopPropagation = True, preventDefault = True } keyMsgDecoder
            ]
            [ viewSelection commonFeatures config model
            , div
                [ Html.Attributes.style
                    [ ( "position", "relative" ) ]
                ]
                [ ul
                    [ class commonFeatures.classes.menu
                    , Html.Events.on "transitionend" (Json.Decode.succeed OnTransitionEnd)
                    , Html.Events.on "animationend" (Json.Decode.succeed OnTransitionEnd)
                    , Html.Attributes.style
                        [ ( "position", "absolute" ) ]
                    ]
                    menuItems
                ]
            ]
            |> Html.map config.msgWrapper