Skip to content
This repository has been archived by the owner on Jul 8, 2020. It is now read-only.

Todo app (The M Project 1.x)

hano edited this page Dec 3, 2013 · 1 revision

Overview

The ToDo App is a simple mobile web application to manage your ToDo's. It uses HTML5 LocalStorage to persist the todo items. This guide shall introduce developers to build apps with The-M-Project framework.

Here's the source of this app: ToDo Source

Screens

Adding a new item is easy. Just enter text into the field. You can add one item after the other without having to click the field again. Items are shown in the list and persisted.
By clicking the 'Edit' button on the upper right,
you enter the 'Edit' mode, where you can delete
items from the list. Look at the label at the top
showing the number of items in the list: thanks to *data binding*, it is updated also!
Before definite deletion, you're asked to confirm the delete operation.

How to build it

As mentioned above, the ToDo app uses HTML5 Local Storage for persistence. We will cover this later. First we're diving into the basics of app creation with the M Project framework.

Creating the view

When we look at the screen, we can determine several view components that we need for this application:

  • a header bar
  • a label showing the current number of items in the list
  • a label completing the 'number of items' label with a sentence (here: item(s) left.)
  • an input field to insert text for a todo item
  • a list showing all todo items
  • an edit button to delete items from the list, placed in the header bar

Optionally there could be a button for adding the item to the list. In our case, we skipped this element, we add todo items by simply pressing the 'return' key on our (virtual) keyboard after inserting text. Remember this, 'cause we will come back to this behaviour when we show the event handling.

We only have one page. We can place all view elements on this page. So we add one:

