Confidenceman02 / elm-select / Select

Select items from a menu list.

Set up


type SelectId


type Config item


type State


type MenuItem item


type alias BasicMenuItem item =
{ item : item, label : String }

A menu item that will be represented in the menu list.

The item property is the type representation of the menu item that will be used in an Action.

The label is the text representation that will be shown in the menu.

type Tool
    = Screwdriver
    | Hammer
    | Drill

toolItems : MenuItem Tool
toolItems =
    [ basicMenuItem { item = Screwdriver, label = "Screwdriver" }
    , basicMenuItem { item = Hammer, label = "Hammer" }
    , basicMenuItem { item = Drill, label = "Drill" }
    ]

yourView model =
    Html.map SelectMsg <|
        view
            (single Nothing
                |> menuItems toolItems
                |> state model.selectState
            )

Combine this with basicMenuItem to create a MenuItem

basicMenuItem : BasicMenuItem item -> MenuItem item

Create a basic type of MenuItem.

Use customMenuItem if you want more flexibility on how a menu item will look in the menu.

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    menuItems : List (MenuItem Tool)
    menuItems =
        [ basicMenuItem
            { item = Screwdriver, label = "Screwdriver" }
        , basicMenuItem
            { item = Hammer, label = "Hammer" }
        , basicMenuItem
            { item = Drill, label = "Drill" }
        ]


type alias CustomMenuItem item =
{ item : item
, label : String
, view : Html.Styled.Html Basics.Never 
}

A menu item that will be represented in the menu list by a view you supply.

The item property is the type representation of the menu item that will be used in an Action.

The label is the text representation of the item.

The view is a Html view that you supply.

type Tool
    = Screwdriver
    | Hammer
    | Drill

toolItems : MenuItem Tool
toolItems =
    [ customMenuItem { item = Screwdriver, label = "Screwdriver", view = text "Screwdriver" }
    , customMenuItem { item = Hammer, label = "Hammer", view = text "Hammer" }
    , customMenuItem { item = Drill, label = "Drill", view = text "Drill" }
    ]

yourView model =
    Html.map SelectMsg <|
        view
            (single Nothing
                |> menuItems toolItems
                |> state model.selectState
            )

The view you provide will be rendered in a li element that is styled according to the value set by setStyles.

    customMenuItem { item = Hammer, label = "Hammer", view = text "Hammer" }
    -- => <li>Hammer</>

Combine this with customMenuItem to create a MenuItem.

customMenuItem : CustomMenuItem item -> MenuItem item

Create a custom type of MenuItem.

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    menuItems : List (MenuItem Tool)
    menuItems =
        [ customMenuItem
            { item = Screwdriver, label = "Screwdriver", view = text "Screwdriver" }
        , customMenuItem
            { item = Hammer, label = "Hammer", view = text "Hammer" }
        , customMenuItem
            { item = Drill, label = "Drill", view = text "Drill" }
        ]


type Group

group : String -> Group

Create a MenuItem group to provide visual organisation for your menu items.

Use with groupedMenuItem to add a MenuItem to a group.

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    toolGroup : Group
    toolGroup =
      group "tool"

    menuItems : List (MenuItem Tool)
    menuItems =
      [ groupedMenuItem toolGroup
            ( basicMenuItem { item = Screwdriver, label = "Screwdriver" } )
      , groupedMenuItem toolGroup
            ( basicMenuItem { item = Hammer, label = "Hammer" } )
      , groupedMenuItem toolGroup
            ( basicMenuItem { item = Drill, label = "Drill" } )
      ]

groupedMenuItem : Group -> MenuItem item -> MenuItem item

Create a grouped MenuItem.

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    toolGroup : Group
    toolGroup =
      group "tool"

    menuItems : List (MenuItem Tool)
    menuItems =
        [ groupedMenuItem toolGroup
              ( customMenuItem
                    { item = Screwdriver
                    , label = "Screwdriver"
                    , view = text "Screwdriver"
                    }
              )
        , customMenuItem
            { item = Hammer, label = "Hammer", view = text "Hammer" }
        , customMenuItem
            { item = Drill, label = "Drill", view = text "Drill" }
        ]

groupStyles : Styles.GroupConfig -> Group -> Group

Create custom styling for a Group.

This will override global styles for this group when using setGroupStyles

    groupStyles : GroupConfig
    groupStyles =
        getGroupConfig default
            |> setGroupColor (Css.hex "#EEEEEE")

    toolGroup : Group
    toolGroup =
      group "tool"
          |> groupStyles groupStyles

    menuItems : List (MenuItem Tool)
    menuItems =
      [ groupedMenuItem toolGroup
            ( basicMenuItem { item = Screwdriver, label = "Screwdriver" } )
      , groupedMenuItem toolGroup
            ( basicMenuItem { item = Hammer, label = "Hammer" } )
      , groupedMenuItem toolGroup
            ( basicMenuItem { item = Drill, label = "Drill" } )
      ]

groupView : Html.Styled.Html Basics.Never -> Group -> Group

Create a custom view for a Group.

    customView : Html Never
    customView =
      text "My custom group"

    customGroup : Group
    customGroup =
      group "tool"
          |> groupView customView

    menuItems : List (MenuItem Tool)
    menuItems =
      [ groupedMenuItem customGroup
            ( basicMenuItem { item = Screwdriver, label = "Screwdriver" } )
      , groupedMenuItem customGroup
            ( basicMenuItem { item = Hammer, label = "Hammer" } )
      , groupedMenuItem customGroup
            ( basicMenuItem { item = Drill, label = "Drill" } )
      ]

filterableMenuItem : Basics.Bool -> MenuItem item -> MenuItem item

Choose whether a menu item is filterable.

Useful for when you always want to have a selectable option in the menu.

Menu items are filterable by default.

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    menuItems : List (MenuItem Tool)
    menuItems =
        [ customMenuItem
            { item = Screwdriver, label = "Screwdriver", view = text "Screwdriver" }
        , customMenuItem
            { item = Hammer, label = "Hammer", view = text "Hammer" }
        , customMenuItem
            { item = Drill, label = "Drill", view = text "Drill" }
            |> filterableMenuItem False
        ]

NOTE: This only takes effect when searchable is True.

dismissibleMenuItemTag : Basics.Bool -> MenuItem item -> MenuItem item

Choose whether a selected menu item tag can produce a Deselect action.

This affects the multi Variant and is useful for when you want a selected tag to not be individually dismissible.

The tag will not render a dismiss button if False.

default: True

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    menuItems : List (MenuItem Tool)
    menuItems =
        [ customMenuItem
            { item = Screwdriver, label = "Screwdriver", view = text "Screwdriver" }
        , customMenuItem
            { item = Hammer, label = "Hammer", view = text "Hammer" }
        , customMenuItem
            { item = Drill, label = "Drill", view = text "Drill" }
            |> dismissibleMenuItemTag False
        ]

stylesMenuItem : Styles.MenuItemConfig -> MenuItem item -> MenuItem item

Set individual styles for a menu item.

These styles will override any global menu item styles set via setStyles.

To set a global style for all menu items use setStyles.

    import Styles exposing (MenuItemConfig, default, getMenuItemConfig)
    import Css

    type Tool
        = Screwdriver
        | Hammer
        | Drill

    drillStyles : MenuItemConfig
    drillStyles =
        getMenuItemConfig default
            |> setMenuItemColorHoverSelected (Css.hex "#EEEEEE")

    menuItems : List (MenuItem Tool)
    menuItems =
        [ customMenuItem
            { item = Screwdriver, label = "Screwdriver", view = text "Screwdriver" }
        , customMenuItem
            { item = Drill, label = "Drill", view = text "Drill" }
            |> stylesMenuItem drillStyles
        ]

valueMenuItem : String -> MenuItem item -> MenuItem item

Explicitly set the value attribute for the input form control.

This is handy for when you are submitting a form and your server is expecting a value that is different from the label, like a database id.

Take the following selection

  <option value="2" selected>Pagani BC</option>

When a form is submitted the server will see:

  something: "2"

instead of:

  something: "Pagani BC"

