This is a project used as an intro to Elm workshop. It builds a filterable dropdown similar to Select2. It takes a lot of small steps and doesn't make assumptions of existing Elm knowledge.
I wrote the dropdown in ~30 minutes with the expectation that a workshop could go through it in 90-120 minutes.
Each commit is a step with a detailed commit message explaining what was done and why. For ease of trying things out, I've linked the full source for each commit as an Ellie along with the accompanying commit message on this README. Unfortunately, GitHub doesn't allow iframe embedding in markdown docs.
Just display some static text on the page. Boring
Display an unordered HTML list with each element in the list
fruit
.The helper functions from
Html
take two arguments: a list of attributes and a list of children. SinceList.map
returns a list, we don't need to wrap it in brackets.
Remove the logic from
main
and into aview
function that handles turning a list of values into an Html list.
Create a model that contains both the list of values and the idea of being open or closed. The view is rendered differently depending on whether it is open or closed.
The closed view is static so it doesn't require any arguments.
Note: There is a bug in the commit where we're passing an argument to the
function viewClosed
when it takes no arguments. This is fixed in the Ellie
embed. It also gets fixed in the source in a later commit.
Using the record type is awkward so alias it as
Model
so we can use it in signatures easily.Note that this is different than the
State
type which is not an alias but instead is its own thing.
The
main
function is no longer static but instead usesHtml.program
. TheHtml.program
function takes in record with three vallues: an initial model, aview
function, and anupdate
function. Note that functions can be passed as arguments to other functions.The
update
function takes in an event and the current model and returns a new model. In this simplest implementation, we're ignoring the event and always returning the current model. Because the view only changes if the model changes, we still effectively have a static app.
We add a
Msg
type that defines two eventsOpenSelect
andCloseSelect
. Theupdate
handles both of these by correctly changing the model. The view gets re-rendered whenever the model changes.We emit the events from the views in response to a click event handler.
Note that the type signatures for the view functions changed from
Html a
toHtml Maybe
. That's because the views are now emittingMsg
events.
The whole point of this is to be able to select values. Since it's possible for no values to be selected, we wrap it in
Maybe
.The view gets trickier because there are now 4 ways to render the dropdown:
Open vs Closed each with the appropriate click handler and displaying the selected text or the help tip if nothing is selected.
To accomplish this, we extract a
dropdownHead
function that deals with selected vs no selection. Since we already know whether the dropdown is open or not, we just pass in theMsg
type we want for the click handler.Because we're now handling whether an item is selected or not, the closed view is no longer static.
This sets our selected item in response to a Msg sent when the user clicks on an item. This Msg is more complex than previous ones because it wraps a value.
This means we need to give it a value in the click handler. When reacting to the event, we unwrap the value as part of the case statement, just like we did for the
Maybe
.
We already have an event that handles an item selection, now we just need to change the model's state to "closed".
Only display values in the dropdown that match the given query (case insensitive). Since all the original values are kept, we can re-filter them by a different query at any time.
When the user types in the text box, we set the query value on the model, thus filtering the dropdown.
Note that the new Msg value
SearchInput
takes a paremeter, we don't give it one inonInput
. WhileonClick
takes a Msg as its argument,onInput
takes a functionString -> Msg
as its argument.
SearchInput
is such a function when not given a value.
onInput
does this because we don't know the value ahead of time. Instead, it will put the whatever value the user has typed inside the Msg.
We introduced a bug in the previous commit occurred when:
- The results were filtered
- A value was selected (which auto-closes the dropdown)
- The user re-opens the dropdown
- The results are still filtered from last time
We fix this by setting the query to empty string whenever a selection is made, thus clearing the filter for next time.
This is a more complex program that allows interaction with the outside world with commands and subscriptions. Following the compiler errors, we update signatures and return values to match what is required from
Html.program
.
This uses a combination of
Dom.focus
andTask.attempt
to create a Cmd that will focus the DOM node with the given id. Since all Cmds trigger a Msg when they are done but we just want to fire and forget, we create a Noop Msg to deal with the situation.
We want to style the dropdown so we embed the Elm app on an HTML page that has a
<style>
element on the body. This means we can no longer just useelm reactor
. We must now compile the app usingelm-make
This project is distributed under the BSD 3-clause license
This example project and walkthrough was developed for a workshop given at thoughtbot.
We love open source software! See our other projects or hire us to design, develop, and grow your product.