Introduction to Backbone.JS with WordPress

Brian Hogg / @brianhogg

Agenda

  • Why Backbone.js
  • Basics of Backbone.js / Underscore.js
  • End-to-end example plugin (Github)

Who are you?

Why Backbone?

  • Enforces some structure on your JavaScript
  • Events system

Why Not Just jQuery?

  • Performance
  • Leveraging the community
  • Re-inventing the wheel
  • Code structure (avoid 1000+ lines of jQuery that "just works")

What is Backbone.JS?

Structure (MV*)

Uses jQuery, but only hard requirement is Underscore.js

What is Underscore.JS?

Utility functions with _

  • _.each
  • _.template
  • Lots more: http://documentcloud.github.io/underscore/

Templates


var template = _.template("hello <%= name %>");
var html = template({ name: 'Brian' });
console.log( html ); // "hello Brian"

var template = _.template("<%- value %>");
var html = template({ value: '<script>' });
console.log( html ); // "&lt;script&gt;"
					

Alternatives

Ember.js, Angular.js, ...

Multiple ways of doing similar things. Even in Backbone.JS:

“It's common for folks just getting started to treat the examples listed on this page as some sort of gospel truth. In fact, Backbone.js is intended to be fairly agnostic about many common patterns in client-side code.”
http://backbonejs.org/#FAQ-tim-toady

Backbone / Underscore

Included in WordPress since 3.5

The "backbone" of the media manager, revisions UI

Models

Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control. You extend Backbone.Model with your domain-specific methods, and Model provides a basic set of functionality for managing changes.”

Model Example


var Post = Backbone.Model.extend({
    defaults: {
        title: "",
        post_status: "draft"
    },
	
    initialize: function() {
        console.log("creating a post");
    }
});

var post = new Post({ title: "Hello, world", post_status: "draft" });

var title = post.get("title"); // Hello, world
var post_status = post.get("post_status"); // draft
					

All models have an id attribute for syncing up with a server

Listening for Changes


post.on("change:title", function(model) {
    alert("Title changed to: " + model.get("title"));
});
					

Or in the models initialize with:

this.on("change:title", this.titleChanged);

Views

  • Used to turn a model into something you can see
  • Always contains a DOM element (the el property), whether its been added to the viewable page or not

Bare Minimum to use Backbone


var PostView = Backbone.View.extend({
    events: {
        "click .edit": "editPost",
        "change .post_status": "statusChanged"
    },

    editPost: function(event) {
        // ...
    },
	
    statusChanged: function(event) {
        // ...
    }
});

var postView = new PostView({ el: '#my-form' });
					

View Example


var PostView = Backbone.View.extend({
    tagName: "div", // div by default
    className: "bbpost", // for styling via CSS
	
    events: {
        "click .edit": "editPost",
        "change .post_status": "statusChanged"
    },

    initialize: {
        this.listenTo(this.model, "change", this.render);
    },

    render: {
        // ...
    }
});
					

Rendering the View


var template = _.template($("#tmpl-bbpost").html());
var html = template(this.model.toJSON());
this.$el.html(html);
return this; // for chaining
					

This uses Underscore.js' _.template, but you can use another!

Collections

Ordered set of models


var Posts = Backbone.Collection.extend({
    model: Post
});

var post1 = new Post({ title: "Hello, world" });
var post2 = new Post({ title: "Sample page" });

var myPosts = new Posts([ post1, post2 ]);
					

What Backbone expects when fetching/reading the collection:


[
    {
        id: 1,
        title: "Hello, world"
    },
    {
        ...
    }
]
					

What this sends:

wp_send_json_success( array( 'id': 1, 'title': 'Hello, world' ) );

{
    success: true,
    data: [
        {
            id: 1,
            title: "Hello, world"
        }
    ]
}
					

Override .parse() to accommodate:


var Posts = Backbone.Collection.extend({
    model: Post,
    url: ajaxurl, // defined for us if we're in /wp-admin
    parse: function( response ) {
        return response.data;
    }
});

// Kick things off
$(document).ready(function() {
    posts = new Posts();
    postsView = new PostsView({ collection: posts });
    posts.fetch({ data: { action: 'bbpost_fetch_posts' } });
});
					

Or can override .sync(), or even .fetch()

Note on calling .fetch() on page load:

“Note that fetch should not be used to populate collections on page load — all models needed at load time should already be bootstrapped in to place. fetch is intended for lazily-loading models for interfaces that are not needed immediately: for example, documents with collections of notes that may be toggled open and closed.”
http://backbonejs.org/#Collection-fetch

Depends on the situation

Routers

Used for routing your application's URLs when using hash tags (#)

(Contrived) Example

Managing WordPress post titles and publish/draft status in an admin panel

DEMO

Directory Structure


plugins/
    backbone-js-wp-example/
        backbone-js-wp-example.php
        css/
            admin.css
        js/
            collections/
                posts.js
            models/
                post.js
            views/
                post.js
                posts.js
                    

models/post.js


var bbp = bbp || {};

(function($){
    bbp.Post = Backbone.Model.extend({
    });
})(jQuery);
                    

Could set defaults here, if creating new posts

backbone-js-wp-example.php

Setting up actions


class BBPostAdmin {
    public function __construct() {
        if ( is_admin() ) {
            add_action( 'wp_ajax_bbpost_fetch_posts', array( &$this, 'ajax_fetch_posts' ) );
            add_action( 'wp_ajax_bbpost_save_post', array( &$this, 'ajax_save_post' ) );

            if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
                add_action( 'admin_menu', array( &$this, 'admin_menu' ) );
                if ( isset( $_GET['page'] ) and 'bbpostadmin' == $_GET['page'] ) {
                    add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_scripts' ) );
                }
            }
        }
    }
                        

