A reusable dropdown in Elm — part 2

The pure version and how it compares to stateful

In our previous article, we built a page with two dropdowns. Where we used a reusable dropdown-module that manages its own state.

In this second part, we will build the reusable dropdown differently. The dropdown will not manage its own state. Instead, it will take in a bunch of stuff and will render.

Route 2: A reusable dropdown without local state

Code for the section below can be found in the examples/Pure folder over on github.

Let’s revisit our overall data model again.

-- in Main.elm
type alias Model =
{ country : Maybe Country
, city : Maybe City
, openDropDown : OpenDropDown
}
type OpenDropDown =
AllClosed
| CountryDropDown
| CityDropDown
-- in Dropdown.elm
type alias Context =
{ selectedItem : Maybe String
, isOpen : Bool
}

In our main module, we store and manage the selected country and city. And we use a new type OpenDropDown which tells us which of the dropdowns is open.

In the dropdown module, we no longer have a Model type. Instead, we define a Context. The contents are similar to the model we used in the stateful route, but there are 2 key differences:

  1. Context is a transparent type alias, and not an opaque type. This is because we want the main module to manage it. Context is no longer an internal data structure for the dropdown. It is part of the dropdown API.
  2. The dropdown does not have an update function, or any other function to change the context.

Inside the pure dropdown component..

The code for the pure reusable dropdown can be found in the examples/Pure/Dropdown.elm file.

The dropdown only exposes one function, its view function. It has the following signature:

-- in Dropdown.elm
view : Config msg -> Context -> List String -> Html msg

Of note here is that the output message of the view is an unspecified msg. This dropdown does not have its own message type. It doesn’t need to, because it doesn’t have its own update function.

But the dropdown will produce messages when the user clicks it, or when the user selects one of the items.

This is where the Config type argument comes in. The dropdown needs the main module to also provide a (static) configuration. With stuff that is supposed to remain static throughout the lifecycle of the app.
This really is up to the main module, but one reason for splitting Config and Context is to distinguish static information and dynamic info.

It looks like this:

-- in Dropdown.elm
type alias Config msg =
{ defaultText : String
, clickedMsg : msg
, itemPickedMsg : (String -> msg)
}

The default text to be provided is a string that the dropdown will display if no item is selected. It is part of Config because this is unlikely to change while running the program.

The next two variables are the messages that the main module needs to provide to trigger when something is clicked.

That’s about it for the pure dropdown module. It is basically a view function which receives a (static) config, some context and a list, and renders to the DOM.

Wiring in main..

The code for main is located in examples/Pure/Main.elm.

The main module will be handling the following messages:

-- in Main.elm
type Msg =
Toggle OpenDropDown
| CountryPicked Country
| CityPicked City
| Blur

The Toggle message gets passed an Id of the dropdown (the OpenDropDown we defined earlier). The CountryPicked and CityPicked messages provide the selection the user has made.

In our main update function, we respond to these messages.

  • For a dropdown Toggle, we check whether the clicked one was already open. If it was, we close it. If not, we open the one clicked.
  • When the user has selection a country we getCountryPicked in, and check if the new country selection is different. Because then we also reset the city selection. And we close the dropdowns.
  • When we get a Blur message (user has clicked outside our dropdowns), we change the OpenDropDown state to AllClosed

In our main module, we also define the configs for both dropdowns. Here’s the one for the country dropdown:

-- in Main.elm
countryConfig : Dropdown.Config Msg
countryConfig =
{ defaultText = "-- pick a country --"
, clickedMsg = Toggle CountryDropDown
, itemPickedMsg = CountryPicked
}

In the configs type definition, we define it as a Dropdown.Config type, using main module’s Msg type. 
Inside we specify the default text, together with the messages the dropdown should be sending us back, whenever the user clicks the dropdown or one of the items.

It comes together in our main view function. Here, we use the main model to derive:

  • The list of cities for the currently selected country. (The list of countries is static).
  • The Context we need to pass on to the country-dropdown and the city-dropdown.

The Context for the country dropdown looks like this:

countryContext =
{ selectedItem = model.country
, isOpen = (model.openDropDown == CountryDropDown)
}

And with this, we have the three necessary building blocks for each of our 2 dropdowns: Config, Context and the List of items. So we can render them in our view. We provided the message types in Config. So we don’t need Html.map in this setup.

Comparing pure and stateful

A rough comparison of the two routes reveals:

  • In total size (lines of code — including comments and docs) of the Main.elm module, there is not much difference between the stateful and pure extraction. The pure version is slightly bigger.
  • The stateful Dropdown.elm has a lot more lines of code than in the pure variant: 141 vs 96, which is over 40% more code.
    The implications of this are small. The reason for extracting the dropdown in the first place is to make the overall structure (in particular the main module) less complex. So a larger extracted module is not necessarily a bad thing.

The important differences become clear when we zoom in on the elements blocks inside the Main.elm modules.

Below is a screenshot of both update functions side by side.

The stateful setup has about 30–35% more lines of code. This is caused by:

  • In stateful, we need to read and manipulate the dropdown models directly from main (to close the city dropdown if a new country is selected).
  • In stateful, we need access from main to both dropdowns to open or close them.

The main update function is the core of our application. It is our central command center, where all switches are to determine how to go from current to next state. Whenever something goes wrong, the first place to look is usually the main update function. So we definitely want to keep this function clean and simple. The stateless or pure route definitely does a better job here.

In this case: Pure is better

The dropdowns in this exploration are of course a simple example. But they demonstrate a broader point:

Reusable elements that manage their own model (i.e. have their own Msg and update), will often make the update function of the importing module more complicated.

The dropdown example in these posts is far too limited to make the claim that pure reusable elements are always better that stateful ones. There is a very useful mdl-library of UI components (not (yet) upgraded to Elm 0.18), which including stateful elements. And there are also examples (e.g. here on Daily Drip) of integrating quite stateful Polymer webcomponents in elm. It is early days, but using web components in Elm looks promising! Perhaps something for a follow-up article…

Advice from this example

If you find yourself in a situation where you want to extract a bit of your own code into a reusable element, it is probably better to reach for a pure approach first, before turning it into a stateful element.

The debate in the elm community about pro’s and cons of stateful vs pure components will probably continue. My personal view is that probably both approaches will have their legitimate uses. Hopefully with the comparison here, this article will help you make better choices for your own code going forward, and make your own Elm code even more delightful..

Show your support

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