This tutorial is for Dojo 1.6 and may be out of date.

Up to date tutorials are available.

Getting Started with TweetView: Tweets and Mentions

In the previous post, Introduction to TweetView, we introduced the mobile application we will be building with dojox.mobile: TweetView. We built the general layout template for our application and now it's time to make TweetView work. This tutorial will focus specifically on the "Tweets" and "Mentions" views of our application. Before we begin coding our application, let's set up our application file structure and review a few mobile app development concepts.

Setting Up the Project File Structure

TweetView File Structure

TweetView will use the same project structure used for most Dojo-powered projects: the application HTML at the root level and TweetView's various JavaScript classes, imagery, and stylesheets living in our namespaced-named directory within the js and resources directories respectively.

TweetView uses very few custom images to cut down on download time. To make image resources as small as possible, use a utility like Pngcrush to optimally compress each image. The CSS file can be compressed during the Dojo build process.

Before We Start Coding: Mobile Development Guidelines

Hold up! Before we jet off into coding TweetView, there are a few things we need to remember about dojox.mobile and mobile web development in general:

  • Size Matters
    Every byte counts when it comes to creating mobile applications, so taking shortcuts you wouldn't otherwise take in a standard web app is acceptable. Remember that every dependency (in the form of Dojo classes, base or otherwise) you add increases download time to your users.
  • Best Practices: Mobile != Web
    Best practices for JavaScript and any JavaScript toolkit dictate certain best practices. A few examples include not extending natives, not using global variables, and creating very flexible/generic classes. These best practices can cost you lots of extra code, so you may have to loosen your rules to create an efficient mobile app.
  • Keep It Simple
    Creating an overly complex mobile application with a million custom styles and widgets and layouts will get you in trouble quickly. Creating a simple layout, then adding to it, is the optimal way to code mobile web applications.

We will not be throwing all best practices out the door with TweetView. We will, however, find a solid balance of size savings vs. strict Dojo best practices.

With these ideas in mind, there's one last pre-code task to complete: preventing caching.

Mobile Devices and Caching

Most mobile devices rely heavily on caching to ease the burden of data transfer. That's great for production apps but we're in the initial development phase so caching will be nothing short of frustrating when testing the application. Let's add a few cache-preventing META tags to our HTML page before developing the Settings view:

<!-- prevent cache -->
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="pragma" content="no-cache">

These META tags will be hugely helpful during development. They will need to be removed once TweetView goes into production.

The Tweets and Mentions View

Tweets View

The "Tweets" and "Mentions" views are the most complex of the three views. They are very similar in look and functionality, so we'll focus on creating one class that accommodates both uses. In looking at the mockup, we know we'll need the following widgets:

  • dojox.mobile.ScrollableView - The entire view itself
  • dojox.mobile.Heading - The main "Tweets" heading
  • dojox.mobile.RoundRectList - The account list holder
  • dojox.mobile.ListItem - The account list items

The mockup also makes clear that we'll be interacting with Twitter to get user information (avatars, tweets, times, etc.), so we'll need a few more Dojo resources:

  • dojo.io.script - JSONP solution to get information from Twitter
  • dojo.DeferredList - Allows handling of multiple Twitter requests once all information is received

The bottom bar was added and completed in the previous tutorial.

Developing TweetView

Ready to get your hands dirty? Great! Let's develop one piece at a time.

Adding Resources to the Page

We'll need to add our stylesheet, TweetView.css, to the app.html page:

<link href="js/tweetview/resources/TweetView.css" rel="stylesheet" />

This stylesheet will contain every style used within the entire app — not just the Settings view.

The Tweets and Mentions View Templates

We created the basic Tweets and Mentions template view in the previous tutorial:

<!-- tweets view -->
<div id="tweets" dojoType="dojox.mobile.ScrollableView" selected="true">
	<h1 dojoType="dojox.mobile.Heading">
		<!-- the refresh button -->
		<div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" style="float:right;" icon="images/refresh.png"></div>
		Tweets
	</h1>
	<ul dojoType="dojox.mobile.RoundRectList">
		<li dojoType="dojox.mobile.ListItem">
			Tweet item here
		</li>
	</ul>
</div>

<!-- mentions view -->
<div id="mentions" dojoType="dojox.mobile.ScrollableView">
	<h1 dojoType="dojox.mobile.Heading">
		<!-- the refresh button -->
		<div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" style="float:right;" icon="images/refresh.png"></div>
		Mentions
	</h1>
	<ul dojoType="dojox.mobile.RoundRectList">
		<li dojoType="dojox.mobile.ListItem">
			Mention tweet item here
		</li>
	</ul>
</div>

These template will need to be updated once our new widget is complete, but we'll keep this HTML layout in mind when creating the Tweets and Mentions view JavaScript.

New Class: tweetview._ViewMixin

When looking at all three views (Tweets, Mentions, and Settings), it's apparent that each view will need access to the dojox.mobile.RoundRectList widget's domNode. To avoid duplicate code across each view class we create, I've created _ViewMixin which will provide a method for gaining a reference to that list node and subsequently allow us to show and hide it at will. _ViewMixin also features a substitute method, which acts as a very primitive templating system, and a getElements method to retrieve elements by CSS class name.

// Provide the class
dojo.provide("tweetview._ViewMixin");

// Declare the class
dojo.declare("tweetview._ViewMixin", null, {
	// Returns this pane's list
	getListNode: function() {
		return this.getElements("tweetviewList",this.domNode)[0];
	},
	// Updates the list widget's state
	showListNode: function(show) {
		dojo[(show ? "remove" : "add") + "Class"](this.listNode, "tweetviewHidden");
	},
	// Pushes data into a template - primitive
	substitute: function(template,obj) {
		return template.replace(/\$\{([^\s\:\}]+)(?:\:([^\s\:\}]+))?\}/g, function(match,key){
			return obj[key];
		});
	},
	// Get elements by CSS class name
	getElements: function(cssClass,rootNode) {
		return (rootNode || dojo.body()).getElementsByClassName(cssClass);
	}
});

These methods will come in handy along the entire course of the project.

Take a moment to look at each mockup and you'll see the need for shared list widget access, templating, and header movement amongst each view. Don't fear if you don't see the uses for the _ViewMixin methods yet — we'll rehash them when creating each view.

Also note that getElements acts as an alternative to dojo.query. The dojo.query method is a large piece of code and we simply need to access classes by className, so avoiding dojo.query as a dependency will boost our application's loading time.

New Class: tweetview.TweetView

Since the Tweets and Mentions views are going to be very specific in content, we're going to create a new class called tweetview.TweetView. This class will extend from ScrollableView and _ViewMixin, and will "oversee" all functionality and content within the view itself:

// Provide the UI class
dojo.provide("tweetview.TweetView");

// Dependencies here
dojo.require("tweetview._ViewMixin");
dojo.require("dojox.mobile.ScrollableView");
dojo.require("dojo.DeferredList");
dojo.require("dojo.io.script");

// Require localization for time
dojo.require("dojo.i18n");
dojo.requireLocalization("dojo.cldr", "gregorian", "", "");

// Declare the class;  inherits from ScrollableView
dojo.declare("tweetview.TweetView",[dojox.mobile.ScrollableView,tweetview._ViewMixin],{

	// Options and methods will go here...

});

The additional dependencies we identified in the "The Tweets and Mentions View" section above have been required within the new class. We've also required localization classes so that our time labels (i.e. "hour", "minute", "day") will display the proper verbiage.

TweetView Properties

With the TweetView class created, and its use for both Tweets and Mentions, we must now think of what's different between the two, and create class properties from them. Right away, we know that the URL format for retrieving tweets and mentions will be different, so that must become a class property. We'll use the tweet format as the default:

// URL to pull tweets from; simple template included
serviceUrl: "http://twitter.com/statuses/user_timeline/${account}.json?since_id=${since_id}",

Note the ${account} and ${since_id} snippets in the serviceUrl. Remember _ViewMixin we created above? The URL will go through our substitute method each time a user's information is requested.

Looking at the mockup, you'll also see that the tweet list items must be specially formatted to include the user's name, avatar, tweet text, and time since the tweet was posted. Let's create a property which will accommodate a tweet template:

// Create a template string for tweets:
tweetTemplateString: '<img src="${avatar}" alt="${name}" class="tweetviewAvatar" />' +
'<div class="tweetviewTime" data-dojo-time="${created_at}">${time}</div>' +
'<div class="tweetviewContent"> ' +
'<div class="tweetviewUser">${user}</div>' +
'<div class="tweetviewText">${text}</div>' +
'</div><div class="tweetviewClear"></div>',

The last custom property we'll need is the path to the "refresh" GIF which will spin when tweets are being requested:

// Icon for loading...
iconLoading: dojo.moduleUrl("tweetview", "resources/images/loading.gif"),

By using dojo.moduleUrl, we avoided a hardcoded path. This approach allows for greater flexibility when referencing in any resources — you can't always assume that you know where your module's code will be source from!

Now that we have our class and options defined, we have to update our HTML template to use the new TweetView class and provide any custom options for the specific view.

Implementing TweetView

As is the case when using any Dijit widget, you must require its resource within the app.html file:

// For the "Tweets" and "Mentions" view
dojo.require("tweetview.TweetView");

With the class required in the page, it's time to update the HTML template for the two views:

<!-- tweets view -->
<div id="tweets" dojoType="tweetview.TweetView" selected="true">
	<h1 dojoType="dojox.mobile.Heading" fixed="top">
		<!-- the refresh button -->
		<div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" icon="js/tweetview/resources/images/refresh.png"></div>
		Tweets
	</h1>
	<ul dojoType="dojox.mobile.RoundRectList" class="tweetviewList"></ul>
</div>

<!-- mentions view -->
<div id="mentions" dojoType="tweetview.TweetView" serviceUrl="http://search.twitter.com/[email protected]${account}&amp;since_id=${since_id}">
	<h1 dojoType="dojox.mobile.Heading" fixed="top">
		<!-- the refresh button -->
		<div dojoType="dojox.mobile.ToolBarButton" class="mblDomButton tweetviewRefresh" icon="js/tweetview/resources/images/refresh.png"></div>
		Mentions
	</h1>
	<ul dojoType="dojox.mobile.RoundRectList" class="tweetviewList"></ul>
</div>

Note the changes we've made to the template:

  • Each view has been changed from a dojox.mobile.ScrollableView to a tweetview.TweetView
  • The mentions widget features a custom serviceUrl attribute for retrieving tweets
  • fixed="top" has been added to the headings so that they stay glued to the top of each view
  • "float:right;" has been removed from the refresh buttons — that style directive will be moved to the stylesheet we will create later in the tutorial
  • The tweetviewList CSS class has been added to the child RoundRectList widgets for the sake of styling and node retrieval from within its parent TweetView widget.

Note that we're using Twitter's search API to pull in the set of mentions. Twitter requires OAuth authentication for their mentions API, and implementing OAuth is well out of scope for this application. If you do choose to implement OAuth, all you would then need to do is change the serviceUrl parameter and you'll be set!

Handling Twitter Accounts

Each view within TweetView (Tweets, Mention, and Settings) relies on a single list of account information. That means that the account information has to be available to all TweetView widgets. We could create a "controller" widget which oversees all TweetView widgets and manages account state, but that's extra overhead that we don't need just yet. Instead, we'll pin the account listing onto the tweetview namespace:

// Set accounts for tweetview directly on the namespace
tweetview.ACCOUNTS = {
	dojo: { enabled: true },
	sitepen: { enabled: true }
};

The ACCOUNTS object contains a series of objects representing account names and their enabled state. More properties will be added to each account's object, but none of them need to be initialized.

TweetView startup

With the accounts defined, HTML templates complete, and base widget code in place, it's time to add the widget's startup method. Let's go through it line by line.

Retain the widget's native startup method functionality:

// When the widgets have started....
startup: function() {
// Retain functionality of startup in dojox.mobile.ScrollableView
this.inherited(arguments);

Get a reference to this widget's refresh button and save the original image path (for when we change it back from the refresh icon after it's clicked):

// Get the refresh button and image
this.refreshButton = dijit.byId(this.getElements("tweetviewRefresh", this.domNode)[0].id);
this.iconImage = this.refreshButton.iconNode.src;

Add a click event to the refresh button which refreshed tweets on the widget:

// Add a click handler to the button that calls refresh
dojo.connect(this.refreshButton, "onClick", this, "refresh");

Immediately load tweets by calling the refresh method:

// Grab tweets right away!
this.refresh();

This refresh call will be removed once the Settings view is completed, but its presence is required during this class' development.

Add the tweetviewPane CSS class to the widget for styling purposes:

// Add CSS class for styling
dojo.addClass(this.domNode, "tweetviewPane");

Get reference to the child RoundRectList widget for later use. Hide it, as it has no content from Twitter yet:

// Get the list widget
this.listNode = this.getListNode();
// Hide the list because it's not populated with list items yet
this.showListNode(false);

Create a interval that fires every minute to update tweet ages:

// Every 60 seconds, update the times
setInterval(dojo.hitch(this, function() {
	dojo.forEach(this.getElements("tweetviewTime", this.domNode), function() {
		timeNode.innerHTML = this.formatTime(dojo.attr(timeNode, "data-dojo-time"));
	},this);
}),60000);

That's the TweetView startup method that sets all of the wheels into motion. We still need to create the refresh method to update the tweet list so let's do it!

TweetView refresh

The refresh method is charged with pulling tweets from Twitter. Since we cannot pull multiple users' tweets in one request to Twitter, we'll need to fire a separate request for every account and then reconcile them once they are all returned. This means we'll create separate dojo.io.script calls and handle them within one dojo.DeferredList callback:

// Contacts twitter to receive tweets
refresh: function() {
	// Updates the refresh icon
	this.refreshButton.iconNode.src = this.iconLoading;

	// Button has been "pressed"
	this.refreshButton.select();

	// For every account, add the deferred to the list
	var defs = [], accounts = tweetview.ACCOUNTS;
	for(var account in accounts) {
		// If the account is enabled...
		if(accounts[account].enabled) {
			// Get tweets!
			defs.push(dojo.io.script.get({
				callbackParamName: "callback",
				preventCache: true,
				timeout: 3000,
				// "substitute" comes from _ViewMixin
				url: this.substitute(this.serviceUrl, { account: account, since_id: accounts[account].since || 1 })
			}));
		}
	}

	// Create a dojo.Deferredlist to handle when all tweets are returned
	// Add this.onTweetsReceived as the callback
	new dojo.DeferredList(defs).then(dojo.hitch(this, this.onTweetsReceived));
},

Be sure to add a timeout parameter to dojo.io.script.get calls to ensure your error callback will properly fire.

A few notes about our refresh method:

  • The refresh image is changed to the spinning GIF when the method is called.
  • An account's tweets are only requested if the user's account is enabled.
  • Notice the since property of the account being appended to the URL. That property will be initialized after the first pull so that the same tweets aren't being pulled repeatedly.

The DeferredList's callback is the onTweetsReceived method. Let's create it!

TweetView onTweetsReceived and sortTweets

The onTweetsReceived method receives the results of all of the dojo.Deferreds created by the dojo.io.script calls. Now that we have the tweets from each account, we have a few problems:

  • The tweets are separated by user — we'll need to join them into one array. This is an easy problem to solve.
  • All tweets need to be sorted by tweet date when merged.

Before we can output tweets to the widget's list, we must sort them. The first part is joining the tweets into one array:

// Merges tweets into one array, sorts them by date
sortTweets: function(deflist) {
	// Create an array for our tweets
	var allTweets = [];

	// For each def list result...
	dojo.forEach(deflist, function(def) {
		// Define which property to check
		// Tweet is just "def[1]", Mentions is def[1].results
		var tweets = (def[1].results ? def[1].results : def[1]);

		// If we received any results in this array....
		if(tweets.length) {
			// Get the username and update the since
			var username = !tweets[0].user ? def[1].query.replace("%40","") : tweets[0].user.screen_name;

			// Update the since for this user
			tweetview.ACCOUNTS[username].since = tweets[0].id_str;

			// If this is a search, we need to add the username to the tweet
			if(def[1].query) {
				dojo.forEach(tweets, function(tweet) { tweet.searchUser = username; });
			}

			// Join into one big array
			allTweets = allTweets.concat(tweets);
		}
	},this);

Note the def[1] handling, username retrieval, and def.query check. The search API returns a different JSON structure than the user timeline API. We could have created a different subclass for mentions handling but that's overhead we don't need in our mobile application, so we'll accommodate for both in TweetView.

With the tweets in one big array, it's time to sort them by time and return them to the onTweetsReceived method:

	// Sort them by date tweeted
	allTweets.sort(function(a, b) {
		var atime = new Date(a.created_at),
			btime = new Date(b.created_at);

		// Common sorting algorithms like this would return b - a, not a - b.
		// However, we want larger times to be prioritized, not smaller times,
		// so we're doing A's time minus B's time.
		return atime - btime;
	});


	// Return the tweets
	return allTweets;
}

With the tweets in sorted order, it's time to stop our refresh widget from spinning and to send the tweets to our updateContent method for incorporation on the page:

// Event for when content is loaded from Twitter
onTweetsReceived: function(rawTweetData) {
	// Sort tweets
	tweetData = this.sortTweets(rawTweetData);

	// Set the refresh icon back
	this.refreshButton.iconNode.src = this.iconImage;
	this.refreshButton.select(true);

	// If we receive new tweets...
	if(tweetData.length) {
		// Update content
		this.updateContent(tweetData);
	}
},

Tweets are sorted oldest to newest — that ensures that the newest tweets will be pushed to the top of the list. Also note that updateContent is called only if we received new tweets from Twitter.

TweetView updateContent

The updateContent method receives the sorted tweets, creates dojox.mobile.ListItem widgets for each of them, and then pushes them to the top of the list. Before we create updateContent, we'll need a few utility methods to format tweet times and tweet text:

// Adds the proper tweet linkification to a string
formatTweet: function(tweetText) {
	return tweetText.
	replace(/(https?:\/\/\S+)/gi,'<a href="$1">$1</a>').
	replace(/(^|\s)@(\w+)/g,'$1<a href="http://twitter.com/$2">@$2</a>').
	replace(/(^|\s)#(\w+)/g,'$1<a href="http://search.twitter.com/search?q=%23$2">#$2</a>');
},

// Formats the time as received by Twitter
formatTime: function(date) {
	// Get now
	var now = new Date();

	// Push string date into an Date object
	var tweetDate = new Date(date);

	// Time measurement: seconds
	var secondsDifferent = Math.floor((now - tweetDate) / 1000);
	if(secondsDifferent < 60) {
	return secondsDifferent + " " + (this.l10n["field-second"]) + (secondsDifferent > 1 ? "s" : "");
	}

	// Time measurement: Minutes
	var minutesDifferent = Math.floor(secondsDifferent / 60);
	if(minutesDifferent < 60) {
		return minutesDifferent + " " + this.l10n["field-minute"] + (minutesDifferent > 1 ? "s" : "");
	}

	// Time measurement: Hours
	var hoursDifferent = Math.floor(minutesDifferent / 60);
	if(hoursDifferent < 24) {
		return hoursDifferent + " " + this.l10n["field-hour"] + (hoursDifferent > 1 ? "s" : "");
	}

	// Time measurement: Days
	var daysDifferent = Math.floor(hoursDifferent / 24);
	return daysDifferent + " " + this.l10n["field-day"] + (daysDifferent > 1 ? "s" : "");
},

With these methods in place, we can create our updateContent method:

// Fires when tweets are received from the controller
updateContent: function(rawTweetData) {

	// For every tweet received....
	dojo.forEach(rawTweetData, function(tweet) {
		// Get the user's screen name
		var screenName = tweet.searchUser || tweet.user.screen_name;

		// Create a new list item, inject into list
		var item = new dojox.mobile.ListItem({
			"class": "tweetviewListItem user-" + screenName
		}).placeAt(this.listNode, "first");

		// Update the list item's content using our template for tweets
		item.containerNode.innerHTML = this.substitute(this.tweetTemplateString, {
			text: this.formatTweet(tweet.text),
			user: tweet.from_user || screenName,
			name: tweet.from_user || tweet.user.name,
			avatar: tweet.profile_image_url || tweet.user.profile_image_url,
			time: this.formatTime(tweet.created_at),
			created_at: tweet.created_at,
			id: tweet.id
		});
	},this);

	// Show the list now that we have content for it
	this.showListNode(true);
},

Notice the user-{screenName} CSS class added to each item. That CSS class will play a crucial role in enabling and disabling users when we create the Settings view in the next tutorial in this series.

For every tweet we receive, we create a new list item. With the new list item created, we fill its content with the tweet-templated string generated from the tweet template and an object containing tweet data. Each tweet is added to the top of the list so that newest is first.

Tweets and Mentions Functionality Complete!

That wraps up the JavaScript functionality for TweetView's TweetView class! The Tweets and Mentions views will now load tweets and refresh! Let's review how we did it:

  • Created _ViewMixin which will be used by all TweetView widgets
  • Created a TweetView class in the tweetview namespace
  • Updated the Tweets and Mentions HTML to use the new TweetView widget
  • Set an ACCOUNTS object in the tweetview namespace so they are available to all widgets
  • Created methods within TweetView to set the widget into motion, retrieve tweets from Twitter, handle the result, and format them into the page

Now that the functionality is complete, go back and review the startup method again. With all of its utility methods in place, it should be easy to understand what's happening throughout the entire widget lifecycle.

There's just one step left: styling the list items.

Styling TweetView And Child Widgets

The styling used to position and format nodes within the list items is the fun part, and probably the easiest. At present, the ListItem widget is styled for one-line text, so we'll need to address that within our TweetView.css stylesheet:

.tweetviewHidden { /* we'll use this to hide nodes within TweetView */
	display:none;
}

.tweetviewList li { /* min-height to allow size of user avatar */
	min-height:55px;
	padding-top:8px;
}

.tweetviewListItem { /* special formatting to allow 2 lines of content */
	line-height:18px;
	height:auto;
}

With the list item ready to receive multiple lines of content, we can leverage the CSS classes created within the tweet item template:

/* provide proper spacing at the bottom of the scrollable views */
.mblScrollableViewContainer {
	padding-bottom:40px !important;
}

/* user's avatar */
.tweetviewAvatar {
	float:left;
}
/* content wrapper for tweets */
.tweetviewContent {
	padding-left:60px;
}

/* username formatting */
.tweetviewUser {
	font-size:80%;
}

/* actual tweet text formatting */
.tweetviewText {
	font-size:70%;
	font-weight:normal;
	padding-right:50px;
	padding-bottom:10px;
}

/* tweet time */
.tweetviewTime {
	float:right;
	color:#777;
	font-size:65%;
	font-weight:normal;
	padding-right:10px;
}

/* clears floats at the end of the list item */
.tweetviewClear {
	clear:both;
}

And there you have it: a perfectly styled list item matching the mockup!

View Demo

The Hard Part is Over!

As promised at the beginning of this tutorial, the Tweets and Mentions views are the most difficult parts of the project. The next part of the tutorial series will focus on the Settings view which will allow us to enable and disable accounts. Until then, review the demo and code in this tutorial to make sure you have a firm grasp on all of the moving parts!

Download The Source

Download Starting TweetView. The archive will work as soon as you unzip the file!