backbone-js-wp-example.php

Adding the scripts


// Add backbone.js models first, then collections, followed by views
$folders = array( 'models', 'collections', 'views' );
foreach ( $folders as $folder ) {
    foreach ( glob( dirname( __FILE__ ) . "/js/$folder/*.js" ) as $filename ) {
        $basename = basename( $filename );
        wp_register_script( "$folder/$basename", plugins_url( "js/$folder/$basename", __FILE__ ), array( 'jquery', 'backbone', 'underscore', 'wp-util' ), BBPOST_VERSION );
        wp_enqueue_script( "$folder/$basename" );
    }
}

wp_register_style( 'bbpost.admin.css', plugins_url( 'css/admin.css', __FILE__ ), false, ECN_VERSION );
wp_enqueue_style( 'bbpost.admin.css' );
                    

Or use something like Grunt to concat into one js file

Admin page template




Backbone.js WordPress Post Admin Example

Admin page template


<div class="bbpost">
    
    <h2><%- title %></h2>
    Post title: <input type="text" class="title" value="<%- title %>" />, 
    Status: 
    <select class="post_status">
        <option value=""></option>
        <option value="publish" <% if ( 'publish' == post_status ) { %>SELECTED<% } %>>Published</option>
        <option value="draft" <% if ( 'draft' == post_status ) { %>SELECTED<% } %>>Draft</option>
    </select>
    <button>Update</button>
</div>
					

views/posts.js


var bbp = bbp || {};

(function($){
    bbp.PostsView = Backbone.View.extend({
        el: '#bbposts', // Specifying an already existing element

        initialize: function() {
            this.collection.bind('add', this.addOne, this);
        },

        addOne: function(post) {
            var view = new bbp.PostView({ model: post });
            this.$el.append(view.render().el);
        }
    });

    $(document).ready(function() {
        bbp.posts = new bbp.PostsCollection();
        bbp.postsView = new bbp.PostsView({ collection: bbp.posts });
        bbp.posts.fetch({ data: { action: 'bbpost_fetch_posts' } });
    });
})(jQuery);
                    

views/post.js


var bbp = bbp || {};

(function($){
    bbp.PostView = Backbone.View.extend({
        className: 'bbpost',

        initialize: function() {
            this.model.on("change", this.render, this);
        },

        render: function() {
            var template = _.template($('#tmpl-bbpost').html());
            var html = template(this.model.toJSON());
            this.$el.html(html);
            return this;
        },

        events: {
            'click button': 'updatePost'
        },

        updatePost: function() {
            this.model.set('title', this.$('.title').val());
            this.model.set('post_status', this.$('.post_status').val());
            this.model.save();
        }
    });
})(jQuery);
					

backbone-js-wp-example.php

Function to send the post data


if ( ! current_user_can( 'edit_published_posts' ) )
    wp_send_json_error();

$posts = get_posts( 
    array(
        'post_status' => 'any'
    )
);
$retval = array();
foreach ( $posts as $post ) {
    $retval[] = array(
        'id' => $post->ID,
        'title' => $post->post_title,
        'post_status' => $post->post_status,
    );
}

wp_send_json_success( $retval );
				                        

collections/posts.js


var bbp = bbp || {};

(function($){
    bbp.PostsCollection = Backbone.Collection.extend({
        model: bbp.Post,
        url: ajaxurl,
        parse: function ( response ) {
            // This will be undefined if success: false
            return response.data;
		}
    });
})(jQuery);
								

Saving

Override save() in models/post.js


var bbp = bbp || {};

(function($){
    bbp.Post = Backbone.Model.extend({
        save: function( attributes, options ) {
            options = options || {};
            options.data = _.extend( options.data || {}, {
                action: 'bbpost_save_post',
                data: this.toJSON()
            });
            var deferred = wp.ajax.send( options );
            deferred.done( function() {
                alert('done');
            });
            deferred.fail( function() {
                alert('failed');
            });
        }
    });
})(jQuery);
                    

backbone-js-wp-example.php

Saving a post title/status


if ( ! $post = get_post( (int) $_POST['data']['id'] ) )
    wp_send_json_error();

if ( ! current_user_can( 'edit_post', $post->ID ) )
    wp_send_json_error();

if ( wp_update_post( array(
        'ID' => $post->ID,
        'post_title' => $_POST['data']['title'],
        'post_status' => $_POST['data']['post_status'],
    ) ) == $post->ID )
    wp_send_json_success();
else
    wp_send_json_error();
                   

Extra work to set up initially, but worth it later on!

wp-backbone

Special versions of Backbone.View (wp.Backbone.View)


revisions.view.Frame = wp.Backbone.View.extend({
    className: 'revisions',
    template: wp.template('revisions-frame'),
    // ...
});
					
  • Handling of SubViews
  • templates use <# #> instead of <% %> (as PHP can see <% %> as code: see trac for details)
  • See Mark Jaquith's talk at WordCamp SF 2014 on wordpress.tv

WP JSON REST API

Resources

https://github.com/brianhogg/backbone-js-wp-example

http://backbonejs.org/

http://backbonetutorials.com/

https://github.com/addyosmani/backbone-fundamentals

http://kadamwhite.github.io/talks/2014/backbone-wordpress-wpsessions

http://wordpress.tv/2014/11/03/mark-jaquith-backbone-views-in-wordpress/

WordPress revisions.js

Enjoy!

brianhogg.com | @brianhogg