By default, the value attribute will be populated with the MenuItem label.

  type Tool
      = Screwdriver
      | Hammer
      | Drill

  menuItems : List (MenuItem Tool)
  menuItems =
      [ customMenuItem
          { item = Hammer, label = "Hammer", view = text "Hammer" }
          |> valueMenuItem "2"
      , customMenuItem
          { item = Drill, label = "Drill", view = text "Drill" }
          |> valueMenuItem "3"
      ]


type Action item
    = InputChange String
    | Select item
    | SelectBatch (List item)
    | Deselect (List item)
    | Clear
    | FocusSet
    | Focus
    | Blur

Specific events happen in the Select that you can react to from your update.

Maybe you want to find out what country someone is from?

When they select a country from the menu, it will be reflected in the Select action.

import Select exposing ( Action(..) )

type Msg
    = SelectMsg (Select.Msg Country)
    -- your other Msg's

type Country
    = Australia
    | Japan
    | Taiwan
    -- other countries

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        SelectMsg selectMsg ->
            let
                (maybeAction, selectState, selectCmds) =
                    Select.update selectMsg model.selectState

                selectedCountry : Maybe Country
                selectedCountry =
                    case maybeAction of
                        Just (Select.Select someCountry) ->
                            Just someCountry

                        Nothing ->
                            Nothing

            in
            -- (model, cmd)

NOTE: Multi native variants use the SelectBatch action to determine selections.

initState : SelectId -> State

Set up an initial state in your init function.

type Country
    = Australia
    | Japan
    | Taiwan

type alias Model =
    { selectState : State
    , items : List (MenuItem Country)
    , selectedCountry : Maybe Country
    }

init : Model
init =
    { selectState = initState (selectIdentifier "country-select")
    , items =
        [ basicMenuItem
            { item = Australia, label = "Australia" }
        , basicMenuItem
            { item = Japan, label = "Japan" }
        , basicMenuItem
            { item = Taiwan, label = "Taiwan" }
        ]
    , selectedCountry = Nothing
    }

keepMenuOpen : Basics.Bool -> State -> State

Keeps the menu open at all times.

Use this with care as all actions that normally close the menu like selections, or escape, or clicking away will not close it.

focus : Msg item

Opens the menu and sets focus on the Variant.

Handy when using a menu Variant as dropdown.

  yourUpdate : (model, Cmd msg )
  yourUpdate msg model =
      case msg of
          FocusTheSelect ->
            let
              ( actions, updatedState, cmds ) =
                  update focus model.selectState
            in
            ({ model | selectState = updatedState }, Cmd.map SelectMsg cmds)

NOTE: Successfull focus will dispatch the FocusSet Action

isFocused : State -> Basics.Bool

Check to see that the variant has focus.

This will return true if any focusable element inside the control has focus i.e. If the clear button is visible and has focus.

  yourUpdate : (State, Cmd msg )
  yourUpdate msg state =
      case msg of
          SelectMsg msg ->
              let
                ( actions, updatedState, cmds ) =
                    update msg state
              in
              if isFocused updatedState then
                (updatedState, makeSomeRequest)
              else
                (updatedState, Cmd.none)

isMenuOpen : State -> Basics.Bool

Check that the menu is open and visible.

  yourUpdate : (State, Cmd msg )
  yourUpdate msg state =
      case msg of
          SelectMsg msg ->
            let
              ( actions, updatedState, cmds ) =
                  update msg state
            in
            if isFocused updatedState && isMenuOpen updatedState then
              (updatedState, makeSomeRequest)

            else
              (updatedState, Cmd.none)


type Msg item

menuItems : List (MenuItem item) -> Config item -> Config item

The items that will appear in the menu list.

NOTE: When using the (multi) select, selected items will be reflected as a tags and visually removed from the menu list.

  items =
      [ basicMenuItem
          { item = SomeValue, label = "Some label" }
      ]

  yourView =
      view
          (Single Nothing |> menuItems items)

clearable : Basics.Bool -> Config item -> Config item

Allows a single variant selected menu item to be cleared.