Todos.app = M.Application.extend({
  page: M.PageView.design({
    // all other view elements are placed here....
  });
  . . .

Hint: We define the properties of a view element by calling design on it and giving an object with the defined properties for the view as parameter.

The page has an onLoad property, which is an object defining the method to call when the page has been loaded.

onLoad: {
  target: Todos.TodoController,
  action: 'init'
}

The target property is a reference to the controller, the action is a string naming the controller's method to call.

As well as any other container view, the page also has a property childViews.

childViews: 'header content'

This string names all child view elements for this container view. The parent view uses this string to trigger the rendering of its children. Our page includes a header bar (named header) and a ScrollView (named content), that itself again is a container view.

Our page definition is now finished. We can move our focus to its children.

The header

header: M.ToolbarView.design({
  childViews: 'centerLabel toggleView',

  toggleView: M.ToggleView.design({
    childViews: 'button1 button2',
    anchorLocation: M.RIGHT,
      
    button1: M.ButtonView.design({
      value: 'Edit',
      target: Todos.TodoController,
      action: 'edit',
      icon: 'gear'
    }),

    button2: M.ButtonView.design({
      value: 'Save',
      target: Todos.TodoController,
      action: 'edit',
      icon: 'check'
    })
  }),

  centerLabel: M.LabelView.design({
    value: 'Todos',
    anchorLocation: M.CENTER
  }),
  
  anchorLocation: M.TOP
})

Our black header bar with the title "Todos" in it, is defined by the view object M.Toolbar. There's one property that defines whether we design a header bar or a footer.

  • anchorLocation: The location of the toolbar, either M.TOP or M.BOTTOM.

The toolbar itself also serves as a container for other elements, e.g. a label, a button, or an image. Therefor it has 3 available locations, where elements can be positioned:

  • M.LEFT
  • M.CENTER
  • M.RIGHT

The elements inside the toolbar set this value: they also have a property anchorLocation which is used when positioning an element as a childview inside a toolbar. The most interesting element here is the ToggleView. Same as the ToolbarView, ScrollView or some others, it is a container for other elements. It is used to toggle two elements when clicking on it. In our case, we want to toggle two buttons: one that sets the list into an "edit mode" and the other one that ends the toggle mode. This means the ToggleView has two states: the button first named in the childViews string is the element representing state 0, the second named is the element for state 1. As we can derive from the anchorLocation property in the ToggleView the element is located on the right of the toolbar. Then we define two buttons in the same way we would do it elsewhere. We give the button a value (representing the text on the button), a target and action to call when clicked and we set an icon for it. If no icon parameter is provided, no icon will be used. If a non-existant icon is named here, the plus icon is used by default (Icon Overview). For sure, we also define a second button for toggling. It receives other parameters but the syntax stays the same.

The title of the header bar is represented by a label (centerLabel) with the value Todos. It is located in the center (anchorLocation: M.CENTER).

The content

As a convention, the standard container element for the content area is a M.ScrollView.

content: M.ScrollView.design({

  /* order in childViews string defines render order*/
  childViews: 'counter text inputField todoList',
	
  counter: M.LabelView.design({
    value: '0',
    contentBinding: 'Todos.TodoController.counter',
    isInline: YES
  }),
	
  text: M.LabelView.design({
    value: ' item(s) left.',
    isInline: YES
  }),
	
  inputField: M.TextFieldView.design({
    name: 'todo_field',
    initialText: 'Enter ToDo Item...',
	
    /* target and action define path for onReturn event */
    target: Todos.TodoController,
    action: 'addTodo'
  }),
	
  todoList: M.ListView.design({
    contentBinding: 'Todos.TodoController.todos',
    listItemTemplateView: Todos.TodoItemView
  })
})

The first property is the child views string, where all elements located in the M.SrollView are named in the order of rendering.

As the first child, we have a label (counter) showing the current number of todo items in the list.

counter: M.LabelView.design({
  value: '0',
  contentBinding: 'Todos.TodoController.counter',
  isInline: YES
})

Its initial value is 0, because at the very first application start, no item is in the list. For the current number state of the list, we define a content binding on a controller value, that represents the number of items. When this value changes, the label view is notified and renders the update (see Data Binding or Architecture for details).

Because we want to have a text placed next to the counter label, we set its isInline property to YES (YES is an alias for true and vice versa, NO is an alias for false). The text next to the counter is represented by a label:

    text: M.LabelView.design({
      value: ' item(s) left.',
      isInline: YES
    }

It has no content binding, its content/value is somehow static, though its content could be changed in code.

Our next view element is a text field where the user can enter new todo tasks.

inputField: M.TextFieldView.design({
  name: 'todo_field',
  initialText: 'Enter ToDo Item...',

  /* target and action define path for onReturn event */
  target: Todos.TodoController,
  action: 'addTodo'
})

No surprise, the name property is used to define the text field's name, which means the HTML attribute name (This attribute is used as the key in form request parameter array, e.g. $_GET['todo_field']). initialText defines a text that is shown inside the text field while not having the focus.

The next two values look familiar to us, we already used them with the buttons in the button view in our header bar. But in contrast to the button view the so defined method is not called in combination with a click event. This wouldn't make much sense as default behaviour for a text field. By convention these parameters are used in combination with a keyUp event, or to be more specific, with the return key pressed event. This means we can add todo items by pressing the return key after entering a text.

Our last element on the page is the list. Because a ListView is slighty more complex than for example a ButtonView, we spend it an own section.

The List

Defining a list is not that complex. The ListView itself only has two important properties to be set: its content binding and the template view defining the structure of each list item:

todoList: M.ListView.design({
  contentBinding: 'Todos.TodoController.todos',
  listItemTemplateView: Todos.TodoItemView
})

Here we bind the list to an array of todo items in the controller, named todos.

Template views extend M.ListItemView, are located in the applications view/directory and are created by the developer. Here's our template:

Todos.TodoItemView = M.ListItemView.design({
  childViews: 'label1',

  label1 : M.LabelView.design({
    valuePattern: '<%= text %>'
  })
});

The childViews string indicates that a ListItemView is a container for any number of other elements. We only want the text of a todo item be shown. Therefor we define one LabelView:

label1 : M.LabelView.design({
  valuPattern: '<%= text %>'
})

The label's value property looks somehow strange to us until now. What do these angle brackets, the procent symbols and the equal sign mean? This is a special syntax that tells the framework to use a property named text of the binded object as the label's value. As mentioned above, we bind the list to an array of todo items in the controller. As we will see when discussing the model part of the app, each items has a text property and this kind of expression tells the list item to use it as its content. Because we defined the content binding in the ListView we are not binding anything here. In conclusion: the list is binded to an array of todo tasks. For each todo tasks a new list item is created with the text property of the particular todo task used as its value.

This is it for the view. All necessary view elements are set up and configured. What we need to know next, is the model.

Some parts of the view definition, e. g. the content binding properties, might be defined not until the controller is set up. But to have all parts of the view explained in one place, we already defined them. In practice this might be a more iterative process between view definition and controller set up.

A simple overview of the todo app's controller and model

The figure points out the following:

  • We have one controller, named TodoController
  • We have a Model called Task that represents a todo task and we have multiple objects (records) of it: one for each todo task
  • The model records are able to save themselve to LocalStorage by invoking their save method.

The model Task

In MVC Models represent entities of the application's domain for example: Person, Invoice, Order, Task, etc. In doing so, they represent also the application's data. They encapsulate the business logic and they know how to validate themselves. Usually models are persisted in a storage system like a database.

In the todo sample app we have a model representing a todo task, called Task. It must be created in the applications models/ directory. Power lies in simplicity and so the only attribute of a todo task in our app is its text. That's why our model also has only one record property, named text:

Todos.Task = M.Model.create({

    __name__: 'Task',

    text: M.Model.attr('String', {
      isRequired:YES
    }

}, M.DataProviderLocalStorage);

Here the circle completes: Do you remember the list item template. We set the value of the only label in it to the strange looking string '<%= text %>'. text refers to this model property!

The __name__ property is a meta property of the model. It doesn't belong to the model's real properties. It is used by the framework to identify its heritage. All properties prefixed and suffixed with two underscores (__foo__) are metaproperties of the model and are not persisted. In the future, the name property is going to be generated automatically when creating a model with the model generator script.

The text property is define by calling M.Model.attr with a parameter object. Here the content of the parameter object is just a flag defining whether this property is required for persisting or not. If the isRequired flag is set to YES, a M.PresenceValidator is automatically added to the property to ensure that this property has been set with a value before persisting the model record.

That's all for the model itself. We will look into persistence later. Now let's have a look at the controller.

The Todo controller

A controller acts as the middle part or connector between the views and the models. Events from the view are dispatched to controller methods. These methods might interact with models and after completion invoke a view to render the results of this process. In our todo app we have one controller that is simply named TodoController.

/* every controller extends M.Controller */
Todos.TodoController = M.Controller.extend({

  todos: null,

  counter: 0,

  init: function() {
    Todos.Task.find();
    this.set('todos', Todos.Task.records());
    this.calculateCounter();
  },

  addTodo: function() {
    var text = M.ViewManager.getView('page', 'inputField').value;
    if(!text) {
      return;
    }

    Todos.Task.createRecord( { text: text } ).save();
    this.set('todos', Todos.Task.records());

    this.calculateCounter();

    M.ViewManager.getView('page', 'inputField').setValue('');
  },

  removeTodo: function(domId, modelId) {
    var doDelete = confirm('Do you really want to delete this item?');
    if(doDelete) {
      var record = Todos.Task.recordManager.getRecordForId(modelId);
      record.del();
      this.set('todos', Todos.Task.records());
      this.calculateCounter();
    }
  },

  calculateCounter: function() {
    this.set('counter', this.todos.length);
  },

  edit: function() {
    M.ViewManager.getView('page', 'todoList').toggleRemove({
      target: this,
      action: 'removeTodo'
    });
  }
});

As mentioned above, the ListView has a content binding on the todos property of the controller. On startup, this property has the value null because no todo task exists. Later when we add tasks, its value is set to the array of records that we get from the model by calling its records method. This means, todos represents the list of task model records. And for each model record we want to show the text (remember the list item template and the model!?).

Another property is the number of items in the list, resectively the number of models in the controller: counter. Initially it is 0.

Next, we'll have a look at the controller's methods. One of the first properties explained at the top of the page was the onLoad property of the PageView. There, we defined a target and action referring to the init method of the TodoController. Here it is:

init: function() {
    Todos.Task.find();
    this.set('todos', Todos.Task.records());
    this.calculateCounter();
}

At first we call the model's find method without parameters to fetch all available tasks. Next we set the controller's todos property (on which the list is binded) with all records that we prviously have fetched. We do this by calling records on the model (not on a model record!). This will return an error of all fetched model records. . By using set we make sure that the content binding takes place. If we would assign the model list to our property (what would be valid javascript) by using the equal operator (=), we would bypass the content binding. This is not what we want here. As the last step we call calculateCounter, a helper method we wrote inside the controller. It simply sets the length of todos to the counter variable. The label that binds on this property is then being triggered to render the new value. The init method`s primary goal is to set up the app correctly from LocalStorage when it is launched again.

Now let's look at the (supposed) most important method, addTodo:

addTodo: function() {
  var text = M.ViewManager.getView('page', 'inputField').value;
    if(!text) {
      return;
    }

    Todos.Task.createRecord( { text: text } ).save();
    this.set('todos', Todos.Task.records());

    this.calculateCounter();

    M.ViewManager.getView('page', 'inputField').setValue('');
}

As the name indicates, we use this method to add a new todo task.

At first we have to get the input that the user entered into the text field. The application global object M.ViewManager is the perfect one to ask for the `ìnputFieldobject and itsvalue``.

If no text was entered before pressing the return key we do nothing and return. We don't want empty tasks in our list. If a text has been entered we create a new Task with it and save it:

Todos.Task.newRecord( { text: text } ).save();

We create a new model blueprint with create (see above at The model Task). But when creating new objectsof this blueprint (we call them records) , we use newRecord and pass it all property values it needs. By using this method, we ensure that all properties passed as parameters are put at the correct place inside the model object where they are taken for persistence. If we would use create here, we would not have this behaviour. Here, we simply pass the text that we've received from the text field before. Now that we've created a new task. To persist it, we call save on the model object.

Now that the model was added and saved to storage we set the todos property and re-calculate the counter. This invokes two updates in the view: one for the label showing the number of items and the other one in the list with a new item.

As a last (cosmetic) step, we empty the text field to let the user easily add one todo task after the other.

Now let's have a look at removeTodo:

removeTodo: function(domId, recordId) {
    var doDelete = confirm('Do you really want to delete this item?');
    if(doDelete) {
      var record = Todos.Task.recordManager.getRecordForId(recordId);
      record.del();
      this.set('todos', Todos.Task.records());
      this.calculateCounter();
    }
}

The domId (meaning the value of the tag's ID attribute) is passed to every method that's being invoked by the view through an event (=> the event dispatcher does this preparation). For 'click' events also the recordId is passed, if available. We need it here. But before removing anything, we want the user to be able to cancel the operation. Therefor we first popup a confirm box by using javascript's confirm method. Only if the user pressed confirmed to the question by pressing OK in the box, the operation is taking place.

First we get the model from the models M.RecordManager with getRecordForId. Every model has a record manager. We used it before with the records methods, which simply delegates a call to the model's record manager to simply return all currently saved models in memory. Next, we delete it. It is automatically deleted from the record manager's record list also. Then we set our controller property todos with the now updated record manager record list by calling Todos.Task.records(). As a last step we re-calculate the counter. That's all.

But how can we remove the todo items? This is a really good question. If we look at the list we can nowhere find a delete button or something similar. But we rember having added a ToggleView. The buttons inside this view had our TodoController as target and an edit method as action. Here it is:

edit: function() {
    M.ViewManager.getView('page', 'todoList').toggleRemove({
      target: this,
      action: 'removeTodo'
    });
}

It simply makes one call to the toggleRemove method of the ListView. We pass it two properties: a target and an action inside the target. toggleRemove activates the edit mode of a list and forces it to re-render itself and to display a remove button for every list view item. Every remove button receives the target and action property that was passed to toggleRemove. In our case, every click event will trigger removeTodo of the TodoController for the item where the button was clicked. When the ToggleView is clicked again, the edit mode ends.