Event Management
with Backbone.js

Jennifer Dodd @jmdodd
Code Wrangler, Automattic

What is Backbone.js?

  • JavaScript library
  • lightweight framework
  • RESTful JSON interface

Why use Backbone.js?

  • Structure: MV* (model-view-whatever) pattern
  • Tools: Underscore.js utility library
  • Communications: flexible JSON

Structure: MV*

Organization

var ToDo = ToDo || {};

ToDo.Models = ToDo.Models || {};

ToDo.Views = ToDo.Views || {};

ToDo.Collections = ToDo.Collections || {};

Trash Recycling with Disposal Containers by epSos.de, on Flickr

Models

  • information container
  • avoids jQuery.data()
  • tracks own state

Model A by Barbara Eckstein, on Flickr

ToDo task model:

ToDo.Models.ToDo = ( function( $, Backbone ) {
	return Backbone.Model.extend( {
	 	defaults: function() {
			return {
				id: 0,
				title: '',
				checked: '',
				completedBy: false,
				latestChange: 0,
				source: 'local'
	 	 	};
	 	}
	} );
} )( jQuery, Backbone );

On the server:

foreach ( (array) $posts as $post ) {
	$todo = array(
		'id'	       => $post->ID,
		'title'	       => $post->post_title,
		'checked'      => $checked,
		'completedBy'  => $userdata,
		'latestChange' => $at,
	);
	$todos[] = $todo;
}
wp_send_json_success( $todos );

Views

  • in the DOM: i.e. what you see in the browser
  • provide the user action interface: i.e. users can do stuff
  • can be attached to models and/or collections
  • can contain subviews
  • use templates

Beach at Grand Mirage - BALI by Donald Man, on Flickr

In the template:


{{ if ( completedBy ) { }}
	
{{ } }}
{{= title }}
{{ if ( completedBy ) { }}
	
	
{{= completedBy.username }} {{ } }}

ToDo task view:

ToDo.Views.ToDo = ( function( $, Backbone ) {
	return Backbone.View.extend( {
	 	model: ToDo.Models.ToDo,
	 	tagName: 'li',
 	 	template: _.template( $( '#todos-template' ).html() ),

	 	initialize: function() {
	  	 	// Start by rendering the View
	  	 	this.render();
	  	},

	 	render: function() {
	 		this.$el.html( this.template( this.model.toJSON() ) );
	 		return this;
	  	}
	} );
} )( jQuery, Backbone );

Collections

  • organization of data
  • sortable
  • can be a collection of models or views

star wars vintage figure collection (closer) by Simon Q, on Flickr

ToDo task collection:

ToDo.Collections.ToDos = ( function( $, Backbone ) {
	return Backbone.Collection.extend( {
	 	model: ToDo.Models.ToDo
	} );
} )( jQuery, Backbone );

Events

Events

  • the reason we use Backbone
  • all interactions are built around using events on models, views, and collections

Inside Canada Hockey Place by s.yume, on Flickr

Some common events are:

  • add when a model is added to a collection
  • remove when a model is removed from a collection
  • reset when a collection's contents are replaced
  • change when a model's attributes have changed
  • change:[attribute] when a single attribute of a model has changed

What does an event look like?

  • generally speaking, an event is something you are doing with a model or collection
  • a function (like model.set, collection.reset) triggers an event
  • to avoid triggering an event, pass { silent: true } as an option
  • it is better to pass a flag in options to avoid that event

Events are what make Backbone powerful

  • instead of using .bind() and .on(), we try to use .listenTo()
  • avoid JS memory leaks

Types of Events

Models and Views and Collections

  • loosely linked
  • events on Views (click, DOM, etc.)
  • communications between them use .listenTo() and .trigger()

Actions on a View can update a Model

