Elm doesn't have a concept of required and optional arguments. Every function takes all the arguments they specify, no more, no less. But sometimes we want to be able to specify only some arguments to a function, and use default values for the rest. The builder pattern is one solution to that challenge.
4 min read
·
By Aksel Wester
·
December 18, 2020
Let's start with a common use case for the builder pattern in Elm: view elements. These are reusable elements that can be used in view code in different places in an app. The most common examples are buttons, input fields, and links. What these elements have in common is that they should be reusable, but that they are not necessarily identical everywhere they are used. A button could be solid or just have an outline, and have different colors depending on where you use it. Text input fields could differ in size, have error messages, or be disabled.
If we were to write separate functions for each of these variants of view elements, we would end up with many similiar, but slightly different, functions, to account for all the combinations of characteristics.
There are multiple strategies to deal with this challenge in Elm, and Brian Hicks examines many of them in his excellent talk Robot Buttons from Mars, which is where I first learned about the builder pattern in Elm. To see the alternatives to the builder pattern, and the limitations with those alternatives, I would encourage you to watch that talk.
We will examine this technique by creating an element for text input fields. We will start out with only required arguments, and build out the capabilities from there. We want our text field to take 3 required arguments: value
(the text the user has typed), onChange
(the message to be sent when the user types), and a label
text, to help with accessibility. In our example we will start with the following, simplified HTML, where our required arguments are shown in braces:
label []
[ text {label}
, input
[ type_ "text"
, value {value}
, onChange {onChange}
]
]
We will keep all the functionality related to text inputs in one file, which we will call Input.elm
. And we will structure our code around an opaque type called Input
, which will hold all of our options for the text field. Input
has the following type signature:
type Input msg
= Input
{ value : String
, onChange : String -> msg
, label : String
}
We will create a constructor function for making an Input
, which will be exposed by our module:
input : { label : String, onChange : String -> msg } -> String -> Input msg
input inputOptions valueString =
Input
{ value = valueString
, onChange = inputOptions.onChange
, label = inputOptions.label
}
To transform an Input
into Html
, we create the following function:
toHtml : Input msg -> Html msg
toHtml (Input options) =
label []
[ text options.label
, input
[ type_ "text"
, value options.value
, onChange options.onChange
]
]
So to use our new view element, we do the following (where NameChanged
is a message that takes a String
as an argument):
view model =
Input.input { label = "Name", onChange = NameChanged } "Aksel"
|> Input.toHtml
If that's the only way our text fields look and behave, this would have been an extremely overengineered solution. But the moment we have different needs in different text fields, this solution starts to shine.
We can continue by adding the ability for our text fields to have a placeholder text. Placeholder texts shouldn't be required in all text fields in our app, so we will add it as a Maybe String
in our options record:
type Input msg
= Input
{ value : String
, onChange : String -> msg
, label : String
, placeholder: Maybe String
}
Then we have to add it to our constructor. But crucially, we won't require it as an argument, we will just initialize it to Nothing
:
input : { label : String, onChange : String -> msg } -> String -> Input msg
input inputOptions valueString =
Input
{ value = valueString
, onChange = inputOptions.onChange
, label = inputOptions.label
, placeholder = Nothing
}
To use the placeholder in the HTML, we modify the toHtml
function to take the option into account:
toHtml : Input msg -> Html msg
toHtml (Input options) =
label []
[ text options.label
, input
[ type_ "text"
, value options.value
, onChange options.onChange
, case option.placeholder of
Just placeholderString ->
placeholder placeholderString
Nothing ->
-- This is just an attribute that doesn't change the html
classList []
]
]
To actually set a placeholder on a text field, we create the following function:
withPlaceholder : String -> Input msg -> Input msg
withPlaceholder placeholderString (Input options) =
Input { options | placeholder = Just placeholderString }
And to use the withPlaceholder
function in our view, we simply add one line:
view =
Input.input { label = "Name", onChange = NameChanged } "Aksel"
|> Input.withPlaceholder "Type your name here..."
|> Input.toHtml
The handy thing here is that adding the placeholder capability to our Input
module doesn't have to result in any code changes in places that already use the text field without a placeholder! This is especially important as your app starts to grow, and you are using a view element in tens or hundreds of places in your code!
In this article I have hopefully shown that the builder pattern is a nice way to handle optional arguments in Elm. Whether using the builder pattern is right for you in your app depends on the size and complexity of your code base. But if you find yourself adding a lot of helper functions for different variations of view elements, I hope you will give it a try. I, at least, have really enjoyed working with it.