Beefing up your markup with functions
With the very basics out of the way, we're now ready to see some real Elm magic come to light. What really sets the language apart from raw HTML and JavaScript is the resilience in the face of change it gives your code. It's that elusive quality known in the software community known as maintainability.
When you're writing raw HTML and JavaScript, it's easy enough get a prototype up and running, but that's just half the battle. Your idea of how things should look and function will change. You'll want to add new features as well as tweak old ones. That means you must take measures to ensure your codebase can easily accommodate those changes without breaking.
Let's pick out a couple places in our static to-do list code where we could make improvements. In the process, we'll make slight tweaks in our code to make it more idiomatic (in other words, Elmier), and thereby more maintainable. Then we'll be able to make sweeping changes in a fraction of the time it would take us in raw HTML. As an added bonus, our efforts will bring us just one step away from adding user interaction!
Writing functions to save time
Let's suppose we want to give all our buttons the same width, so that we end up with HTML like this:
<button style="width: 6em">All</button>
<button style="width: 6em">Pending</button>
<button style="width: 6em">Completed</button>
Using what we learned about Html.Attributes.style
in the last section, we can come up a snippet like this:
style [ ("width", "6em") ]
Now, we could go and paste this three times to cover each button
, but we can do better. Why copy and paste over and over when we can let the computer handle the repetition for us? This is a good time to write a new function.
Extracting common functionality
Let's make a new filterButton
function to use in place of these existing calls to button
. We can start by taking any one of those three calls to button
and pasting the whole thing it into a new value definition.
filterButton =
button
[ ]
[ text "All" ]
Since all three of our filter buttons are identical except for their text, we'll make that text into a buttonText
argument.
filterButton buttonText =
button
[ ]
[ text buttonText ]
Now we can call this filterButton
function from within our filterButtons
div, and the browser will give us the same result as before.
filterButtons =
div [ ] [
filterButton "All",
filterButton "Completed",
filterButton "Pending"
]
-- preview it in the browser by changing your main value:
-- main = filterButtons
Making changes in the refactored code
For now, even though our code is different, we've kept its current behavior the same. (We've refactored it.) What's changed is that now we can add styles just once right within the filterButton
function, and it changes all three buttons instantly!
filterButton buttonText =
button
[ style [ ("width", "6em") ] ]
[ text buttonText ]
Any change to filterButton
will now affect all three of our <button>
elements. No longer do we have to go into each button individually when we decide to make a small tweak.
Your average person probably has more than three things to do, so let's add more tasks to our to-do app skeleton.
Like our filter <button>
s, our <li>
elements are all mostly identical and serve the same purpose. It would make sense to write a taskLi
function following the same pattern. As before, let's take an existing element, in this case an li
, and use that as our basis for the new function.
taskLi =
li
[ style [ ("text-decoration", "line-through") ] ]
[ text "get milk" ]
A slightly more complex function
Now we look at what varies between task <li>
s and convert those parts to arguments. This looks exactly the same with our task names as with our button text!
taskLi name =
li
[ style [ ("text-decoration", "line-through") ] ]
[ text name ]
Except our <li>
s vary not just in their text. Only completed tasks should appear crossed out--we need to do something with that style
attribute, as well.
You might be tempted to make that entire style
attribute into an argument, to be passed in whole like we did for our name
argument. That isn't ideal for two reasons. First, that style
attribute is kind of long. Second, we'd have to edit each individual style
attribute any time we decided to change what a completed task looks like.
It would be better if our function just asked us, "Is this task completed, yes or no?" and take care of the styles accordingly.
Conditional expressions in action
This calls for a Bool
! When we call taskLi
, after passing in a String
for its name, we'll also provide one more argument. This will be either True
for completed tasks, or False
for pending tasks. We'll use this completed
value in a conditional expression where our style attribute is defined.
taskLi name completed =
li
[ style (if completed == True then [ ("text-decoration", "line-through") ] else [ ]) ]
[ text name ]
Note how our conditional expression replaces that argument of style
with the type List (String, String)
. That's why the expression after then
and that after else
must both have the type List (String, String)
. A completed task produces a List
containing that "text-decoration"
style rule...
[ ("text-decoration", "line-through") ]
...while a pending task produces an empty list.
[ ]
(You might fear that this would leave our resulting HTML with some ugly empty style attributes, but the Elm compiler is really smart. You can check it yourself with your browser's developer tools!)
Breaking things down
Though this code works, it's kind of hard to read. Let's extract the contents of that style attribute and make it into its own function.
crossedOut completed =
if completed == True
then [ ("text-decoration", "line-through") ]
else [ ]
Now, with a simple function call in the place of that lengthy conditional expression, our taskLi
code looks just as clean as the li
call we started out with. Only now, we get a lot more reuse out of it.
taskLi name completed =
li
[ style (crossedOut completed) ]
[ text name ]
Now if we take those li
calls within taskList
and convert them to taskLi
calls, the Elm compiler will render us the same result as before.
taskList =
ul [ ] [
taskLi "get milk" True,
taskLi "call Mom" False,
taskLi "learn Elm" False
]
Of course, this code is not only cleaner and more compact--it's easy as pie to extend.
Making changes in the refactored code
Adding three new tasks is now as easy as adding three new lines.
taskList =
ul [ ] [
taskLi "get milk" True,
taskLi "call Mom" False,
taskLi "learn Elm" False,
taskLi "mail letter" True,
taskLi "bake cookies" True,
taskLi "eat cookies" False
]
Extracting out common bits in your code and putting them into functions really makes your life easier. See how quickly you can pick out the varying bits in our taskList
when they're passed as arguments?
Our code so far
We've just barely introduced some exotic concepts into our code (from an HTML perspective), but hopefully you can already see how these features pay off. Even just when generating static markup, Elm code proves much more maintainable than the standard solution.
-- after your import statements
main =
body [ ] [
taskForm,
taskList,
filterButtons
]
taskForm =
div [ ] [
input [ type' "text" ] [ ],
button [ ] [ text "Add Task" ]
]
taskList =
ul [ ] [
taskLi "get milk" True,
taskLi "call Mom" False,
taskLi "learn Elm" False,
taskLi "mail letter" True,
taskLi "bake cookies" True,
taskLi "eat cookies" False
]
taskLi name completed =
li
[ style (crossedOut completed) ]
[ text name ]
crossedOut completed =
if completed == True
then [ ("text-decoration", "line-through") ]
else [ ]
filterButtons =
div [ ] [
filterButton "All",
filterButton "Completed",
filterButton "Pending"
]
filterButton buttonText =
button [ style [ ("width", "6em") ] ] [ text buttonText ]
Remember--maintainability means not just ease of modification, but also ease of extension. These functions we just wrote will go a long way to help us introduce features that will make our to-do app fully functional.