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.

results matching ""

    No results matching ""