phollyer / elm-ui-dropdown / Dropdown

A dropdown component for Elm-UI.

In order to provide built-in keyboard navigation, and option filtering, it is necessary for the Dropdown to manage it's own internal state. Therefore any Dropdowns you require need to be stored on your Model, with events being handled in your update function.

There are a few gotchas to watch out for with functions that operate on the internal state of the Dropdown. Because of the effect they have on the internal state, using them in your view code will have no effect. They should therefore be used when you init the Dropdown, or in your update function where model changes can be captured.

The affected functions are, id, filterType, setSelected, removeSelected, [removeOption](#removeOption] & openOnMouseEnter, along with all the functions for setting the menu options. Each function or section has a warning documenting this restriction where it is applicable.

All other functions can be used safely within view code.

Build

Setting Up


type Dropdown option

An opaque type representing the internal model.

Use this to define the option type on your model.

import Dropdown exposing (Dropdown)

type alias Model =
    { stringDropdown : Dropdown String
    , intDropdown : Dropdown Int
    , customTypeDropdown : Dropdown CustomType
    }

type CustomType
    = A
    | B
    ...

init : Dropdown option

Initialize a dropdown on your model.

import Dropdown

initialModel : Model
initialModel =
    { myDropdown = Dropdown.init }

id : String -> Dropdown option -> Dropdown option

Provide an ID for the dropdown.

This will become the element id in the DOM, and is required in order for keyboard navigation to work - it should therefore be unique.

import Dropdown

initialModel : Model
initialModel =
    { myDropdown =
        Dropdown.init
            |> Dropdown.id "my-drodown"
    }

Warning

The id needs to be stored on the dropdown model, and so should be set when you init the dropdown, or in your update function where the changes to the model can be captured.

If you set this in your view code it will have no effect and keyboard navigation won't work.

Input Type


type InputType
    = Button
    | TextField

The type of input the user uses to access the dropdown.

The default is Button.

inputType : InputType -> Dropdown option -> Dropdown option

Set the InputType.

Setting Options

Warning

Options need to be stored on the dropdown model, and so should be set when you init the dropdown, or in your update function where the changes to the model can be captured.

If you set these in your view code they will have no effect and so no menu will appear.

optionsBy : (option -> String) -> List option -> Dropdown option -> Dropdown option

This is the easiest way to set the options for custom types.

Simply provide a function that takes an option and returns the String to be used for the label in the menu.

import Dropdown exposing (Dropdown)

type alias Model =
    { nameDropdown : Dropdown Person
    , ageDropdown : Dropdown Person
    }

initialModel : Model
initialModel =
    { nameDropdown =
        Dropdown.init
            |> Dropdown.optionsBy .name people
    , ageDropdown =
        Dropdown.init
            |> Dropdown.optionsBy (.age >> String.fromInt) people
    }

type alias Person =
    { name : String
    , age : Int
    }

people : List Person
people =
    [ { name = "John Doe", age = 99 }
    , { name = "Jane Doe", age = 98 }
    ]

options : List ( String, option ) -> Dropdown option -> Dropdown option

The options to set for your dropdown.

The first element in the list of tuples is always a String, and is used for the option's label in the menu that is displayed to the user.

import Dropdown exposing (Dropdown)

type alias Model =
    { customTypeDropdown : Dropdown CustomType }

initialModel : Model
initialModel =
    { customTypeDropdown =
        Dropdown.init
            |> Dropdown.options customTypeOptions
    }

type CustomType
    = A
    | B

customTypeOptions : List ( String, CustomType )
customTypeOptions =
    [ ( "A", A )
    , ( "B", B )
    ]

stringOptions : List String -> Dropdown String -> Dropdown String

The options to set for your dropdown if they are all Strings.

import Dropdown exposing (Dropdown)

type alias Model =
    { stringDropdown : Dropdown String }

initialModel : Model
initialModel =
    { stringDropdown =
        Dropdown.init
            |> Dropdown.stringOptions
                [ "A"
                , "B"
                , "C"
                ]
    }

intOptions : List Basics.Int -> Dropdown Basics.Int -> Dropdown Basics.Int

The options to set for your dropdown if they are all Ints.

import Dropdown exposing (Dropdown)

type alias Model =
    { stringDropdown : Dropdown Int }

initialModel : Model
initialModel =
    { stringDropdown =
        Dropdown.init
            |> Dropdown.intOptions
                [ 1
                , 2
                , 3
                ]
    }

floatOptions : List Basics.Float -> Dropdown Basics.Float -> Dropdown Basics.Float

The options to set for your dropdown if they are all Floats.

import Dropdown exposing (Dropdown)

type alias Model =
    { stringDropdown : Dropdown Float }

initialModel : Model
initialModel =
    { stringDropdown =
        Dropdown.init
            |> Dropdown.floatOptions
                [ 0.1
                , 0.2
                , 0.3
                ]
    }

reset : Dropdown option -> Dropdown option

Reset the dropdown.

The selected option will be set to Nothing, and any text in the TextField InputType will be removed.

The list of options will be reset to the last full list of options supplied if any options have been programmatically removed.

Label

label : Element (Msg option) -> Dropdown option -> Dropdown option

Provide the label element for the InputType.

labelHidden : ( Basics.Bool, String ) -> Dropdown option -> Dropdown option

Hide the label.

buttonLabel : Element (Msg option) -> Dropdown option -> Dropdown option

The text element for the Button if nothing is selected.

The default is "-- Select --".

Placeholder

placeholder : Maybe (Element.Input.Placeholder (Msg option)) -> Dropdown option -> Dropdown option

Provide the Placeholder for the text field if TextField is the InputType.

The default is Nothing.

Positioning


type Placement
    = Above
    | Below
    | Left
    | Right

The position of the label or the dropdown menu in relation to the InputType.

labelPlacement : Placement -> Dropdown option -> Dropdown option

Set the position of the label in relation to the InputType.

The default is Above.

labelSpacing : Basics.Int -> Dropdown option -> Dropdown option

Set the spacing between the InputType and its label.

The default is 10.

menuPlacement : Placement -> Dropdown option -> Dropdown option

Set the position of the dropdown menu in relation to the InputType.

The default is Below.

menuSpacing : Basics.Int -> Dropdown option -> Dropdown option

Set the spacing between the InputType and the menu.

The default is 0.

Size & Style

maxHeight : Basics.Int -> Dropdown option -> Dropdown option

The max height for the dropdown, the default is 150.

(Vertical scrolling kicks in automatically.)

inputAttributes : List (Element.Attribute (Msg option)) -> Dropdown option -> Dropdown option

The Attributes to set on the InputType.

menuAttributes : List (Element.Attribute (Msg option)) -> Dropdown option -> Dropdown option

The Attributes to set on the menu container.

optionAttributes : List (Element.Attribute (Msg option)) -> Dropdown option -> Dropdown option

The Attributes to set on each option.

optionHoverAttributes : List (Element.Attribute (Msg option)) -> Dropdown option -> Dropdown option

The Attributes to set on an option when hovered over with the mouse, or navigated to with the keyboard.

optionSelectedAttributes : List (Element.Attribute (Msg option)) -> Dropdown option -> Dropdown option

The Attributes to set on an option when it has been selected by a user.

Filtering

Filtering is currently case insensitive.


type FilterType
    = NoFilter
    | StartsWith
    | Contains
    | StartsWithThenContains

The type of filter to apply when TextField is used as the InputType.

The default is NoFilter.

filterType : FilterType -> Dropdown option -> Dropdown option

Set the FiterType.

import Dropdown exposing (FilterType(..))

initialModel : Model
initialModel =
    { dropdown =
        Dropdown.init
            |> Dropdown.filterType StartsWith
    }

Warning

The FilterType needs to be stored on the dropdown model, and so should be set when you init the dropdown, or in your update function where the changes to the model can be captured.

If you set this in your view code it will have no effect, and filtering won't work.

Selected Option

setSelected : Maybe option -> Dropdown option -> Dropdown option

Set the selected option - it must exist in the list of options originally provided.

Warning

This function changes the internal state, and so needs to be used where the state change can be captured. This is likely to be your update function.

If you use this in your view code it will have no effect.

removeSelected : Dropdown option -> Dropdown option -> Dropdown option

Remove the selected option of one dropdown from the list of options of another dropdown.

This is useful if you have two dropdowns that show the same list of options, but each selection must be unique, therefore you don't want to show the selected option again.

For example, selecting a home team and away team from the same list of teams. In this case, once the home team has been selected, you may wish to remove that option from the list of away teams.

awayTeamDropdown =
    Dropdown.removeSelected homeTeamDropdown awayTeamDropdown

Warning

This function changes the internal state, and so needs to be used where the state change can be captured. This is likely to be your update function.

If you use this in your view code it will have no effect.

removeOption : option -> Dropdown option -> Dropdown option

Remove an option from the internal list.

Warning

This function changes the internal state, and so needs to be used where the state change can be captured. This is likely to be your update function.

If you use this in your view code it will have no effect.

Selected Label

setSelectedLabel : Maybe String -> Dropdown option -> Dropdown option

Set the selected label - it must exist in the list of options originally provided.

Warning

This function changes the internal state, and so needs to be used where the state change can be captured. This is likely to be your update function.

If you use this in your view code it will have no effect.

Controls

openOnMouseEnter : Basics.Bool -> Dropdown option -> Dropdown option

Choose whether the menu opens when the mouse enters.

If this is set to True the menu will also close automatically when the mouse leaves.

The default is False.

Warning

This function changes the internal state, and so needs to be used where the state change can be captured. This is likely to be your update function.

If you use this in your view code it will have no effect.

open : Dropdown option -> Dropdown option

close : Dropdown option -> Dropdown option

Query

selected : Dropdown option -> Maybe ( Basics.Int, String, option )

Determine if an option has been selected by the user.

If a Just is returned, it consists of the following:

selectedOption : Dropdown option -> Maybe option

Maybe retrieve the selected option.

selectedLabel : Dropdown option -> Maybe String

Maybe retrieve the label for the selected option.

list : Dropdown option -> List ( Basics.Int, String, option )

List all the option information. The tuples returned represent:

listOptions : Dropdown option -> List option

List all the options.

listLabels : Dropdown option -> List String

List all the labels for each option.

text : Dropdown option -> String

Get the text entered in the TextField.

isOpen : Dropdown option -> Basics.Bool

Determine if the dropdown is open or not.

getId : Dropdown option -> String

Get the id of the dropdown.

Update


type OutMsg option
    = NoOp
    | Selected (( Basics.Int, String, option ))
    | TextChanged String
    | FocusIn
    | FocusOut
    | Opened
    | Closed

Pattern match on this type in your update function to determine the event that occured.


type Msg option

This is an opaque type, pattern match on OutMsg.

update : Msg option -> Dropdown option -> ( Dropdown option, Platform.Cmd.Cmd (Msg option), OutMsg option )

import Dropdown exposing (OutMsg(..))

type Msg
    = DropdownMsg Dropdown.Msg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        DropdownMsg subMsg ->
            let
                ( dropdown, cmd, outMsg ) =
                    Dropdown.update subMsg model.dropdown
            in
            case outMsg of
                Selected ( index, label, option ) ->
                    ( { model | dropdown = dropdown }
                    , Cmd.map DropdownMsg cmd
                    )

                TextChanged text ->
                    ( { model | dropdown = dropdown }
                    , Cmd.map DropdownMsg cmd
                    )

                ...

Subscriptions

subscriptions : Dropdown option -> Platform.Sub.Sub (Msg option)

Subscribe to the browser onResize event.

When the orientation changes on some mobile devices the dropdown can lose focus, resulting in it failing to close if the user taps outside the dropdown.

Subscribing to this subscription results in the dropdown regaining focus when the orientation changes so that the user experience doesn't change.

This subscription is only active when the dropdown is open.

View

view : (Msg option -> msg) -> Dropdown option -> Element msg

Render the dropdown.

import Dropdown

type alias Model =
    { dropdown : Dropdown String }

type Msg
    = DropdownMsg Dropdown.Msg

view : Model -> Element Msg
view model =
    Dropdown.view DropdownMsg model.dropdown