DailyJS

DailyJS

The JavaScript blog.


Tagbackgoog
Featured

jquery node mvc backbone.js bootstrap backgoog

Backbone.js Tutorial: jQuery Plugins and Moving Tasks

Posted on .

Preparation

Before starting this tutorial, you'll need the following:

  • alexyoung / dailyjs-backbone-tutorial at commit 705bcb4
  • The API key from part 2
  • The "Client ID" key from part 2
  • Update app/js/config.js with your keys (if you've checked out my source)

To check out the source, run the following commands (or use a suitable Git GUI tool):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git  
cd dailyjs-backbone-tutorial  
git reset --hard 705bcb4  

Using Backbone with jQuery Plugins

Although Backbone doesn't need to be used with jQuery specifically, a lot of people use it with jQuery (and RequireJS) to get access to the diverse plugins made by the jQuery community. In this tutorial I'll explain how to use jQuery plugins with Backbone projects, and how to find ones that will work well.

The example I've used is integrating a drag-and-drop "sortable" plugin to allow tasks to be reordered.

HTML5 Sortable

The plugin I've used for drag-and-drop is the HTML5 Sortable Plugin by Ali Farhadi. The reason I used this particular plugin is it has a simple event-based API that allows the plugin to be unloaded and sort events to be captured and responded to. It just needs a container element and the child elements that need to be sorted. The unordered list of tasks in this project directly translates to the expected markup.

Sometimes it's easier to just write out data attributes to elements rather than trying to create relationships between the DOM nodes used by plugins and models. HTML5 Sortable emits a 'sortupdate' event when a node has been dragged and dropped, and it'll pass the relevant element to the listener callback. From this we need to figure out which model has changed, then translate that into something Google's API can understand.

Loading Plugins with RequireJS

In an earlier tutorial I demonstrated how to load non-AMD libraries using RequireJS. If you want a recap, just check out app/js/main.js and look at the shim property in the RequireJS configuration:

requirejs.config({  
  baseUrl: 'js',

  paths: {
    text: 'lib/text'
  },

  shim: {
    'lib/underscore-min': {
      exports: '_'
    },
    'lib/backbone': {
      deps: ['lib/underscore-min']
    , exports: 'Backbone'
    },
    'app': {
      deps: ['lib/underscore-min', 'lib/backbone', 'lib/jquery.sortable']
    }
  }
});

The 'app' property expresses a dependency between the main Backbone application file and lib/jquery.sortable, which means /lib/jquery.sortable.js will get automatically loaded (or compiled in by r.js when creating a production build of the app).

Google Tasks Ordering API

It would be too easy if HTML5 Sortable's API was a one-to-one match with the Google Task's ordering API. Google's API has a specific method for moving tasks, and it's based around moving one task to occupy the position of another one:

gapi.client.tasks.tasks.move({ tasklist: listId, task: id, previous: previousId });  

Moving a task to the top of the list is handled by passing null for previous.

Next I'll explain how to create some simple interface elements for the draggable handle, and then we'll look at how to persist moved tasks by translating Google's API into Backbone model and collection code.

Implementation: Views and Templates

I added a little handle by using a Bootstrap icon and an anchor element in app/js/templates/tasks/task.html:

<a href="#" class="handle pull-right"><i class="icon-move"></i></a>  

Next I added the code that maps between the Backbone view and the jQuery HTML5 Sortable plugin to app/js/views/tasks/index.js:

makeSortable: function() {  
  var $el = this.$el.find('#task-list');
  if (this.collection.length) {
    $el.sortable('destroy');
    $el.sortable({ handle: '.handle' }).bind('sortupdate', _.bind(this.saveTaskOrder, this));
  }
},

saveTaskOrder: function(e, o) {  
  var id = $(o.item).find('.check-task').data('taskId')
    , previous = $(o.item).prev()
    , previousId = previous.length ? $(previous).find('.check-task').data('taskId') : null
    , request
    ;

  this.collection.move(id, previousId, this.model);
},

The makeSortable method makes an element that appears within TasksIndexView "sortable" -- that is, HTML Sortable has been wrapped around it. The plugin's 'sortupdate' method is then bound to saveTaskOrder.

The saveTaskOrder method gets the current task's ID by looking at the checkbox, because I'd already added a data attribute to that element in the template. This ID is then passed to the collection with the previous task's ID. In this case, the previous task is the one adjacent to it, which Google's API needs to figure out how to move the task.

The collection property in this view is a Tasks property, so let's take a look at how to implement the move method that causes the changes to be persisted.

Implementation: Models and Collections

Open app/js/collections/tasks.js and add a new method called move:

move: function(id, previousId, list) {  
  var model = this.get(id)
    , toModel = this.get(previousId)
    , index = this.indexOf(toModel) + 1
    ;

  this.remove(model, { silent: true });
  this.add(model, { at: index, silent: true });

  // Persist the change
  list.moveTask({ task: id, previous: previousId });
}

This method just exists to trigger remove and add calls on the collection so cause the objects to be reshuffled internally. It then calls moveTask on the TaskList model (in app/js/models/tasklist.js):

moveTask: function(options) {  
  options['tasklist'] = this.get('id');
  var request = gapi.client.tasks.tasks.move(options);

  Backbone.gapiRequest(request, 'update', this, options);
}

The gapiRequest method forms the basis for the custom Backbone.sync method used in this project, which I've talked about in previous tutorials. I wasn't able to figure out how to make Backbone.sync cope with moving items in a way that made sense given how gapi.client.tasks.tasks.move works, but I was able to at least reuse some of the syncing functionality by creating a request and calling the "standard" request handler.

Summary

When you can't find a suitable Backbone plugin for something and want to use a jQuery plugin, my advice is to look for plugins that have event-based APIs and can be cleanly unloaded. That will make them easy to hook into your Backbone views.

The full source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit e9edfa3.

Featured

node mvc backbone.js bootstrap backgoog

Backbone.js Tutorial: Updates for 1.0, Clear Complete

Posted on .

Preparation

Before starting this tutorial, you'll need the following:

  • alexyoung / dailyjs-backbone-tutorial at commit 711c9f6
  • The API key from part 2
  • The "Client ID" key from part 2
  • Update app/js/config.js with your keys (if you've checked out my source)

To check out the source, run the following commands (or use a suitable Git GUI tool):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git  
cd dailyjs-backbone-tutorial  
git reset --hard 711c9f6  

Updating to Backbone 1.0

I updated bTask to work with Backbone 1.0, which required two small changes. The first was a change to the behaviour of callbacks in Backbone.sync -- the internal call to the success callback now only needs one argument, which is the response data. I think I've mentioned that on DailyJS before, but you shouldn't need to worry about this in your own Backbone projects unless you've written a custom Backbone.sync implementation.

The second change was the collection add events were firing when the views called fetch. I fixed this by passing reset: true to the fetch options. Details on this have been included in Backbone's documentation under "Upgrading to 1.0":

If you want to smartly update the contents of a Collection, adding new models, removing missing ones, and merging those already present, you now call set (previously named "update"), a similar operation to calling set on a Model. This is now the default when you call fetch on a collection. To get the old behavior, pass {reset: true}.

Adding "Clear Complete"

When a task in Google Tasks is marked as done, it will appear with strike-through and hang around in the list until it is cleared or deleted. Most Google Tasks clients will have a button that says "Clear Complete", so I added one to bTask.

I added a method called clear to the Tasks collection which calls the .clear method from the Google Tasks API (rather than going through Backbone.sync):

define(['models/task'], function(Task) {  
  var Tasks = Backbone.Collection.extend({
    model: Task,
    url: 'tasks',

    clear: function(tasklist, options) {
      var success = options.success || function() {}
        , request
        , self = this
        ;

      options.success = function() {
        self.remove(self.filter(function(task) {
          return task.get('status') === 'completed';
        }));

        success();
      };

      request = gapi.client.tasks.tasks.clear({ tasklist: tasklist });
      Backbone.gapiRequest(request, 'update', this, options);
    }
  });

  return Tasks;
});

I also added a button (using Bootstrap's built-in icons) to app/js/templates/app.html, and added an event to AppView (in app/js/views/app.js):

var AppView = Backbone.View.extend({  
  // ...
  events: {
    'click .clear-complete': 'clearComplete'
  },

  // ...
  clearComplete: function() {
    var list = bTask.views.activeListMenuItem.model;
    bTask.collections.tasks.clear(list.get('id'), { success: function() {
      // Show some kind of user feedback
    }});
    return false;
  }
});

I had to change app/js/views/lists/menuitem.js to set the current collection in the open method to make this work.

Summary

Because I've been reviewing Backbone's evolution as it progressed to 1.0 for DailyJS, updating this project wasn't too much effort. In general the 1.0 release is backwards compatible, so you should definitely consider upgrading your own projects. Also, now bTask has 'Clear Complete', I feel like it does enough of the standard Google Tasks features for me to actually use it regularly.

Remember that you can try it out for yourself at todo.dailyjs.com.

The full source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit 705bcb4.

Featured

node mvc backbone.js bootstrap backgoog

Backbone.js Tutorial: Customising the UI

Posted on .

Preparation

Before starting this tutorial, you'll need the following:

  • alexyoung / dailyjs-backbone-tutorial at commit 85c358
  • The API key from part 2
  • The "Client ID" key from part 2
  • Update app/js/config.js with your keys (if you've checked out my source)

To check out the source, run the following commands (or use a suitable Git GUI tool):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git  
cd dailyjs-backbone-tutorial  
git reset --hard 85c358  

Customising Bootstrap

Before customisation.

So far our Backbone application has had a rudimentary interface. It's based on Bootstrap, which is a popular choice for developers who want a usable interface without spending too much time working on the design side of things. However, Bootstrap is a victim of its own popularity, and most of us are starting to grow tired of seeing it everywhere.

This post is about techniques for customising projects built with Backbone and Bootstrap. There are three main approaches I use to add some originality to my Bootstrap projects:

  1. Colours: Get the project configured with suitable branding
  2. Texture: Judicious use of images to add an extra dimension to background, panels, and buttons
  3. Custom fonts and icons: Carefully applied custom fonts and icons can create a more distinct look

Colours

Bootstrap has a customisation page that allows you to change typographic elements and colours. This is self-explanatory so I'm not going to spend too much time on it. Bootstrap is built on LESS CSS so it's easy to create your own builds of Bootstrap with custom colours baked right in.

Texture

Subtle Patterns.

To save time when working on client projects, I'll often dig through stock photography sites to find useful illustrations and textures. This project, however, just needs something to add a texture to the left-hand-side navigation to make it look visually distinct. An excellent resource for textures is Subtle Patterns -- a curated directory of tiled textures suitable for web and mobile projects. For legal information for use in commercial projects, see About Subtle Patterns.

I've added two tiled images to the project: one for the navigation bar and another for the body background. The image used on the body is white, while the navigation bar is dark grey. The navigation list elements use alpha blending to make the underlying texture appear lighter, with rgba(255, 255, 255, .25).

It's easy to add textures to a project, particularly when they tile, and the Subtle Patterns images include a higher-DPI version as well. I find this a fun second stage to Bootstrap customisation, because it's easy to quickly swap textures around to experiment.

Custom Fonts and Icons

Using Google Web Fonts.

The first thing I wanted to change was the logo font. Rather than using an image I found a suitable font on Google Web Fonts and applied some CSS shadows. This font was added to the project by updating index.html to load the font and updating the CSS to refer to it by name:

<link href='http://fonts.googleapis.com/css?family=Playball' rel='stylesheet' type='text/css'>  

Then in the CSS the font is selected with font-family: playball, sans-serif.

I also switched the main body font to PT Sans, which doesn't look radically different to the default but again elevates the look and feel away from stock-Bootstrap.

Another quick win is to use Font Awesome.

Go Forth and Customise

The finished article.

Adding custom fonts, textures, and icons are just a few easy ways to distinguish a Bootstrap-based project from the crowd. You've got no excuse for releasing boring-looking apps!

I'm running a build of bTask at todo.dailyjs.com. It's not exactly the same as the tutorial version at the moment because I wrote it while researching this tutorial series, but eventually I'll switch it over to use the same code as the GitHub project. It doesn't implement everything Google Tasks supports (like subtasks for example), but I use it every day at work.

The full source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit 711c9f6.

Featured

node mvc backbone.js backgoog

Backbone.js Tutorial: Routes

Posted on .

Preparation

Before starting this tutorial, you'll need the following:

  • alexyoung / dailyjs-backbone-tutorial at commit 0c6de32
  • The API key from part 2
  • The "Client ID" key from part 2
  • Update app/js/config.js with your keys (if you've checked out my source)

To check out the source, run the following commands (or use a suitable Git GUI tool):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git  
cd dailyjs-backbone-tutorial  
git reset --hard 0c6de32  

Routes

So far we've implemented basic list and task management, but working with multiple lists is tricky because lists can't be referenced by the URL. If the page is reloaded, the current list isn't remembered, and lists can't be bookmarked.

Fortunately, Backbone provides a solution for both of these issues: Backbone.Router. This provides a neat wrapper around hash URLs and history.pushState.

When to Use Hash URLs

I'll admit I find hash URLs annoying, and this sentiment seems to have been perpetuated by Twitter's implementation of them. However, there is a good side to hash URLs: they require less work to build and are backwards compatible with older browsers.

Using history.pushState means the browser can potentially display any URL you want. Rather than /#lists/id, the prettier /lists/id can be displayed. However, without a suitable server-side setup, visiting /lists/id before the main application has loaded will fail while the hash URL version will work.

If you're making a fairly simple and self-contained single page application, then you may wish to avoid pushState and use hash URLs instead.

Either way, Backbone makes it easy to switch between both schemes. Hash URLs are the default, and history.pushState will be used when specified with Backbone.history.start({ pushState: true }).

The Routes File

It's generally a good idea to keep routes separate from the rest of the application. Create a new file called app/js/routes.js and extend Backbone's router:

define(function() {  
  return Backbone.Router.extend({
    routes: {
      'lists/:id': 'openList'
    },

    initialize: function() {
    },

    openList: function(id) {
    }
  });
});

This code defines the route. This application will just need one for now: lists/:id. The :id part is a parameter, which will be extracted by Backbone.Router and sent as an argument to openList.

Load the Router

The centralised App class is as good a place as any to load the routes and set them up. Open app/js/app.js and change define to include 'routes':

define([  
  'gapi'
, 'routes'
, 'views/app'
, 'views/auth'
, 'views/lists/menu'
, 'collections/tasklists'
, 'collections/tasks'
],

function(ApiManager, Routes, AppView, AuthView, ListMenuView, TaskLists, Tasks) {  
  var App = function() {
    this.routes = new Routes();

Now, move down to around line 35 where there's a callback that runs when the API manager is ready. This is where Backbone.history.start should be called:

App.prototype = {  
  views: {},
  collections: {},
  connectGapi: function() {
    var self = this;
    this.apiManager = new ApiManager(this);
    this.apiManager.on('ready', function() {
      self.collections.lists.fetch({ data: { userId: '@me' }, success: function(collection, res, req) {
        self.views.listMenu.render();
        Backbone.history.start();
      }});
    });
  }
};

It's technically safe to call this when the routes have been loaded, but the openList route handler requires that some lists exist, so it's better to load it when the API is ready.

The purpose of the start method is to begin monitoring hashchange events -- whenever the browser address bar's URL changes the router will be invoked.

Opening Lists Using Events

To write decoupled Backbone applications, you need to think in terms of the full Backbone stack: models and collections, and views. When someone visits a list URL from a bookmark that refers to a specific model, the route handler should be able to find the associated model.

Backbone's documentation is quite clear about the power of custom events, and that's basically how openList in app/js/routes.js should work:

openList: function(id) {  
  if (bTask.collections.lists && bTask.collections.lists.length) {
    var list = bTask.collections.lists.get(id);
    if (list) {
      list.trigger('select');
    } else {
      console.error('List not found:', id);
    }
  }
}

I've been strict about checking for the existence of the lists collection, and even when fetching a given list model from the collection. The main reason for this was to be able to show sensible error messages, but for now there's just a console.error to help track issues loading data.

The final piece of the puzzle is the view code that has the responsibility of opening lists. Open app/js/views/lists/menuitem.js and make the following changes:

  1. Add this.model.on('select', this.open, this); to the initialize method
  2. Add bTask.routes.navigate('lists/' + this.model.get('id')); to the render method

The first line binds the custom event, select, from the view's model (which represents the list). The second line causes the browser's URL to be updated -- you'll find yourself using routes.navigate quite a lot in more complicated applications.

Summary

Combining events and routes is the secret to writing decoupled Backbone applications. It can be difficult to do this well -- there are definitely often lazy solutions that can result in a little bit too much spaghetti code. To avoid situations like this, think in terms of models, collections, views, and their relationships.

The full source for this tutorial can be found in alexyoung / dailyjs-backbone-tutorial, commit 85c358.

Featured

node backbone.js backgoog

Upgrading to Grunt 0.4

Posted on .

I was working on dailyjs-backbone-tutorial and I noticed issue #5 where "fiture" was unable to run the build script. That tutorial uses Grunt to invoke r.j from RequireJS, and it turned out I forgot to specify the version of Grunt in the project's package.json file, which meant newcomers were getting an incompatible version of Grunt.

I changed the project to first specify the version of Grunt, and then renamed the grunt file to Gruntfile.js, and it pretty much worked. You can see these changes in commit 0f98f7.

So, what's the big deal? Why is Grunt breaking projects and how can this be avoided in the future?

Global vs. Local

If you're a client-side developer, npm is probably just part of your toolkit and you don't really care about how it works. It gets things like Grunt for you so you can work more efficiently. However, us server-side developers like to obsess about things like dependency management, and to us it's important to be careful about specifying the version of a given module.

Previous versions of Grunt kind of broke this whole idea, because Grunt's documentation assumed you wanted to install Grunt "globally". I've never liked doing that, as I've experienced why this is bad first-hand with the Ruby side projects I've been involved with. What I've always preferred to do with Node is write a package.json for every project, and specify the version of each dependency. I either specify the exact version, or the minor version if the project uses semantic versioning.

For example, with Grunt I might write this:

 , "grunt": "0.3.x"

This causes the grunt command-line tool to appear in ./node_modules/.bin/grunt, which probably isn't in your $PATH. Therefore, when you're ready to build the project and you type grunt, the command won't be found.

Knowing this, I usually add node_modules/.bin/grunt as a "script" to package.json which allows grunt to be invoked through the npm command. This works in Unix and Windows, which was partly the reason I used Grunt instead of make anyway.

There were problems with this approach, however. Grunt comes with a load of built-in tasks, so when the developers updated one of these smaller sub-modules they had to release a whole new version of Grunt. This is dangerous when a module is installed globally -- what happens if an updated task has an API breaking change? Now all of your projects that use it need to be updated.

To fix this, the Grunt developers have pulled out the command-line part of Grunt from the base package, and they've also removed the tasks and released those as plugins. That means you can now write this:

 , "grunt": "0.4.x"

And install the command-line tool globally:

npm install -g grunt-cli  

Since grunt-cli is a very simple module it's safer to install it globally, while the part that we want to manage more carefully is locked down to a version range that shouldn't break our project.

Built-in Tasks: Gone

The built-in tasks have been removed in Grunt 0.4. I prefer this approach because Grunt was getting extremely large, so it seems natural to move them out into plugins. You'll need to add them back as devDependencies to your package.json file.

If you're having trouble finding the old plugins, they've been flagged on the Grunt plugin site with stars.

Uninstall Grunt

Before switching to the latest version of Grunt, be sure to uninstall the old one if you installed it globally.

Other Changes

There are other changes in 0.4 that you may run into that didn't affect my little Backbone project. Fortunately, the Grunt developers have written up a migration guide which explains everything in detail.

Also worth reading is Tearing Grunt Apart in which Tyler Kellen and Ben Alman explain why Grunt has been changed, and what to look forward to in 0.5.

Peer Dependencies

If you write Grunt plugins, then I recommend reading Peer Dependencies on the Node blog by Domenic Denicola. As a plugin author, you can now take advantage of the peerDependencies property in package.json for defining the version of Grunt that your plugin is compatible with.

Take a look at grunt-contrib-requirejs/package.json to see how this is used in practice. The authors have locked the plugin to Grunt 0.4.x.