To handle a cleared item refer to the ClearedSingleSelect action.

  items =
      [ basicMenuItem
          { item = SomeValue, label = "Some label" }
      ]

    yourView model =
        Html.map SelectMsg <|
            view
                ( single Nothing
                    |> clearable True
                    |> menuItems items
                )

placeholder : String -> Config item -> Config item

The text that will appear as an input placeholder.

  yourView model =
      Html.map SelectMsg <|
          view
              (single Nothing |> placeholder "some placeholder")

selectIdentifier : String -> SelectId

The ID for the rendered Select input

NOTE: It is important that the ID's of all selects that exist on a page remain unique so I add some extra stuff at the end of the String you provide to help out.

If you don't want this see staticSelectIdentifier.

Illegal id characters will be replaced with "_".

init : State
init =
    initState (selectIdentifier "someUniqueId")

staticSelectIdentifier : String -> SelectId

A static ID for the rendered Select input

The exact string you pass will be the ID used internally.

This is handy when you want a label tags for attribute to match the variant id without needing to remember to add the extra stuff.

See also selectIdentifier.

Illegal id characters will be replaced with "_".

init : State
init =
    initState (staticSelectIdentifier "someUniqueStaticId")

state : State -> Config item -> Config item

The select state.

This is usually persisted in your model.

  model : Model
  model =
      { selectState = initState }

  yourView : Model
  yourView model =
      Html.map SelectMsg <|
          view
              (single Nothing
                  |> state model.selectState
              )

update : Msg item -> State -> ( Maybe (Action item), State, Platform.Cmd.Cmd (Msg item) )

Add a branch in your update to handle the view Msg's.

  yourUpdate msg model =
      case msg of
          SelectMsg selectMsg ->
              update selectMsg model.selectState

view : Config item -> Html.Styled.Html (Msg item)

Render the select

  yourView model =
      Html.map SelectMsg <|
          view (single Nothing)

searchable : Basics.Bool -> Config item -> Config item

Renders an input that let's you input text to search for menu items.

  yourView model =
      Html.map SelectMsg <|
          view
              (single Nothing |> searchable True)

NOTE: This doesn't affect the Native single select variant.

setStyles : Styles.Config -> Config item -> Config item

Change some of the visual styles of the select.

Useful for styling the select using your color branding.

  import Select.Styles as Styles

  baseStyles : Styles.Config
  baseStyles =
      Styles.default

  controlBranding : Styles.ControlConfig
  controlBranding =
      Styles.getControlConfig baseStyles
          |> Styles.setControlBorderColor (Css.hex "#FFFFFF")
          |> Styles.setControlBorderColorFocus (Css.hex "#0168B3")

  selectBranding : Styles.Config
  selectBranding =
    baseStyles
        |> Styles.setControlStyles controlBranding

  yourView model =
      Html.map SelectMsg <|
          view
              (single Nothing |> setStyles selectBranding)

name : String -> Config item -> Config item

The name attribute of a native select variant

A form will need this attribute to know how to label the data.

yourView model =
    label
        [ id "selectLabelId" ]
        [ text "Select your country"
        , Html.map SelectMsg <|
            view
                (singleNative Nothing
                    |> name "country"
                )
        ]

Single select

single : Maybe (MenuItem item) -> Config item

