dropping down

A reusable dropdown in Elm — part 1

Experimenting with components

Disclaimer: my animated gif tools & skills need work, I know

This is an exploration. Where we will compare a stateful (‘component’) approach to a stateless approach, in making a reusable dropdown menu in Elm. This one.

It took me a while to ditch the habit of reaching for ‘components’ whenever some piece of code got too big, or when I needed something reusable. Many in the community told me “don’t do components in Elm, it’s bad”, but I couldn’t find any “do this instead” info anywhere. Hopefully this exploration will contribute to filling this gap (a lot has already been done since), and help others in their learning curve of the wonderful language of Elm.
I use dropdowns a lot, and the standard
<select> element simply has not enough styling possibilities for my taste. So I rolled my own, and thought it would make a good use case for this exploration.

Inspiration: this is not a component.

A notable characteristic of Evan’s elm-sortable-table example is that it does not have its own Msg type or update function.

In other frameworks like React this would be called a pure component. A component that does not manage its own state. There have been quite a few back-and-forths in the elm community about “components”, e.g. here, or here. My take on this:

In Elm, it doesn’t matter whether we call something a “component” . What matters is whether something manages its own state or not.

Comparing stateful and pure

To compare both approaches, we will be doing an extraction of a module in two different ways:

  • Route 1 will be to extract a module that manages its own state (stateful)
  • Route 2 is where we extract a module without its own state (pure)

After that, we will compare the two routes taken.

Some comments beforehand: Please do not consider the example below to be some sort of best practice on how to do stateful or pure components. They are the best ways I could design both variants, and I am very much still on a learning curve.
Also, the extractions we make for the dropdown are probably be a bit of an overkill, introducing more boilerplate and abstractions than is warranted for with such a limited use case.

Our use case: a double dropdown.

Here’s what we will be building: a simple page with two dropdowns. The user can select a country on the left. And select a one of the cities in that country on the right. Functions of the page include:

  • As long as no country is selected, the user cannot select a city.
  • When the user selects a different country, the city model dropdown will be reset.
  • When the user clicks on a dropdown, it opens (duh), and at the same time the other dropdown will close if it was open.
  • Clicking anywhere outside the dropdowns closes both dropdowns.

Starting point is a from a simple app — in one module — with only one dropdown. From there, we will look at two approaches.

  • First with the dropdown as a module with its own Msg and update, aka a stateful solution, aka component, aka a widget that manages its own state.
  • Then we will do it another way, extract the dropdown into a widget that does not manage its own state.

All code related to this article can be found over on github.

Starting point: A simple dropdown

The file examples/Single/Main.elm contains the code for this section. You can run it locally in elm-reactor to see it in action. (You will also be needing the Styles.elm file).

For an app with just one dropdown, our model is straightforward. It contains a selectedCountry (which could be empty), and a flag to indicate whether the dropdown is open or not. That’s it.

type alias Model =
{ selectedCountry : Maybe Country -- (alias for String)
, isOpen : Bool
}

Since the list of countries is static in this example (does not change over the app’s lifecycle), we do not manage it in our model. But treat it as something of a global constant.

In our Msg, we have messages for when the dropdown is clicked, for when the user selects a country, and for when the user clicks outside the dropdown (to close).

type Msg = 
DropdownClicked
| CountryPicked Country
| Blur

The position in the Blur message is passed with Mouse.clicks, to which we have a subscription.

The update function takes care of the following:

  • If the dropdown (the text part of it) is clicked, it sets the .isOpen flag in our model to open (True) or closed (False).
  • If a country is selected, we set that value in our model, and close the dropdown by with isOpen = False

In the subscriptions function, the subscription to Mouse.clicks is only “on” if the dropdown is open.

In the view function, we set a text to display (we need to output something if there is no country selected), and we define the Html style for when the dropdown is open or closed.

For onClick, we use our own custom made version. We need to include a stopPropagation flag, to make sure that our Mouse.clicks subscription does not fire a Blur message if we click something valid.

Ok, so now that we set this baseline, let’s explore how we could extract the dropdown if we have 2 of them on our page.

Route 1: A dropdown module managing its own state.

This section is about the code in the folder examples/Stateful.

So now we want to have 2 dropdowns on the page. This is the point where we extract the dropdown from our main app.

Let’s look at the total model with stateful children first.

-- in Main.elm
type alias Model =
{ country : Dropdown.Model
, city : Dropdown.Model
}
-- in Dropdown.elm
type Model =
Model
{ selectedItem : Maybe String
, isOpen : Bool
}

The model we import from Dropdown is an opaque type. Which is a common practice. By not exposing the internals of Dropdown.elm, we can make changes to its inner workings, without introducing any breaking changes to the code where it is imported. More importantly, using an opaque type here is a nice guarantee: the only way to change the dropdown’s model, is to use the dropdown’s update function.

