Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@sporto
Last active March 7, 2017 03:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sporto/4dad024c5e4cf4865063d480dadc1f9b to your computer and use it in GitHub Desktop.
Save sporto/4dad024c5e4cf4865063d480dadc1f9b to your computer and use it in GitHub Desktop.
Reusable view feedback

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:

  1. 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
  2. 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.

My current API

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.

update

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.

view

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.

Version 2 - A

  • 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.

Version 2 - B

  • 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?

Version 2 - C

  • 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment