Fork me on GitHub

Toura Mulberry

Creating Custom Functionality With Mulberry

| Comments

Out of the box, Mulberry’s great at making simple content-driven apps, but one of the things we heard loud and clear during the closed alpha was that developers want to use Mulberry to make more complex, data-driven apps as well. No worries – Mulberry has that covered too.

In this post, we’ll take a look at what’s involved in creating a Twitter app – Twitter is the new “hello world”, after all. In the process, we’ll learn about creating custom components, templates, interactions, and routes. You can follow along by downloading Mulberry and heading to the demos/twitter directory.

To start, we’ll create a new Mulberry project named “twitter”. Once you’ve downloaded Mulberry and added the location of the binary to your $PATH, you can run the following command from any location on your filesystem where you have write permission:

1
mulberry scaffold twitter

This will create a new directory named twitter; you’ll want to cd twitter so you’re inside the project as you follow along.

An Overview of the App

The app we’re building will have two pages:

  • A home page that shows a list of people, and allows users to choose from the list; choosing a person from the list will show a map of the city where that person lives, their latest tweet, and their Twitter bio with a link to see all of their tweets.
  • A secondary page that shows a person’s 10 latest tweets.

Creating the Data

Since this is a data-driven app, the first thing we need to do is provide it with some seed data to drive it. We’ll run this command from the root of our Mulberry project:

1
mulberry create_data users

This creates a data file named users.yml in the assets/data/ directory inside your project. It’s an empty file, so we’ll add some information about our users to the file, in the YAML format:

demos/twitter/assets/data/users.yml
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
type: users
users:
  - name: Paul Irish
    twitter: paul_irish
    location:
      lat: 37.7749295
      lng: -122.4194155
    image: https://twimg0-a.akamaihd.net/profile_images/1326877605/greenavatar_crop_normal.jpg
  - name: Alex Sexton
    twitter: SlexAxton
    location:
      lat: 30.267153
      lng: -97.7430608
    image: https://twimg0-a.akamaihd.net/profile_images/1384837213/SlexAxtonAvatar_normal.jpg
  - name: Adam Sontag
    twitter: ajpiano
    location:
      lat: 40.7143528
      lng: -74.0059731
    image: https://twimg0-a.akamaihd.net/profile_images/1396366703/twitpic_pool_normal.png
  - name: Rebecca Murphey
    twitter: rmurphey
    location:
      lat: 35.9940329
      lng: -78.898619
    image: https://twimg0-a.akamaihd.net/profile_images/1447727594/IMG_8534_normal.jpg

Note that there are two pieces to this data: a type property, and a users array that contains the actual data. The type property will help us locate the data later; its presence means that a page can have easy access to many different kinds of data.

Now that we’ve added this data to our project, we can move on to setting up the home page.

Creating the Home Page

When you scaffold a Mulberry app, a home page is automatically created in your project’s pages/ directory. By default, this page uses the home-tablet and home-phone templates, but we’ll want to change that, as well as tell the page that it should have access to the data we created. Here’s what our home.md file looks like when we’re done:

demos/twitter/pages/home.md
1
2
3
4
5
6
---
title: Home
template: twitter
data:
  - users.yml
---

Next, let’s take a closer look at the mockup of the home page. There are four distinct pieces of functionality on the page, and in Mulberry we refer to these pieces of functionality as “components”:

Breaking the home page into components

Mulberry has a GoogleMap component built in, but we’ll need to create custom components for the rest:

1
mulberry create_component LatestTweet UserInfo UserList

Running this command creates skeleton files in the project’s javascript/components directory for each of the components, and adds dojo.require statements to the project’s javascript/base.js file so that the components will automatically be available to the rest of your code.

Writing Our Custom Components

Components in Mulberry have three jobs:

  • receiving data from an external source
  • rendering that data
  • announcing user interaction with the component, if applicable

Mulberry automatically provides every component on a page with the information associated with that page; it puts that information into a component’s baseObj property. This means that our components will automatically get access to the user data we associated with the home page.

Because we remembered to add a type property to that data, fetching the array of people in the users.yml file from inside a component is easy:

1
this.baseObj.getData('users').users

Let’s look at the UserList component as an example. Its job is to receive data (in this case, a list of people); render that data (in this case, as an unordered list), and then announce user interaction (in this case, when a user selects a person from the list).

Here’s what our UserList component ends up looking like:

demos/twitter/javascript/components/UserList.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
dojo.provide('client.components.UserList');

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

  prep : function() {
    this.users = this.baseObj.getData('users').users;
  },

  init : function() {
    this.connect(this.domNode, 'click', '_handleClick');
  },

  _handleClick : function(e) {
    var target = e.target;

    while (target !== this.domNode && target.parentNode !== this.domNode) {
      target = target.parentNode;
    }

    if (target.nodeName.toLowerCase() !== 'li') { return; }
    if (dojo.hasClass(target, 'selected')) { return; }

    this.query('.selected').removeClass('selected');
    dojo.addClass(target, 'selected');

    this.onSelect(dojo.attr(target, 'data-twitter-username'));
  },

  onSelect : function(username) {
    // stub for connection
  }
});

The prep and init methods are called automatically; the prep method is called before the component’s DOM structure is created, and the init method is called after its DOM structure is created.

In the case of the UserList component, we use the prep method to prepare the data associated with the page. Then, in the init method, we tell the component to listen for user interaction – in this case, a click on the component’s root DOM node. A click on that node will result in the component’s _handleClick method being called; if _handleClick decides that the click was on a list item, then it will call the component’s onSelect method, passing it the username of the person who was selected.

Note that the onSelect method doesn’t do anything. Later in this post, we’ll see how we can “connect” to that method from another part of our code, but it’s important to understand this key concept in Mulberry apps: components should never directly affect other pieces of the application. Their job is to receive, render, and announce – nothing more.

Some of the components in our app will need to receive data once they’re already on the page. We can use “setter methods” to make this possible. For example, the UserInfo component should be able to display a different user without having to re-create the component from scratch. To allow this, we create a _setUserAttr method on the UserInfo component

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

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

  _setUserAttr : function(user) {
    this.nameNode.innerHTML = user.name;
    this.twitterLinkNode.href = '#/twitter/' + user.twitter;
    this.bioNode.innerHTML = user.bio || '';
  }
});

This means that any code that has access to an instance of the UserInfo component can do the following:

1
myUserInfoInstance.set('user', userObject);

Whenever the set method is called on a component instance, the component looks for a setter method that matches the property name passed as the first argument to set. If it finds a corresponding method, it calls it; if it does not find a corresponding method, then it simply sets the value of a property on the component instance.

(This functionality is based entirely on Dojo’s dijit._WidgetBasesee how it works here.)

In the UserInfo component, we refer to several properties that do not seem to be defined anywhere: this.nameNode, this.bioNode, etc. These properties get set automatically by the component’s template using “attach points.” Here’s the UserInfo template, located at javascript/components/UserInfo/UserInfo.haml:

demos/twitter/javascript/components/UserInfo/UserInfo.haml
1
2
3
4
5
.component.user-info
  %h1{ dojoAttachPoint : 'nameNode' }
  %p.bio{ dojoAttachPoint : 'bioNode' }
  %p
    %a{ dojoAttachPoint : 'twitterLinkNode' } View all tweets

Using attach points greatly reduces the need for querying the DOM; you can create an attach point on any node, and then refer to that node from inside your component by using the attach point’s name:

1
this.myAttachPointName

Creating the Page Template

Once we’ve created our components (see the demo in the repo for details on how the rest of the components are set up), it’s time to assemble them into a page template. Earlier, we told our home page to use the twitter template; now, we’ll ask Mulberry to create that template for us:

1
mulberry create_template twitter

This creates a file at templates/twitter.yml that contains the skeleton of a template. We’ll fill it out with the details about how we want our page to look:

demos/twitter/templates/twitter.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twitter:
  screens:
    - name: index
      regions:
        - size: fixed
          components:
            - custom.LatestTweet
        - size: fixed
          components:
            - GoogleMap
        - containerType: column
          regions:
            - className: user-list
              size: fixed
              components:
              - custom.UserList
            - className: user-info
              size: flex
              components:
              - custom.UserInfo

Again, we use YAML here, this time to say that we want a page template named “twitter” that has one screen named “index” (page templates can have more than one screen, with the intention that only one screen is visible at any time, but the details of that are beyond the scope of this post). That screen is broken down into regions:

  • The first region will be a fixed height, and it will contain our custom LatestTweet component.
  • The second region will be a fixed height, and it will contain the built-in GoogleMap component.
  • The third region will be split into two sub-regions, which will be displayed as columns (that is, side-by-side). The first sub-region will contain the custom UserList component, and will get the class name user-list so that we can target it with CSS; the second sub-region will contain the custom UserInfo component, and will get the class name user-info. The sub-region that contains UserList will be fixed-width; the UserInfo sub-region will flex to fill the remaining size.

At this point, we should be able to serve our application using the Mulberry development server:

1
mulberry serve

If you navigate to the home page, you’ll see that all of the components display in the proper arrangement, but not much is happening yet. We need some data.

Loading the External Data

We want to create an interface to the Twitter data we’ll need in order to populate our pages. For our home page, we’ll need a user’s latest tweet, as well as their bio; for our secondary page, we’ll need a users 10 most recent tweets. We can start by asking Mulberry to create a skeleton file for our datasource:

1
mulberry create_datasource Twitter

This creates a file at javascript/data/Twitter.js, and adds a dojo.require statement to our project’s javascript/base.js file. There’s not much here (we’re working on a more elaborate API for remote data sources), so for now it’s up to us to fill in the details:

demos/twitter/javascript/data/Twitter.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
dojo.provide('client.data.Twitter');

mulberry.datasource('Twitter', {
  getLatest : function(username) {
    return this._get(username, 1).then(dojo.hitch(this, '_getLatest'));
  },

  getAll : function(username) {
    return this._get(username, 10).then(dojo.hitch(this, '_getAll'));
  },

  _get : function(username, count) {
    var url = 'http://twitter.com/status/user_timeline/${username}.json?count=' + (count || 10);

    return dojo.io.script.get({
      url : toura.tmpl(url, { username : username }),
      callbackParamName : 'callback'
    });
  },

  _getLatest : function(data) {
    if (!data || !data.length) { return false; }
    return this._formatTweet(data[0]);
  },

  _getAll : function(data) {
    return dojo.map(data, this._formatTweet);
  },

  _formatTweet : function(tweet) {
    return {
      text : tweet.text,
      date : dojo.date.locale.format(new Date(tweet.created_at)),
      bio : tweet.user.description
    };
  }
});

There’s not much that’s interesting here, except that this code takes advantage of the fact that all async methods in Dojo – such as dojo.io.script.get – return a “promise.” Promises are incredibly useful structures that greatly facilitate the development of asynchronous processes. A promise is, quite literally, a promise: a promise object is a guarantee that when the async operation is completed, the promise will execute any function that was passed to it via the promise’s then method.

In this example, the getLatest method receives the promise returned by the _get method, then attaches a callback to it using the then method of the promise. The then method ultimately returns another promise, which is resolved with the return value of the _getLatest method. This means that another piece of code with access to an instance of the Twitter datasource can do this:

1
2
3
myTwitterInstance.get('rmurphey').then(function(tweets) {
  console.log('these are the tweets', tweets);
});

The code inside the function will run once the tweets have been fetched.

Connecting the Components

We have our page template, we have our components, and we have our data – now it’s time to glue it all together. Mulberry uses “capabilities” to broker communication between components and datasources. We’ll ask Mulberry to create a capability that we’ll use to encapsulate the functionality of our Twitter page:

1
mulberry create_capability Twitter

This creates a skeleton file at javascript/capabilities/Twitter.js, and adds a dojo.require statement to our project’s javascript/base.js file.

Capabilities have a requirements object that indicates the components that it expects to be present. It assigns those components names that will be used to reference the components inside the capability. Capabilities also have a connects array, which contains zero or more arrays that describe how the capability will react when a certain method on a component is called. Remember how we had an empty onSelect method in our UserList component? Our Twitter capability connects to it, and describes how the rest of the page should react. Finally, capabilities have an init method, where you can do initial setup of components that might be page-dependent.

Here’s the Twitter capability in its entirety; you can see how all of the pieces we’ve talked about above come together:

demos/twitter/javascript/capabilities/Twitter.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
dojo.provide('client.capabilities.Twitter');

mulberry.capability('Twitter', {
  requirements : {
    latestTweet   : 'custom.LatestTweet',
    map           : 'GoogleMap',
    userList      : 'custom.UserList',
    userInfo      : 'custom.UserInfo'
  },

  connects : [
    [ 'userList', 'onSelect', '_onUserSelect' ],
    [ 'map', 'onMapBuilt', '_onMapBuilt' ]
  ],

  init : function() {
    this.users = this.baseObj.getData('users').users;
    this.twitter = new client.data.Twitter();

    var user = this.users[0];
    this.userInfo.set('user', user);
    this._loadUser(user);
  },

  _onMapBuilt : function() {
    this.map.set('center', this.users[0].location);
  },

  _onUserSelect : function(username) {
    var user = dojo.filter(this.users, function(u) {
      return u.twitter === username
    })[0];

    this._loadUser(user);
    this.map.set('center', user.location);
  },

  _loadUser : function(user) {
    var req = this.twitter.getLatest(user.twitter);
    req.then(dojo.hitch(this.latestTweet, 'set', 'tweet'));
    req.then(dojo.hitch(this, function(tweet) {
      user.bio = tweet.bio;
      this.userInfo.set('user', user);
    }));
  }
});

Adding a Custom Route for the User Tweets Page

Our last task before our Twitter app is complete is to create the secondary page, which shows a specific person’s recent tweets. We already know how to set up a page template and a new component, and how to fetch the Twitter data using the datasource we created, but how do we get the page to figure out which person’s Tweets to load?

For the sake of this discussion, let’s assume that we’ve created another page template named user, and that the page template includes a custom component named Tweets. (You can see this page template and the custom component in the demo in the repo.)

In the UserInfo component, we indicated that we want to access the page by visiting #/twitter/<username>. We need to define a custom route that will run when a user navigates to this kind of page; the route will need to capture the username from the URL’s hash, and then get the proper data to the components on the page. We can add the following to our project’s javascript/base.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dojo.subscribe('/routes/loaded', function() {

  mulberry.app.Router.registerRoute('/twitter/:username', function(params) {
    var twitter = new client.data.Twitter(),
        page = mulberry.app.PageFactory.createPage({
          pageController : 'user',
          tweets : twitter.getAll(params.username),
          name : params.username
        });

    mulberry.app.UI.showPage(page);
  });

});

This code tells the Mulberry router to be on the lookout for a URL hash that looks like /twitter/:username. When the router sees this hash, it should run the provided function. The provided function receives a params object, which contains any parameters that were included in the hash. So, in this case, our params object would have a username property, containing whichever username was in the URL hash.

Inside the function, we create an instance of our Twitter datasource, and then we ask the Mulberry PageFactory to create a page for us by passing it an object. This object will be available to all components on the page; it also must have a pageController property, which the PageFactory will use to determine which page template to use in creating the page.

The object that we pass to the PageFactory’s createPage method also has a tweets property, and here we see the power of promises again. We use the tweets property to pass the promise that’s returned by the Twitter datasource; by doing this, we allow the Tweets component to receive data without interacting directly with the datasource. The Tweets component simply attaches a callback to the promise using the promise’s then method. When the promise resolves, it provides an array of tweets to any callbacks that were attached; the Tweets component uses that array to populate itself.

Conclusion

Mulberry’s built-in components and page templates are focused on facilitating the rapid creation of static content apps, but the underlying patterns, tools, and architecture provide powerful tools for building all kinds of apps. If you build something interesting, we hope you’ll let us know!

Comments