Fork me on GitHub

Toura Mulberry

0.3 Brings New Tools for Interactive Apps

| Comments

Dan Imal gave a good overview of the 0.3 release of Mulberry the other day, but I wanted to follow up by taking a deeper dive into some of the new features that landed that make it easier to use Mulberry to build all kinds of apps. I’ll be talking about all of this at some upcoming events, but if you can’t make it to any of them, hopefully this post will give you good insight into how these improvements can help you build apps faster using Mulberry.

Probably the easiest way to understand these features is to look at a simple Todos app. Our app will have a home page that lets you add todo items and view unfinished ones, and a page that shows completed items; those items, and their state, will persist on the device until the user deletes them. You can follow along with the completed app on GitHub.

A Mulberry Todos App

Once we’ve scaffolded our app by running mulberry scaffold todos, we know that we’ll need a few things:

  • A page definition for the Todos page
  • A page definition for the Completed page
  • A store for the todo items
  • Components for entering todos, listing todos, and working with todos en masse

We can add all of those things via the command line:

1
2
3
mulberry create page_def todos completed
mulberry create store todos
mulberry create component TodoForm TodoList TodoTools

We’ll also want to specify routes for our two pages. As of 0.3, we get a new API method, mulberry.page, that lets us define a data-driven route and skip the need to create a markdown file for the pgae. Here’s what our routes.js file will look like:

javascript/routes.js
1
2
3
4
5
6
7
8
9
10
11
dojo.provide('client.routes');

mulberry.page('/todos', {
  name : 'Todos',
  pageDef : 'todos'
}, true);

mulberry.page('/completed', {
  name : 'Completed',
  pageDef : 'completed'
});

Each of these calls to mulberry.page passes an object that has details about how to display the page associated with the route, and in each case that object has a pageDef property. Page definitions spell out how different components will be laid out for a given page, and what capabilities that page will have. Here’s the page definition for the todos page, located at page_defs/todos.yml:

page_defs/todos.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todos:
  capabilities:
  - name: PageTodos
  screens:
    - name: index
      regions:
        - className: todo-form-container
          components:
          - custom.TodoForm
        - scrollable: true
          className: todo-list-container
          components:
          - custom.TodoList
        - className: todo-tools-container
          components:
          - custom.TodoTools

The 0.3 release also introduced the concept of “stores,” which help us persist and query data. The default behavior of a store is to persist the data on device using local storage, though you can override or extend that behavior if you’d like. Here’s our todos store, located at javascript/stores/todos.js:

javascript/stores/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
dojo.provide('client.stores.todos');

mulberry.store('todos', {
  model : 'Todo',

  finish : function(id) {
    this.invoke(id, 'finish');
  },

  unfinish : function(id) {
    this.invoke(id, 'unfinish');
  }
});

This creates a store at client.stores.todos; that store will automatically get methods like add, query, remove, and put, plus the additional finish and unfinish methods that we’ve specified in the prototype we passed. Those last two methods are used to pass messages to individual models.

We also specify a model property, which tells the store to expect that we’ve defined a Todo model. We do so in javascript/models/Todo.js, and it looks like this:

javascript/models/Todo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
dojo.provide('client.models.Todo');

mulberry.model('Todo', {
  complete : false,

  finish : function() {
    this.set('complete', true);
  },

  unfinish : function() {
    this.set('complete', false);
  }
});

Now, we’re ready to wire up our app’s todos page. When we generated the page definition file for the todos page, Mulberry automatically assigned it a capability called PageTodos; this capability will be responsible for getting the page set up, and then brokering communication between the components as the user works with their todo items.

Here’s what that capability, located at javascript/capabilities/PageTodos.js, looks like, with some inline comments to make it easier to follow:

javascript/capabilities/PageTodos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
dojo.provide('client.capabilities.PageTodos');

mulberry.capability('PageTodos', {
  /*
   * The capability expects the following components to be present in order for
   * the capability to work.
   */
  requirements : {
    todoList : 'custom.TodoList',
    todoForm : 'custom.TodoForm',
    todoTools : 'custom.TodoTools'
  },

  /*
   * These "listeners" will be set up as part of setting up the page. So, for
   * example, when the TodoForm component instance announces that a user has
   * added a todo (by calling its `onAdd` method), then the capability's `_add`
   * method will be run.
   */
  connects : [
    [ 'todoForm', 'onAdd', '_add' ],
    [ 'todoList', 'onComplete', '_complete' ],
    [ 'todoList', 'onDelete', '_delete' ],
    [ 'todoTools', 'onCompleteAll', '_completeAll' ]
  ],

  /*
   * When the page is set up, we'll grab a reference to the todos store, and
   * then update the list of todos.
   */
  init : function() {
    this.todos = client.stores.todos;
    this._updateList();
  },

  _delete : function(id) {
    this.todos.remove(id);
    this._updateList();
  },

  _add : function(item) {
    this.todos.add(item);
    this._updateList();
  },

  _complete : function(id) {
    this.todos.finish(id);
    this._updateList();
  },

  _updateList : function() {
    var items = this.todos.query(function(item) {
      return !item.complete;
    });

    this.todoList.set('todos', items);
  },

  _completeAll : function() {
    this.todos.query(function(item) {
      return !item.complete;
    }).forEach(dojo.hitch(this, function(t) {
      t.finish();
      this.todos.put(t);
    }));

    this._updateList();
  }
});

All that’s left to do now is to actually make the components do what they need to do. Components are intentionally “dumb” – their job is to receive data from an outside source, render that data using a template, and announce user interaction by calling (often empty) methods. Here, for example, is the TodoForm component, located at javascript/components/TodoForm.js:

javascript/components/TodoForm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dojo.provide('client.components.TodoForm');

mulberry.component('TodoForm', {
  componentTemplate : dojo.cache('client.components', 'TodoForm/TodoForm.haml'),

  init : function() {
    this.connect(this.domNode, 'submit', function(e) {
      e.preventDefault();

      var description = dojo.trim(this.descriptionInput.value),
          item;

      if (!description) { return; }

      item = { description : description };
      this.domNode.reset();
      this.onAdd(item);
    });
  },

  onAdd : function(item) { }
});

It displays the form (using the template located at javascript/components/TodoForm/TodoForm.haml), listens for the user to submit the form, checks to make sure there was some input, and then calls its onAdd method, which the PageTodos capability has registered its interest in. The other components are similarly simple.

The Mulberry framework was born out of our experience with making content-rich apps, but we’re hard at work making it easy to use Mulberry to build all kinds of apps – pretty much anything that might manifest itself as a single-page JavaScript application. We hope you’ll download the 0.3 version, take the Todos demo for a spin, and let us know how we’re doing.

Comments