Todo list
The previous tutorial taught the bare basics of creating user interfaces using mobl. This next tutorial will focus on two additional aspects of building mobl applications:
- Data modeling (using a database)
- Application logic (scripting)
To keep it simple, we'll be constructing a todo list application. Here's what the end result will look like:
Click here to see it in action (Webkit-based browser required, e.g. Safari, Chrome, iOS or Android browser).
So, let's started, shall we?
Divide and conquer
In the previous tutorial we put our entire application in a single file. That clearly won't scale. In a larger project we'd like to divide our applications into multiple modules. And luckily, mobl allows you to do so. In mobl there are three kinds of modules:
- Application modules, which start with
application <app-name>
. These are required to have aroot
screen, and in addition to being compiled to Javascript, a HTML file is also generated for application modules. One mobl project can have multiple application modules, although typically, there's only one. - Configuration module, named
config.mobl
and starts withconfiguration
. This module defines some application configuration funny quotes, such as the application's title, database name and so on. - Regular modules, which start with
module <module-name>
. These are typically imported from an application module or from other modules. Just like application modules they can define new controls, screens, functions etc.
Data model
Create a new mobl project called "todo". The todo.mobl
application module is now automatically generated. We could define our data model directly in this file, but instead, we're going to define it a dedicated data model module. Right-click your project, pick "New" and then "File". Name the file model.mobl
. Copy the following code into it:
module model
entity Task {
name : String (searchable)
description : String (searchable)
done : Bool
date : DateTime
}
Every module starts with the keyword module
, followed by the name of the module, which has to match its filename. This module contains a single definition, an entity
definition. Entities are used to define your application's data model -- the types of objects that your application will deal with that need to be persisted to the mobile device's local database. In mobl there are two kinds of types:
entity
types, for which mobl handles persistency fully automatically: it creates tables in the database and it ensure that changes make to these objects are automatically saved.- regular
type
s, which are volatile in-memory types, whose values are lost when the application is shut down.
The model
module defines a single entity named Task
. Task
has four properties: name
, description
, done
and date
. name
and description
are textual properties (of type String
). done
keeps track of whether the task has been completed or not -- a boolean (true
or false
) value. date
is used to keep track of when the task was added. Entity properties can have annotations, which are enumerated in-between parentheses. The name
and description
properties have been annotated with a searchable
annotation, which makes them full-text searchable. We'll see how to use that ability later in this tutorial.
Save the module and switch back to your main application file (todo.mobl
). Import the module there:
import model
Save the application. When running the application for the first in the browser, it will create a local database on the device. It is recommended to use a webkit-based desktop browser during development. For instance, Safari or Chrome. When your application is loaded you can see the database tables that have been created using your browser's developer tools. In Chrome you can access these using the "Developer", "Developer Tools" menu. In Safari using the "Develop" menu, "Show error console". Chrome and Safari's developer tools look similar and consist of a number of tabs:
The two most useful ones during mobl development are the "Storage" and "Console" tabs. "Storage" shows you your local databases, its tables and data. In this case you'll see it contains two tables, one for Task
objects and one table for keeping the full-text search index. The "Console" tab shows you all kinds of error messages and logging information if you enabled the debug
setting in your config.mobl
file as follows:
configuration
debug
title "Todo"
Remember to force a recompile of your main application after creating or changing your config.mobl
file (by inserting a newline or space somewhere and saving the main application file), you'll now see all the SQL statements that mobl executes to create the database schema when your application loads.
An initial root screen
Let's get started with an initial root
screen:
screen root() {
header("Tasks")
group {
list(t in Task.all() order by date desc) {
item { label(t.name) }
}
}
}
A lot of this you will have seen before. There's the familiar header
control, and the familiar group
and item
controls. What's new is the list
construct. What does that do? list
is one of mobl's control structures for user interfaces. It can be compared to a for-each loop, except that it adapts automatically to changes to the collection it iterates over. What that means will become clear later on. The syntax is:
list(<item> in <collection-exp>) {
...
}
or, if you want to be more explicit about types:
list(<item> : <Type> in <collection-exp>) {
...
}
This particular list
iterates over the Task.all() order by date desc
collection. So what's that? The Task.all()
collection is a virtual collection that contains all known instances of the Task
entity. Task.all()
is of type Collection<Task>
. Collection
s represent (sometimes virtual) collections of objects that can be filtered, sorted and manipulated.
A collection's order(prop, asc)
method, for instance, sets the sort order for a collection: Task.all().order("date", false)
is a collection of all instances of Task
ordered in descending order by date.
A collection's filter(prop, op, value)
method filters a collection based on a property: Task.all().filter("done", "=", false)
is a collection of all Task
instances that have not yet been completed.
These can also be combined, of course: Task.all().filter("done", "=", false).order("date", false)
. However, this chained method call syntax does not look very pretty. Therefore, mobl has a syntactic abstraction over these methods that looks more SQL-like. The latter expression can therefore be rewritten as Task.all() where done == false order by date desc
.
When we test our new root
screen it will be a disappointing sight. Where are all the tasks? The database is still empty, so there's not much to see.
We need a way to add new tasks.
Adding tasks
Add a new screen above or below the root
screen:
screen addTask() {
var newTask = Task()
header("Add") {
backButton()
button("Add", onclick={
newTask.date = now();
add(newTask);
screen return;
})
}
group {
item { textField(newTask.name,
placeholder="Task name") }
}
}
Woah! Some new stuff there. First of all, yes, you can create multiple screens. Here we created a new screen named addTask
, again without any arguments. The screen has a local variable newTask
that is initialized with a new Task
instance. Instantiating a mobl type can be done simply by calling the type name as a function. Optionally, we can initialize properties directly while creating the object:
var newTask = Task(done=false, date=now())
Let's skip the header
for now and move on to the textField
control. This control is bound to the name
property of the newly created newTask
object, meaning that whenever the value of the text field is changed by the user, it automatically changes newTask
's name
property as well. The placeholder
argument sets the place holder text that is displayed within the text field when it has no value. It functions as a hint to the user as to what is supposed to be filled in there.
We now have a new Task
object whose name
property is bound to a text field. So when the user fills in the task name, it is assigned to the newTask.name
property. Great. But what should the user do when he or she's done filling in the task name? That's what the "Done" button is for.
As you can see, a header
control can have body elements as well. In fact, this one has two: a backButton
and a button
element. These controls will appear inside the header. By convention, a backButton
always appears at the left, and regular button
s appear along the right of the header.
Let's focus on the "Done" button. A button
has two important arguments: the button text and the onclick handler. In this case, the button text is "Done". The onclick
arguments defines what should happen when the user clicks (or "taps") the button. The onclick
argument is of type Callback
. A callback function is a snippet of application logic that is to be executed when a certain event occurs. Application logic is encoded using mobl's scripting language.
Scripting
Mobl's scripting language is syntactically similar to Javascript. It has many of Javascript's constructs, such as var
declarations, if
, else
, while
and return
statements. However, like the rest of mobl, it is a typed language. Callbacks are typically defined in-line by enclosing scripting code within curly braces. So, let's see what happens when the "Done" button is clicked:
newTask.date = now();
add(newTask);
screen return;
The first line assigns a new value to newTask
's date
property. It calls the now()
function, which returns a DateTime
object representing, shockingly, the current time and date. The next line marks the newTask
object for persistence. Newly instantiated entity objects are not immediately persisted, that would lead to a lot of garbage in the database. They have to be marked for persistence once in their lifetime using the add(obj)
function. From then on, mobl manages the persistence of the objects. Whenever its properties are changed, it will make sure those changes are persisted to the database. The third line may seem the strangest. You may know about return
statements, but what is a screen return
?
Screens in mobl are called like functions. To invoke the addTask
screen, you can simply call addTask()
from script. When doing so, the currently visible screen will be hidden and replaced by the called screen. The question is, how does a screen signal it's "done"? How does the user return to the previous screen? That's what screen return
does. screen return
says: I'm done, return to whatever screen you were at before. Optionally, a value can be returned, if a return type is defined for a screen.
While we created an addTask
screen, there's no way to get to it yet. In order to invoke the screen, we will add an "Add" button to the header of our root
screen. Adapt the header
call in root
to the following:
header("Tasks") {
button("Add", onclick={ addTask(); })
}
When the user pushes the button, the addTask
screen will appear, the user will fill in a task name and push the "Done" button. That will add the new task to the database, set the date
property and then return the user to the root
screen (using screen return
).
Save your application and test it. You can now add new tasks!
Did you notice that newly added tasks automatically appear in the task list, without you having to do anything? Implicitly, when add()
ing a new task, you modify the Task.all()
collection, which triggers a re-render of the list
in the root
screen.
Marking tasks as done
Although we can now add tasks, and they appear in a list, we cannot yet mark them as done. To enable that, replace the item
control within the list
in the root
screen with the following:
item { checkBox(t.done, label=t.name) }
This uses the checkBox
control (which renders, well, a checkbox) and binds its value to t.done
. Whenever the user taps the checkbox and changes its value, the t.done
property is automatically changed as well (and the change is propagated to the database as well). The label
argument adds a label to the checkbox. An alternative solution could have been:
item {
checkBox(t.done)
label(t.name)
}
The only difference is that in the former solution the label can be clicked to toggle the checkbox, in the latter solution the user has to aim a little bit better and click the checkbox itself.
Save your application and run it:
Editing and deleting tasks
We can now add, and mark tasks as done. Now let's also enable the user to edit and delete tasks.
First define an editTask
screen:
screen editTask(t : Task) {
header("Edit") {
button("Done", onclick={ screen return; })
}
group {
item { textField(t.name,
placeholder="Task name") }
item { textField(t.description,
placeholder="Task description") }
}
}
Yep. The first screen with an argument -- the task to be edited, to be precise. For the rest, there's not much new. The screen defines two text fields, one for the task name, the other for its description. The "Done" button doesn't do anything other than returning the user to the previous screen. Note an add(obj)
call is not required, because the task is already in the database. There's also no explicit save call, changes are persisted to the database as the user edits the text fields.
In order to expose the edit and remove features, we will create a context menu for every task in our root
screen. Again, adapt the item
control call in your root
screen:
item {
checkBox(t.done, label=t.name)
contextMenu {
button("Delete", onclick={
remove(t);
})
button("Edit", onclick={
editTask(t);
})
}
}
We now added a contextMenu
control. In its body, we add two buttons that will become visible when we push the context menu's icon. The first button
is is for deleting tasks. It simply calls remove(t)
when clicked. remove(obj)
removes that object from the database, as expected. The "Edit" button invoked the editTask
screen, as expected.
Save your application and run it, you can now remove and edit tasks!
Googling it up
If you're a very busy person, the amount of tasks may overwhelm you. Wouldn't it be nice to be able to search tasks as well? That would also make adding those searchable
annotations in our data model more useful.
Before we start, we'll first do some refactoring of our current code. In our search screen, we'll also want to display task items, with the same checkbox and edit/delete buttons. Can't we reuse that code somehow?
We can do that by creating our own control. We'll call this control taskItem
and we define it as follows:
control taskItem(t : Task) {
checkBox(t.done, label=t.name)
contextMenu {
button("Delete", onclick={
remove(t);
})
button("Edit", onclick={
editTask(t);
})
}
}
As you can see, a control is defined very similar to screens. Easy huh? So, let's use it. Let's once again (last time, I promise) change the item
control in our root
screen, in fact, let's replace it, turning the list
construct there into:
list(t in Task.all() order by date desc) {
item { taskItem(t) }
}
We replaced that whole item
thing with a single taskItem
control call. Cool huh? Cleaner code with exactly the same behavior.
Time to add search. Define the following new screen:
screen search() {
var phrase = ""
header("Search") { backButton() }
searchBox(phrase, placeholder="Search term")
group {
list(t in Task.searchPrefix(phrase)) {
item { taskItem(t) }
}
}
}
Once again, we use a local screen variable. It's the variable that will keep the search phrase. We use the searchBox
control to render a search input box. A searchBox
is basically the same as a textField
, except with different styling. It binds the control to the phrase
variable.
The list
construct iterates over the Task.searchPrefix(phrase)
collection. This is a special collection that performs a full-text search on all properties that have been marked searchable
. Because the collection depends on the value of the phrase
variable, the list of updates automatically updates as the user types in his or her search query. We reuse our taskItem
control here to render the task.
Add a button to the search screen at the bottom of the root
screen:
button("Search", onclick={ search(); })
Save your application and run it. We're done!
In barely 80 lines of code we built a pretty functional todo list applications that supports adding, editing, removing and searching tasks. We defined a simple data model, a custom control and a number of screens. We defined some event callbacks to navigate between screens and set properties.
What about MVC?
While mobl supports the Model-View-Controller pattern, it does not enforce it. The model (entities) of the application can be defined separately from the rest of the application. However, views (controls and screens) and controllers (script callbacks) are mixed. In the todo application we built, the amount of controller logic is so small that it would a waste to create a whole separate controller to implement it. However, if desired, it is possible to move more of the logic to functions. For instance, the following snippet of code:
button("Add", onclick={
newTask.date = now();
add(newTask);
screen return;
})
Could be refactored to:
button("Add", onclick={
createTask(newTask);
screen return;
})
function createTask(t : Task) {
t.date = now();
add(t);
}
The function
could even be moved to a separate module. But you don't have to. Mobl lets you organize the code the way you like. It is good practice to move complex logic into functions in a separate module, but it is typically much more productive to write simple one or two-liners inline in the control or screen itself.