Higher-Order Controls: Do More With Less

In the previous tutorial we built a todo list manager. In this tutorial we'll continue improving that application by using a few higher-level controls.

A higher-level control is a control that takes other controls as arguments.

Say what?

We're going to add new functionality to our task manager, while reducing the amount of lines of code. How does that sound? Here's screenshots of the end result, both on iPhone and iPad:

The last screenshots shows the same screen as the first one. However, as you can see, it automatically adapts to the fact that the screen is wider. The iPad version shows the list of tasks to the left and details immediately at the right, whereas with the narrow (iPhone) version, details appear on a separate screen when the task is selected.

Tabs

The original version of our todo list application had a single screen of tasks, and a button to move to a separate "Search" screen. Kind of silly, isn't it? Wouldn't it be nicer to have a tabbed screen, one showing all tasks, and a second that had the search functionality? (One could argue it would be nice to have everything integrated, but for the purpose of this exercise, let's not.)

In order to do that, we have to make a few changes. First, we're going to rename our root screen, and turn it into a control. So, we change our root screen:

screen root() {
  ...
}

Into:

control tasks() {
  ...
}

Now we'll do the same thing to the search screen, we'll turn this screen into a control:

screen search() {
  header("Search") {
    backButton()
  }
  var phrase = ""
  ...
}

Becomes:

control search() {
  header("Search")
  var phrase = ""
  ...
}

We changed screen into control and removed the backButton, because we won't use it anymore. The IDE will now complain about the missing root screen, so let's define a new one:

screen root() {
  tabSet([("Tasks", "", tasks),
          ("Search", "", search)])
}

So, what does that do? It uses the tabSet control to build a tab set. The tabSet control takes a single argument: an array of tuples. A tuple could be described as an array with a fixed length. Mobl's syntax to create arrays, as you can tell, is [item1, item2] and its syntax to create tuples is (item1, item2). Each tuple in this array represents a single tab:

  • The first element of the tuple is the tab's title
  • The second an URL to an icon (not used, at the moment)
  • The third is the control to use as the body of the tab.

Yes, you can pass a control as an argument to another control. Controls that use this functionality are called higher-order controls. So, what happens after making these minor changes? Go look for yourself.

A nicely tabbed interface. That wasn't so hard. Let's see if we can use some more of these higher-order controls.

Task details

Let's define a control that gives some details about a single task:

control taskDetail(t : Task) {
  group {
    item { label(t.name) }
    when(t.description) {
      item { label(t.description) }
    }
    item { label(t.date.toDateString()) } 
    item {
      label(t.done ? "This task has been performed"
                   : "This task hasn't been performed")
    }
  }
  button("Edit", onclick={ editTask(t); })
}

This control uses two mobl language features we haven't seen before:

  • The when construct, which conditionally shows its body elements (somewhat like an if-statement). Yes, it does have an optional else clause.
  • The ternary e1 ? e2 : e3 operator, which is like an if-expression. If e1 is true, the return e2, else return e3.

In this particular application, such a detail control is not extremely useful, but typically applications do often have more information to display than fits a list view.

We're going to use this taskDetail control in combination with the masterDetail control. Wikipedia defines the master-detail user interface pattern as follows:

In computer user interface design, a master-detail page is one where a master area and its related detail area are represented on the same page. The content of the detail area is displayed based on the current record selection in the master area.

This UI pattern works great on larger screens (say, tablets), but not so much on smaller screens (say, phones). Therefore, the mobl::ui::generic library contains two implementations of masterDetail:

  1. One for screen widths <= 500 pixels. This version initially only renders a list view. Then, when the user selects an item from the list, it shows its details view on a separate screen (with a "Back" button to return).
  2. One for screen widths > 500 pixels. This version shows the list of items along the left side of the screen, and the details of the currently selected item directly to the right.

The "right" version of the controls is picked at run-time, automatically. Let's see how we use it. Replace the tasks control with the following implementation:

control tasks() {
  header("Tasks") {
    button("Add", onclick={ addTask(); })
  }
  masterDetail(Task.all() order by date desc,
               taskItem, taskDetail)
}

Compared to the previous version, we now removed the group control that was there before with the masterDetail control. As you can see, masterDetail takes three arguments:

  1. A collection of items to show the master-detail view for.
  2. A control taking a single argument (an item from the collection) to use in the list view.
  3. A control taking a single argument (again, an item from the collection) to use in the detail view.

The result looks as follows on phones:

And as follows on tablets:

Pretty nice huh, those higher-level controls? One more? Alright, because you insist.

Searching

Replace your current search control with the following:

control search() {
  header("Search")
  searchList(Task, taskItem, taskDetail,
                             resultLimit=10)
}

That's right. There's a searchList control -- very similar to the masterDetail control -- that automatically creates a standard search for you. It takes three required and one optional argument:

  1. An entity type
  2. A control taking a single argument (an item from the collection) to use in the list view.
  3. A control taking a single argument (again, an item from the collection) to use in the detail view.
  4. A maximum number of search results to show (defaults to 10)

And voila, the new search screen:

Conclusion

Higher-order controls are controls that take other controls as arguments. They enable the implementation of controls such as tab sets, master-detail and search lists (and more to come), greatly reducing the amount of code you need to write to build your applications.

tutorial/higherorder.txt · Last modified: 2013/10/01 02:29 (external edit)