ToDo.Views.ToDo = ( function( $, Backbone ) {
...
	events: {
		'click .on-completion': 'onCompletion'
	},

	onCompletion: function( e ) {
	 	if ( e.currentTarget.checked ) {
	 	 	this.model.set( {
	 	 	 	checked: 'checked',
	 	 	 	completedBy: ToDo.currentUser,
	 	 	 	source: 'local'
	 	 	} );
	 	} else {
	 	 	this.model.set( {
	 	 	 	checked: '',
	 	 	 	completedBy: false,
	 	 	 	source: 'local'
	 	 	} );
	 	}
	}

Updates to a Model can rerender a View

ToDo.Views.ToDo = ( function( $, Backbone ) {
...
	model: ToDo.Models.ToDo,

	initialize: function() {
	 	// Update the View when the model changes
	 	this.listenTo( this.model, 'change', this.render );
	},

Updates to a Model send out the data

ToDo.Models.ToDo = ( function( $, Backbone ) {

...
	initialize: function() {
		this.listenTo( this, 'change', this.update );
	},
	update: function() {
	 	if ( this.get( 'source' ) === 'local' ) {
	 	 	var data = {
	 	 	 	action: 'todos_check',
	 	 	 	id: this.id,
	 	 	 	checked: this.get( 'checked' )
	 	 	};

and get a response from the server

var jqXHR = $.ajax( {
	dataType: 'json',
	url: ToDo.ajaxurl,
	xhrFields: {
	 	withCredentials: true
	},
	data: data
} )
.done( function( response, textStatus, jqXHR ) {
	var todo = response.data[0];
	todo.source = 'check';
	ToDo.toDos.add( response.data[0], { merge: true } );
} );

Adding a Model to a Collection can add a View

ToDo.Views.Widget = ( function( $, Backbone ) {
...
	initialize: function() {
	 	this.listenTo( this.collection, 'add', this.addOne );
	},

	addOne: function( todo ) {
	 	var view = new ToDo.Views.ToDo( {
	 	 	model: todo
	 	} );
	 	this.$el.prepend( view.el );
	 	return this;
	},

What about other people's actions?

  • server-push...

What about other people's actions?

  • polling...

Polling in the browser sends an AJAX request,

poll: function() {
	var data = {
	 	action: 'todos_poll',
	 	since: ToDo.since
	};

	ToDo.current = Date.now();

	var jqXHR = $.ajax( {
	 	dataType: 'json',
	 	url: ToDo.ajaxurl,
	 	data: data
	} )

receives back an encoded JSON response,

	.done( function( response, textStatus, jqXHR ) {
	 	if ( 'undefined' != typeof response.data ) {
	 	 	ToDo.since = ToDo.current;
	 	 	for ( m = 0, dl = response.data.length; m < dl; m++ ) {
	 	 	 	var todo = response.data[m];
				todo.source = 'poll';
	 	 	 	ToDo.toDos.add( todo, { merge: true } );
	 	 	}
	 	}
	} )

and then sets the next AJAX poll event

	.always( function() {
	 	ToDo.Polling.poller = setTimeout( ToDo.Polling.poll, ToDo.Polling.pollInterval ); 
	} );

Polling on the server is a wp_ajax_ action

add_action( 'wp_ajax_todos_poll', array( 'ToDos_Widget', 'poll' ) );

public static function poll() {
	$todos = array();
	if ( isset( $_GET['since'] ) ) {
	 	$since = absint( substr( $_GET['since'], 0, 10 ) ) - 3;
	 	$min = time() - 24 * 60 * 60;
	 	if ( $since < $min ) {
	 	 	$since = $min;
	 	}
	 	$todos = self::get_todos_since( $since );
	}

	wp_send_json_success( $todos );
}

Getting a new Model from the server:

  • adds the model to the collection
  • adds a view for the new model

Getting an updated Model from the server

  • updates the model in the collection
  • updates the view on that model

Demo

Custom Events within Backbone

ToDo.dispatcher= _.clone( Backbone.Events );

ToDo.dispatcher.trigger( 'toDo:server-update', { data: response.data } );
// object.trigger(event, [*args])

this.listenTo( ToDo.dispatcher, 'toDo:server-update', this.serverUpdated );
// object.listenTo(other, event, callback)

Why use custom events?

  • available to models, views, and collections
  • creates a centralized event bus
  • allows loosely coupled communications
  • uses pseudo-namespacing

Talking to jQuery (and the DOM)

ToDo.appContainer = $( "#content" );

ToDo.appContainer.trigger( 'toDo:data-received', data );

ToDo.appContainer.on( 'toDo:data-received', function( data ) {
	// Do something with data
} );

Why talk to jQuery?

  • fires a jQuery-catchable event on a DOM element
  • use when adding/removing elements from the DOM
  • increase ability of other jQuery plugins to extend your code

Make it more familiar


ToDo.actions = [];
ToDo.currentActions = [];
ToDo.doneActions = [];
ToDo.doAction = function( hook ) {
	if ( 'string' == typeof hook ) {
		var args = Array.prototype.slice.call( arguments, 1 );
		ToDo.currentActions.push( hook );
		ToDo.appContainer.trigger( hook, args );
		ToDo.doneActions.push( hook );
		ToDo.currentActions.pop();
	}
};

Add some helper functions

ToDo.currentAction = function() {
	l = this.currentActions.length;
	if ( l > 0 ) {
		return this.currentActions[ l - 1 ];
	}
	return false;
}
ToDo.didAction = function( hook ) {
	if ( -1 === $.inArray( hook, this.doneActions ) ) {
		return false;
	}
	return true;
}

Then we can do this in JavaScript

ToDo.doAction( 'toDo:alertOn', data );

ToDo.appContainer.on( 'toDo:alertOnce', function() {
	if ( ! ToDo.didAction( 'toDo:alertOnce' ) {
	 	// Do something once
	}
} );

Communication Issues

  • Crosstalk: collisions on the server
  • PEBKAC: collisions on the browser
  • Chaos: events hit the server/browser out of order

Communication Solutions

  • mark models as coming from the client or server
  • use server-side checking to avoid overwrites
  • use client-side debouncing to avoid value-flipping
  • queue transactions and only perform the last one in a series of same actions
  • serialize based on browser-time to avoid chaos

Questions?

Slides: http://jmdodd.github.io/talks/2014/YUL/
ToDos: http://bit.ly/1l4JHsA

(We're hiring!)