A select widget for elm-ui.
Internal.Model.Model a
The main Select type
init : String -> Select a
Initialise the Select. You must provide a unique id. The id will be used for getting DOM elements etc.
setItems : List a -> Select a -> Select a
Set the list of items
You can do this on init:
type alias Model =
{ select : Select String
}
init : List String -> ( Model, Cmd Msg )
init things =
( { select =
Select.init "thing-select"
|> Select.setItems things
}
, Cmd.none
)
Or you can do it in your view if you need to keep your items in your own model. You'd probably only do this if you want to select from things that are owned/managed by other parts of the app.
view : Model -> Element Msg
view model =
Select.view
|> Select.toElement []
{ select = Select.setItems model.things model.select
, onChange = SelectMsg
, itemToString = identity
, label = Input.labelAbove [] (Element.text "Choose a thing")
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
}
setSelected : Maybe a -> Select a -> Select a
Set the selected item
setInputValue : String -> Select a -> Select a
Set the input value
openMenu : Select a -> Select a
Open the menu programatically (You probably don't need to use this)
closeMenu : Select a -> Select a
Close the menu programatically (You probably don't need to use this)
toValue : Select a -> Maybe a
Get the selected item
toInputValue : Select a -> String
Get the value of the input
toInputElementId : Select a -> String
Get the id of the DOM input element. Useful in tests or to associate a label with the input
toMenuElementId : Select a -> String
Get the id of the DOM menu element. Useful for testing
isMenuOpen : Select a -> Basics.Bool
Is the menu open?
isLoading : Select a -> Basics.Bool
Is there a request currently loading? Could be used to add loading styling.
isRequestFailed : Select a -> Basics.Bool
Did a request fail?
isFocused : Select a -> Basics.Bool
Is the input focused?
Internal.Msg.Msg a
The Msg type
update : (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Platform.Cmd.Cmd msg )
Update the Select
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SelectMsg subMsg ->
Select.update SelectMsg subMsg model.select
|> Tuple.mapFirst (\select -> { model | select = select })
If you'd like to use the Effects pattern, see Select.Effect
Internal.UpdateOptions.UpdateOption err (Platform.Cmd.Cmd msg) a msg
Options for use with updateWith.
updateWith : List (UpdateOption err a msg) -> (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Platform.Cmd.Cmd msg )
Update with options.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SelectMsg subMsg ->
Select.updateWith [ Select.onSelectedChanged ThingSelected ] SelectMsg subMsg model.select
|> Tuple.mapFirst (\select -> { model | select = select })
ThingSelected maybeThing ->
Debug.todo "Do something when the thing is selected/deselected"
request : (String -> (Result err (List a) -> msg) -> Platform.Cmd.Cmd msg) -> UpdateOption err a msg
Use an HTTP request to retrieve matching remote results. Note that in order to avoid an elm/http dependency in this package, you will need to provide the request Cmd yourself. Provide a function that takes the input value and a msg tagger and returns a Cmd. Update will return this Cmd when the user types in the input.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SelectMsg subMsg ->
Select.updateWith [ Select.request fetchThings ] SelectMsg subMsg model.select
|> Tuple.mapFirst (\select -> { model | select = select })
fetchThings : String -> (Result Http.Error (List Thing) -> Msg) -> Cmd Msg
fetchThings query tagger =
Http.get
{ url = "https://awesome-thing.api/things?search=" ++ query
, expect = Http.expectJson tagger (Decode.list thingDecoder)
}
requestMinInputLength : Basics.Int -> UpdateOption err a msg
How many characters does a user need to type before a request is sent? If this is too low you may get an unmanagable number of results! Default is 3 characters.
Select.updateWith
[ Select.request fetchThings
, Select.requestMinInputLength 4
]
SelectMsg
subMsg
model.select
requestDebounceDelay : Basics.Float -> UpdateOption err a msg
Configure debouncing for the request. How long should we wait in milliseconds after the user stops typing to send the request? Default is 300.
Select.updateWith
[ Select.request fetchThings
, Select.requestDebounceDelay 500
]
SelectMsg
subMsg
model.select
onSelectedChange : (Maybe a -> msg) -> UpdateOption err a msg
If provided this msg will be sent whenever the selected item changes.
Select.updateWith [ Select.onSelectedChange SelectionChanged ] SelectMsg subMsg model.select
onInput : (String -> msg) -> UpdateOption err a msg
If provided this msg will be sent whenever the input value changes.
Select.updateWith [ Select.onInput SelectInputValueChanged ] SelectMsg subMsg model.select
onFocus : msg -> UpdateOption err a msg
If provided this msg will be sent whenever the input is focused.
Select.updateWith [ Select.onFocus SelectFocused ] SelectMsg subMsg model.select
onLoseFocus : msg -> UpdateOption err a msg
If provided this msg will be sent whenever the input loses focus.
Select.updateWith [ Select.onLoseFocus SelectLostFocus ] SelectMsg subMsg model.select
onKeyDown : (String -> msg) -> UpdateOption err a msg
If provided this will be sent whenever there is a keydown event in the input.
Select.updateWith [ Select.onKeyDown SelectKeyDown ] SelectMsg subMsg model.select
sendRequest : (Msg a -> msg) -> (String -> (Result err (List a) -> msg) -> Platform.Cmd.Cmd msg) -> Maybe (a -> Basics.Bool) -> Select a -> ( Select a, Platform.Cmd.Cmd msg )
Send a request to populate the menu items. This is useful for initialising the select with items from an api. Provide a function that takes the current input value and a msg tagger and returns a Cmd which can be used to perform an HTTP request.
init : ( Model, Cmd Msg )
init =
let
( select, cmd ) =
Select.init "thing-select"
|> Select.sendRequest SelectMsg fetchThings Nothing
in
( { select = select }
, cmd
)
fetchThings : String -> (Result Http.Error (List Thing) -> Msg) -> Cmd Msg
fetchThings query tagger =
Http.get
{ url = "https://awesome-thing.api/things?search=" ++ query
, expect = Http.expectJson tagger (Decode.list thingDecoder)
}
Optionally provide a function to select one the items when the response returns:
init : ThingId -> ( Model, Cmd Msg )
init thingId =
let
( select, cmd ) =
Select.init "thing-select"
|> Select.sendRequest SelectMsg fetchThings (Just (\{ id } -> id == thingId))
in
( { select = select }
, cmd
)
The View Configuration
view : Model -> Element Msg
view model =
Select.view
|> Select.toElement []
{ select = model.thingSelect
, onChange = SelectMsg
, itemToString = .name
, label = Input.labelAbove [] (text "Choose a thing")
, placeholder = Just (Input.placeholder [] (text "Type to search"))
}
view : ViewConfig a msg
Initialise the default ViewConfig
withMenuAttributes : (MenuPlacement -> List (Element.Attribute msg)) -> ViewConfig a msg -> ViewConfig a msg
Set arbitrary attributes for the menu element. You can call this multiple times and it will accumulate attributes. You can define different attributes based on whether the menu appears above or below the input.
Select.view
|> Select.withMenuAttributes
(\placement ->
[ Element.Font.size 16
, Element.Border.width 2
]
++ (case placement of
Select.MenuAbove ->
[ Element.moveUp 10 ]
Select.MenuBelow ->
[ Element.moveDown 10 ]
)
)
|> Select.toElement []
{ select = model.select
, onChange = SelectMsg
, itemToString = .name
, label = Input.labelAbove [] (Element.text "Choose a thing")
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
}
Will the menu appear above or below the input?
withMenuMaxHeight : Maybe Basics.Int -> ViewConfig a msg -> ViewConfig a msg
Set a maximum height for the menu
withMenuMaxWidth : Maybe Basics.Int -> ViewConfig a msg -> ViewConfig a msg
Set a maximum width for the menu
withNoMatchElement : Element msg -> ViewConfig a msg -> ViewConfig a msg
Provide your own element to show when there are no matches based on the filter and input value. This appears below the input.
Option state for use with custom option element
withOptionElement : (OptionState -> a -> Element msg) -> ViewConfig a msg -> ViewConfig a msg
Provide your own element for the options in the menu, based on the current state of the option.
Select.view
|> Select.withOptionElement
(\state item ->
Element.el
[ Element.width Element.fill
, Element.paddingXY 14 10
, Background.color <|
case optionState of
Idle ->
Element.rgb 1 1 1
Highlighted ->
Element.rgb 0.95 0.95 0.95
Selected ->
Element.rgba 0.64 0.83 0.97 0.8
SelectedAndHighlighted ->
Element.rgba 0.64 0.83 0.97 1
]
(Element.text item.name)
)
|> Select.toElement []
{ select = model.select
, onChange = SelectMsg
, itemToString = .name
, label = Input.labelAbove [] (Element.text "Choose a thing")
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
}
defaultOptionElement : (a -> String) -> OptionState -> a -> Element msg
The default option element. Use this with withOptionElement only if you want the item text on the options to be different from that used in the input and search filtering.
withClearButton : Maybe (ClearButton msg) -> ViewConfig a msg -> ViewConfig a msg
Add a button to clear the input. This element is positioned as Element.inFront.
Select.view
|> Select.withClearButton
(Just
(Select.clearButton
[ Element.alignRight
, Element.centerY
, Element.moveLeft 12
]
(Element.el [ Element.Region.description "clear selection" ] (Element.text "❌"))
)
)
|> Select.toElement []
{ select = model.select
, onChange = SelectMsg
, itemToString = .name
, label = Input.labelAbove [] (Element.text "Choose a thing")
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
}
A button to clear the input
clearButton : List (Element.Attribute Basics.Never) -> Element msg -> ClearButton msg
Create a clear button
withFilter : Maybe (Filter a) -> ViewConfig a msg -> ViewConfig a msg
Customise the filtering of the menu based on input value. See Select.Filter. Default is startsWithThenContains.
withMenuAlwaysAbove : ViewConfig a msg -> ViewConfig a msg
Force the menu to always appear above the input. You may use this for example if you have issues with an input inside a scrollable transformed container.
withMenuAlwaysBelow : ViewConfig a msg -> ViewConfig a msg
Force the menu to always appear below the input. You may use this for example if you have issues with an input inside a scrollable transformed container. By default the menu will try to detect whether there is more space above or below and appear there, preferring below.
withMenuPlacementAuto : ViewConfig a msg -> ViewConfig a msg
Menu decides whether to appear above or below based on how much space is available. This is the default. You'd only use this function if you're passing around a config and need to reset this option.
withMenuPositionFixed : Basics.Bool -> ViewConfig a msg -> ViewConfig a msg
Use style: position fixed for the menu. This can be used if the select is inside a scrollable container to allow the menu to overflow the parent. Note that if any transforms (e.g. Element.moveUp/Element.moveLeft) are applied to the parent, this no longer works and the menu will be clipped. This is due to a feature of the current CSS spec. Also if the container or window is scrolled or resized without the input losing focus, the menu will appear detached from the input! To overcome this you may want to listen to scroll and resize events on the parent and window and use closeMenu to hide the menu.
withClearInputValueOnBlur : Basics.Bool -> ViewConfig a msg -> ViewConfig a msg
Should the input value be cleared when the input loses focus if nothing is selected?
withSelectExactMatchOnBlur : Basics.Bool -> ViewConfig a msg -> ViewConfig a msg
If nothing is selected, but the input value matches exactly one of the options (case insensitive), should we select it automatically when the input loses focus?
withSelectOnTab : Basics.Bool -> ViewConfig a msg -> ViewConfig a msg
Should we select the highlighted option when the TAB key is pressed?
withMinInputLength : Maybe Basics.Int -> ViewConfig a msg -> ViewConfig a msg
If set, no options will show until the specified number of characters have been typed into the input
withOpenMenuOnFocus : Basics.Bool -> ViewConfig a msg -> ViewConfig a msg
Should the menu be opened when the input gets focus?
toElement : List (Element.Attribute msg) -> { select : Internal.Model.Model a, onChange : Msg a -> msg, itemToString : a -> String, label : Element.Input.Label msg, placeholder : Maybe (Element.Input.Placeholder msg) } -> ViewConfig a msg -> Element msg
Turn the ViewConfig into an Element.
Internal.Effect.Effect effect msg
For use with the Effect pattern and elm-program-test, see Select.Effect.