Select a single item.

  countries : List (MenuItem Country)
  countries =
      [ basicMenuItem
          { item = Australia, label = "Australia" }
      , basicMenuitem
          { item = Taiwan, label = "Taiwan"
        -- other countries
      ]

  yourView =
      Html.map SelectMsg <|
          view
              (single Nothing |> menuItems countries)

Menu select

singleMenu : Maybe (MenuItem item) -> Config item

Menu only single select.

  countries : List (MenuItem Country)
  countries =
      [ basicMenuItem
          { item = Australia, label = "Australia" }
      , basicMenuitem
          { item = Taiwan, label = "Taiwan"
        -- other countries
      ]

  yourView =
      Html.map SelectMsg <|
          view
            (singleMenu Nothing |> menuItems countries)

NOTE: By default the menu will not render until it is focused and interacted with. This is for accessibility reasons.

You can use focus to open and focus the menu if you are using this variant as a dropdown.

menu : Config item

Menu only select.

Unlike a singleMenu this variant does not accept or display options as selected.

Useful when you want to know what someone has selected like a list of settings or options.

  actions : List (MenuItem Actions)
  actions =
      [ basicMenuItem
          { item = Update, label = "Update" }
      , basicMenuitem
          { item = Delete, label = "Delete"
        -- other actions
      ]

  yourView =
      Html.map SelectMsg <|
          view
            (menu |> menuItems actions)

NOTE: By default the menu will not render until it is focused and interacted with. This is for accessibility reasons.

You can use focus to open and focus the menu if you are using this variant as a dropdown.

Multi select

multi : List (MenuItem item) -> Config item

Select multiple items.

Selected items will render as tags and be visually removed from the menu list.

yourView model =
    Html.map SelectMsg <|
        view
            (multi model.selectedCountries
                |> menuItems model.countries
            )

Native Single select

singleNative : Maybe (MenuItem item) -> Config item

Select a single item with a native html select element.

Useful for when you want to give a native select experience such as on touch devices.

  countries : List (MenuItem Country)
  countries =
      [ basicMenuItem
          { item = Australia, label = "Australia" }
      , basicMenuItem
          { item = Taiwan, label = "Taiwan"
      -- other countries
      ]

  yourView =
      Html.map SelectMsg <|
          view
              (singleNative Nothing |> menuItems countries)

Note

Native Multi select

multiNative : List (MenuItem item) -> Config item

Select multiple items with a native html select element.

Useful for when you want to give a native select experience such as on touch devices.

  countries : List (MenuItem Country)
  countries =
      [ basicMenuItem
          { item = Australia, label = "Australia" }
      , basicMenuItem
          { item = Taiwan, label = "Taiwan"
      -- other countries
      ]

  yourView =
      Html.map SelectMsg <|
          view
              (multiNative [] |> menuItems countries)

Note

Common

disabled : Basics.Bool -> Config item -> Config item

Disables the select input so that it cannot be interacted with.

    yourView model =
        Html.map SelectMsg <|
            view
                (single Nothing |> disabled True)

labelledBy : String -> Config item -> Config item

The element ID of the label for the select.

It is best practice to render the select with a label.

yourView model =
    label
        [ id "selectLabelId" ]
        [ text "Select your country"
        , Html.map SelectMsg <|
            view
                (single Nothing |> labelledBy "selectLabelId")
        ]

ariaDescribedBy : String -> Config item -> Config item

The ID of an element that describes the select.

yourView model =
    label
        [ id "selectLabelId" ]
        [ text "Select your country"
        , Html.map SelectMsg <|
            view
                (single Nothing
                    |> labelledBy "selectLabelId"
                    |> ariaDescribedBy "selectDescriptionId"
                )
        , div [ id "selectDescriptionId" ] [ text "This text describes the select" ]
        ]

loading : Basics.Bool -> Config item -> Config item

Displays an animated loading icon to visually represent that menu items are being loaded.

This would be useful if you are loading menu options asynchronously, like from a server.

    yourView model =
        Html.map SelectMsg <|
            view
                (single Nothing |> loading True)

loadingMessage : String -> Config item -> Config item

Displays when there are no matched menu items and loading is True.

    yourView model =
        Html.map SelectMsg <|
            view
                (single Nothing |> loadingMessage "Fetching items...")

Advanced

jsOptimize : Basics.Bool -> State -> State

Opt in to a Javascript optimization.

Read the Advanced section of the README for a good explanation on why you might like to opt in.

    model : Model model =
        { selectState =
            initState (selectIdentifier "some-unique-id")
                    |> jsOptimize True
        }

Install the Javascript package:

npm

npm install @confidenceman02/elm-select

Import script

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Viewer</title>

    <script src="/node_modules/@confidenceman02/elm-select/dist/dynamic.min.js"></script>
  </head>
  <body>
    <main></main>
    <script src="index.js"></script>
  </body>
</html>

Alternatively you can import the script wherever you are initialising your program.

import { Elm } from "./src/Main";
import "@confidenceman02/elm-select"

Elm.Main.init({node, flags})