This is an example reusable element I made for our application https://github.com/sporto/elm-select
It is an input field with auto suggestions.
This element needs three pieces of state:
- List of things to search for
- Current selected thing
- And the query the user is typing (At the moment this is in "hidden" state)
I think that when building reusable views there are two aspects that need discussion:
- How do we store the state for a reusable view. Does we expose everything to the container or do we try to hide it? e.g. there is state that the container doesn't care about
- How do we return output to the container, e.g. a selection in a dropdown. This could be done with messages or can be done directly in
update
.
You need to store the Select state in your model:
type alias Model =
{ ...
, selectedMovieId : Maybe String
, selectState : Select.State
}
In init you need to make a new state
initialModel id =
{ ...
, selectedMovieId = Nothing
, selectState = Select.newState id
}
You need to have a message for the Select
type Msg
= OnSelect (Maybe Movie)
| SelectMsg (Select.Msg Movie)
You pass the message you want to trigger in a configuration record:
Select.newConfig OnSelect .label
The idea is that this element will trigger a OnSelect
when the user selects an item from the list.
In your update you have to handle SelectMsg
e.g.
update msg model =
OnSelect maybeMovie ->
...
SelectMsg subMsg ->
let
( updated, cmd ) =
Select.update selectConfig subMsg model.selectState
in
( { model | selectState = updated }, cmd )
The Select.update returns a command because that is the way the OnSelect
is triggered.
In the view you have to map the SelectMsg
:
Html.map SelectMsg (Select.view selectConfig model.selectState model.movies selectedMovie)
So there is a lot of places where the Select "component" needs to be hooked. Not a great API.
Based on our conversation I was thinking that this can change to something simpler like the following options.
- No hidden state
- Messages for output
No more mapping of messages in the view, you will need to hold to all state and pass everything to the view.
Select.view selectConfig OnSelect OnQueryChange movies selectedMovie query
Select.view would have this signature
view: Config -> (Maybe a -> msg) -> (String -> msg) -> List a -> Maybe a -> String -> Html msg
Something like this will mean that the user doesn't have to create a SelectMsg
and a Select.State
. But they will have to handle and store the current query
(which is the internal state) and pass it to view.
The function signature can be reduced by putting things in a record.
This approach favours triggering messages for the user to handle. I like this as it feels more inline with current elements like input
, select
, etc in the core html library. But asking the user to handle the internal state is annoying. e.g. they have to handle OnQueryChange
and update query
in their model.
Is asking the consumer to handle the "private" state a good idea? the query
in this case.
- Hidden state
- Messages for output
Maybe having a SelectMsg
and Select.State
is not too terrible in order to "hide" the query
piece of state from the main app.
You still hold to an internal state
type alias Model =
{ ...
, selectedMovieId : Maybe String
, selectState : Select.State
}
And have a message for the select:
type Msg
= OnSelect (Maybe Movie)
| SelectMsg Select.Msg
The view could look like:
Select.view selectConfig model.selectState OnSelect SelectMsg movies selectedMovie
And update:
update msg model =
OnSelect maybeMovie ->
...
SelectMsg sub ->
let
updatedState =
Select.update sub
in
( { model | selectState = updatedState }, cmd )
Is hidding the state a good idea?
Is asking the consumer to handle an opaque SelectMsg
a good thing?
- Hidden state
- No message, output returned in update
Yet another approach is not to have OnSelect
at all. Instead the Select.update
return the updated model.
The view could look like:
Html.map SelectMsg <| Select.view selectConfig model.selectState movies selectedMovie
update will return the selected movie directly.
update msg model =
SelectMsg subMsg ->
let
( updated, selectedMovie ) =
Select.update selectConfig subMsg model.selectState
in
( { model | selectState = updated, selectedMovie = selectedMovie }, Cmd.none )
Is returning the value in the update a good idea? Or is trigger messages more the Elm way?