Inside the dropdown component..

The file examples/Stateful/Dropdown.elm contains the code for this part.

Internally, the dropdown manages which item is selected (when the user clicks on an item in the list), and whether the dropdown is open or not (when the user clicks on the dropdown or when they select an item).

The dropdown does not manage the list of items, so we don’t store that in the model.

For managing its state, the dropdown uses the following messages:

-- in Dropdown.elm
type Msg =
ItemPicked (Maybe String)
| SetOpenState Bool

Because the main module needs to be able to explicitly close the dropdown on a click somewhere else, we make the open/close call explicit in our messages. So the SetOpenState will be called not only by dropdown itself, but also by the main module.

To allow the main module to remove any selected item from the dropdown — i.e. reset the dropdown — we change the signature for ItemPicked as well.

With our dropdown Model being opaque, we also need to expose some simple functions, to allow the main module to read the value of the selected item (we want our cities dropdown to depend on the selected country), and to allow the main module to find out if the dropdown is open (our subscriptions for blurring is only “on” if one of the dropdowns is open).

-- in Dropdown.elm
selectedFrom : Model -> Maybe String
openState : Model -> Bool

In addition, the dropdown needs to communicate to our main module whenever an item is selected. A typical pattern for this is to define an OutMsg union type, with all possible outgoing messages. And add a Maybe OutMsg to the output of the update function. 
For our dropdown, the only message we need is one with the selected item, so without defining an OutMsg, we can define our update like this:

-- in Dropdown.elm
update : Msg -> Model -> (Model, Maybe String)

We don’t output any Cmd from our dropdown, because the dropdown has no side effects of its own.

The signature of the view function of our dropdown is:

-- in Dropdown.elm
view : Context -> Model -> List String -> Html Msg

The first argument is a Context, which is a type alias for String. This is the default text we want displayed if there is no item selected. We use a separate argument here because this context is not managed by our dropdown. It is simply passed in from main.

The other arguments are the dropdown’s model, plus a list of items. If the list is empty, the dropdown will be disabled. The text will be greyed out, and clicking will have no effect.

Inside the main module

The file examples/Stateful/Main.elm contains the code for the section below.

Let’s wire up this dropdown module in our main module. Our main module will handle the following messages:

-- in Main.elm
type Msg =
CountryMsg Dropdown.Msg
| CityMsg Dropdown.Msg
| Blur

Inside main update, we need to do a bit of extra work now:

  • When a message from the country-dropdown comes in, after calling Dropdown.update, we also need to close the city-dropdown. And we need to check for an outmessage from the country-dropdown with a selected country. If there is one, we need to compare the new to the old country (if any), and if the country has changed, we need to reset the city-dropdown as well (remove the selected city).
  • When a message from city-dropdown comes in, we need to close the country-dropdown.
  • When a Blur message comes in (from our subscriptions), we need to close both dropdowns.

In the main view function, we look inside the model of the country-dropdown to find out if any country is selected. We use this information to generate the appropriate list of cities for the city-dropdown.

The trouble with stateful children

Nesting trouble

This setup works, and we have successfully delegated the selection and open-close state to the dropdown component.

But if we look closer at the structure we built with a dropdown-managing-its-own-state, there are some areas of concern, where our code is rather inefficient/ complex, and prone to bugs:

  • The combined modeling of our open/closed state allows for impossible states. We have 2 Bools, living inside our dropdowns. But overall, there can always only be 1 dropdown opened at any time. We had to build quite a bit of extra code in our update to check this, and if needed close the other dropdown.
  • The dropdown saves and updates its own selected item state. But it also sends updates out to the main module (so that the main module can use this to update the city dropdown if the country dropdown changes). In most other cases, the main module doesn’t care about the outmessage.
  • For updating the country, the main module needs to first get the current selected country, then do the update to the country dropdown, and then check the result to see if the selected country has changed. And if so, reset the city dropdown.
  • Inside the main view, we need to inspect the dropdown’s model again to know which cities (if any) to render.

This stuff happens a lot when you work with components that manage their own state.

When you delegate state management to a component, you will often find that you still need outside read/ write access to the delegated state, which destroys the benefit of delegation, and often makes things worse.

This is probably true in other languages and frameworks as well. When I coded in React, many SO topics were about inspecting or updating local component state from a place where you were not supposed to.

Elm makes these anti-patterns hurt early and badly. Which is a good thing, because it will make our code better.

Up next in part 2: pure version and comparison

In part 2 of this exploration we will look at the alternative route: making a pure extraction of the dropdown. And finish off with a comparison of the 2 approaches.

Yeah, pure will prove to be better in this example. But it is actually quite interesting to learn where and why. Hope to see you back in part 2!

Show your support

Clapping shows how much you appreciated Wouter In t Velt’s story.