Using Backbone.js

with drupal 8 and 7

Vadim Mirgorod aka @dealancer



ahoj!

VADIM MIRGOROD

Team Lead at Blink Reaction



http://blinkreaction.com

VADIM MIRGOROD

Author of Backbone.js Cookbook

VADIM MIRGOROD

Father

get in touch



@dealancer
dealancer@gmail.com



RICH UX AND DRUPAL?

here is how ajax works



Keep off choosing any file

and click Attach button.






keep calm!

AND GET SOME HTML


...in jSON

{"status":true,"data":"\u003cdiv class=\"form-item\" id=\"edit-upload-wrapper\"\u003e\n \u003clabel for=\"edit-upload\"\u003eAttach new file: \u003c\/label\u003e\n \u003cinput type=\"file\" name=\"files[upload]\"  class=\"form-file\" id=\"edit-upload\" size=\"60\" \/\u003e\n\n \u003cdiv class=\"description\"\u003eThe maximum upload size is \u003cem\u003e3 MB\u003c\/em\u003e. Only files with the following extensions may be uploaded: \u003cem\u003ejpg jpeg gif png txt xls pdf ppt pps odt ods odp gz tgz patch diff zip test info po pot psd\u003c\/em\u003e. \u003c\/div\u003e\n\u003c\/div\u003e\n\u003cinput type=\"submit\" name=\"attach\" id=\"edit-attach\" value=\"Attach\"  class=\"form-submit\" \/\u003e\n"} 

and pay...




917 ms



for 1.3 kb of nothing!

this is how we do it

Defining AJAX in the PHP arary!


  if ($field['cardinality'] != 1) {
$parents = array_slice($element['#array_parents'], 0, -1); $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value']; $field_element = drupal_array_get_nested_value($form, $parents); $new_wrapper = $field_element['#id'] . '-ajax-wrapper'; foreach (element_children($element) as $key) { if (isset($element[$key]['#ajax'])) { $element[$key]['#ajax']['path'] = $new_path; $element[$key]['#ajax']['wrapper'] = $new_wrapper; } } unset($element['#prefix'], $element['#suffix']); }

IS THIS A WIN?


or aN EPIC fail?



common js problems

AND WHAT YOU SHOULD AVOID

spaghetti code

events handling   *   AJAX request   *   JSON processing
using template   *   DOM updates

$(document).ready(function() {
  $("#getData").click( function() {
    $.getJSON("artistsRemote.cfc?method=getArtists&returnformat=json",
        function(data) {
          $("#artistsContent").empty();
          $("#artistsTemplate").tmpl(
            '<div class="fname">${fname}</div>' +
            '<div class="lname">${lname}</div>'
            '<div class="bio">${bio}</div>'
            data
          ).appendTo("#artistsContent");
          var nowIs = new Date().toLocaleString();
          $('#lastLoad').html( nowIs );
        });
  });
});




mixing business logic and rendering

is like mixing beer and sparkling wine




copy paste




passing html
in ajax response




re-rendering whole form

on AJAX event

is like if you decided to fix a chair, but did a apartment renovation

 

MEET

a solution to all of those problems

Backbone.js



  • An Open Source MVC
    framework
  • Based on Underscore.js
  • Integrates with jQuery
  • 100% minimalistic
  • Modular and expandable
  • Has a great community

benefits




  • Separation of concerns
  • Code re-usability
  • Less HTTP traffic
  • Easy to learn

jeremy ashkenas

Created Backbone.js in 2010.
Was known for CofeeScript
and  Underscore.js
Photo by UnspaceCollective.



used by

Groupon Now


FOURSQUARE


air bnb


Linkedin mobile


A SPECIAL FEATURE


Magic that bings fun into the development
and make  clients happy!



LEARN  BACKBONE.JS!

Photo by Kamil Porembiński

modeL





  • Contains data
  • Implements business logic

Model


var InvoiceItemModel = Backbone.Model.extend({  defaults: {
    description: '',
    price: 0,
    quantity: 1,
  },
  calculateAmount: function() {
    return this.get('price') * this.get('quantity');
  }
});
var model = new InvoiceItemModel({ description: 'Toy Tracktor', price: 15, quantity: 1});

MODEL


  • get, set, has, unset, clear, toJSON, escape
hacker.set('name', "<script>alert('xss')</script>");
var escaped_name = hacker.escape('name');// &lt;script&gt;alert(&#x27;xss&#x27;)&lt;&#x2F;script&gt;
  • id, idAttribute, cid
var invoiceItemModel = Backbone.Model.extend({
  idAttribute: "_id"
});invoiceItemModel._id = Math.random().toString(36).substr(2);var id = invoiceItemModel._id;

COLLECTION




  • Set of models
  • Iteratable
  • Sortable
  • Filterable

collection



var InvoiceItemCollection = Backbone.Collection.extend({
  model: InvoiceItemModel
});
var invoiceItemCollection = new InvoiceItemCollection([ {description: 'Wooden Toy House', price: 22, quantity: 3}, {description: 'Farm Animal Set', price: 17, quantity: 1} ]);


COLLECTION

  • length, at, indexOf, get
var model = invoiceItemCollection.at(2);
model.get('description'); // Farmer Figure 
  • add, remove, reset
invoiceItemCollection.reset
 ([
{description: 'Wooden Toy House', price: 22, quantity: 3},
{description: 'Farm Animal Set', price: 17, quantity: 1}
]); 
  • chain
var amount = invoiceItemCollection.chain()
.map(function(model) {
  return model.get('quantity') * model.get('price');
})
.reduce(function(memo, val) {
  return memo + val;
})
.value(); // 83 

view




  • Renders a model
  • Handles model events
  • Handles DOM events
  • Updates a model

view


var InvoiceItemView = Backbone.View.extend({
  
  tagName: 'tr',
  template: _.template($('#item-row-template').html()),

  render: function() {

    var data = this.model.toJSON();
    data.amount = this.model.calculateAmount();
    this.$el.html(this.template(data));

    return this;
  },
});

view

var InvoiceItemListView = Backbone.View.extend({
  tagName: 'table',
  template: _.template($('#item-table-template').html()),
  
  render: function() {
    $(this.el).html(this.template());
    _.each(this.collection.models, function(model, key) {
      this.append(model);
    }, this);

    return this;
  },
  append: function(model) {
    var view = new InvoiceItemView({ model: model });    this.$el.append(view.render().el);
  }
});


BINDING MODEL TO A VIEW


 var InvoiceItemView = Backbone.View.extend({
  // ...

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

HANDLING DOM EVENTS


  var InvoiceItemView = Backbone.View.extend({

  //...

  events: {
    'click button.delete': 'delete',
  },

  function: delete() {
    this.remove();
  }

});

router




  • Handles URL changes
  • Delegates app flow to a view

router


var Workspace = Backbone.Router.extend({
  routes: {
    '': 'invoiceList',
    'invoice': 'invoiceList',
    'invoice/:id': 'invoicePage',
  },
invoiceList: function() { // ... var view = new InvoiceItemListView({ collection: invoiceItemCollection } $('body').html(view.render().el); // ... }});

BACKBONE.JS COOKBOOK

  • Going deeper into  models, collections and views
  • Handling  DOM events in a view
  • Binding a model and a collection to a view
  • Using templates, forms, grids and layouts
  • Communicating with a RESTful service
  • Working with the local storage
  • Optimizing and testing Backbone application
  • Writing your own Backbone extensions
  • Ensuring compatibility with search engines
  • Creating mobile apps with jQueryMobile and PhoneGap

WRITING A BOOK

I have never expected that:
 
  • the blog post could lead to a book authoring
  • writing a book could take more time
    than being a pregnant
  • I would learn more then I knew
  • it could be a reason of stopping doing any
    other work in the end of June and in August
  • it can bring new challenges in my life
  • speaking at DrupalCon Prague

How to make Backbone.js to communicate with Drupal?

Representational state transfer


REST OVER HTTP


            C (CREATE) - POST
            R (READ) - GET
            U (UPDATE) - PUT
            D (DELETE) - DELETE


REST: RESOURCES and QUERIES



FLOW IN Backbone APP



REST IN BACKBONE.JS


var PostModel = Backbone.Model.extend({
  // Override id attribute.
  idAttribute: '_id',
  // Define URL root to access model resource.
  urlRoot: function() { return 'http://example.com/posts/'; }
// Otherwise use url() method to provide full path // to the model resource. }); var PostCollection = Backbone.Collection.extend({ model: PostModel, // Define path to the collection resource. url: function() { return 'http://example.com/posts/'; } });

REST IN BACKBONE.JS


// Fetches data into a model.
model.fetch();
// Saves a model.
model.save();

// Destroys a model. model.destroy();
// Fetches data into a collection. collection.fetch();
// Adds models to a collection. collection.add(models);
// Removes specific models from collection. collection.remove(models);

REST IN BACKBONE.js


// Pass success and error callbacksmodel.fetch({  success: function(collection, response, options) {
},
error: function(collection, response, options){
}});

REST IN Drupal 7





RESTful Web Services:
https://drupal.org/project/restws

BACKBONE MODULE


  • Is a D7 module: https://drupal.org/project/backbone
  • Provides models and collections for Drupal
    entities in the Backbone application:
    • Node as Node model
    • All nodes as NodeIndex collection
    • Arbitrary view as NodeView collection
  • Requires Services or RESTful Web Services

BACKBONE MODULE


 // Create new NodeView collection.
var viewCollection = new Drupal.Backbone.Collections.NodeView();
// Set Drupal View name.
viewCollection.viewName = 'backbone_example';
// Fetch data from the collection.
viewCollection.fetch({success: function() {
  console.log(viewCollection.toJSON());
});

BUILDING a MOBILE APP WITH
DRUPAL 8
AND BACKBONE.JS



Ptoto by James Wheeler.




BACKBONE.JS IS

IN Drupal 8 CORE!



DEMO!





Social mobile application


Drupal 8 site demo:
http://drupal8.coderblvd.com


Backbone app sources:
http://github.com/dealancer/sma


Enable core modules


  • RESTful Web Services
    Exposes entities and other resources as RESTful web API
  • Serialization
    Provides a service for (de)serializing data to/from formats such as JSON and XML
  • HAL (Hypertext Application Language)
    Serializes entities using HAL

SET PERMISSIONS



...CONFIG IS GENERATED

sites/default/files/config_XXX/active/rest.settings.yml
resources:
  'entity:node':
    GET: {  }
    POST: {  }
    PATCH: {  }
    DELETE: {  } 

CREATE A VIEW




ACCESS VIA REST

cross-site request forgery (CSRF)

aka one-click attack, session riding

Works:
<img src="http://example.com/logout">
<iframe src="http://example.com/logout"></iframe>        

Protected by modern browsers:
$.get('http://localhost/csrf/redirect').success(function(data) {});
$('#iframe').contents().find("body").html();
<form action="http://example.com/entity/node/<id>" method="DELETE">  
    <input type="submit" value="Delete" />
</form>

CSRF protection


CSRF TOKEN:

HEADER:
X-CSRF-Token: KzZD5jUyib4MmjTuRd6hZy34cW7GjcF-WnK-sbhRtFk

ADVANCED REST CLIENT

Chrome extension to test RESTful web services


Hypertext Application language (HAL)


{
  _links: {
    self: {      href: "http://example.com/node/5"    },
    type: {      href: "http://example.com/rest/type/node/article"    },
    http://example.com/rest/relation/node/article/uid: [{
      href: "http://example.com/user/0"
    }]
    http://example.com/rest/relation/node/article/revision_uid: [{
      href: "http://example.com/user/0"
    }]
  },
 

HYPERTEXT APPLICATION LANGUAGE (HAL)

   _embedded: {
    http://example.com/rest/relation/node/article/uid: [{
      _links: {
        self: {          href: "http://example.com/user/0"        },
        type: {          href: "http://example.com/rest/type/user/user"        }
      }
    },    // ...
  },
  nid: [{ value: "5" }],  uuid: [{ value: "fbb5780d-3e72-4796-ab40-64c8765a8c61" }],  vid: [{ value: "5" }],  type: [{ value: "article" }],  // ...
}




CONFIGURING 

BACKBONE.jS APP

DEFINE BASE URL AND GET A TOKEN


// Disable ajax cache
jQuery.ajaxSetup({ cache: false }); // Add REST service URL var appConfig = { baseURL: 'http://localhost/drupal-rest/', addURL: '', token: '' } // Get token and save it $.get(appConfig.baseURL + 'rest/session/token') .done(function(data) { appConfig.token = data; });

SET HEADERS


// Override Backbone.sync to include a token and set headersvar sync = Backbone.sync;
Backbone.sync = function(method, model, options) {
  options.beforeSend = function (xhr) {
    xhr.setRequestHeader('Content-type', 'application/hal+json');
    xhr.setRequestHeader('Accept', 'application/hal+json');
    xhr.setRequestHeader('X-CSRF-Token', appConfig.token);
  };
  sync(method, model, options);
};

CONVERT JSON


// Define a mixinvar mixin = {
// Transform JSON generated by Drupal parse: function(resp, options) { if (!_.isNull(resp)) { var id = this.idAttribute; _.each(resp, function(value, key) { if (_.isString(key) && key.charAt(0) != '_') { if (_.isArray(value)) { resp[key == 'nid' ? id : key] = value[0].value; } } }); delete resp.nid; } return resp; },

CONVERT JSON

  // Convert regular JSON into MongoDB extended one.
  toExtendedJSON: function() {
    var attrs = this.attributes;

    var id = this.idAttribute;
    _.each(attrs, function(value, key) {
      if (_.isString(key) && key.charAt(0) != '_') {
        attrs[key == id ? 'nid' : key] = [{ 'value': value }];
      }
    });

    return attrs;
  },

CONVERT JSON


  // Substute toJSON method when performing synchronization.
  sync: function() {
    var toJSON = this.toJSON;
    this.toJSON = this.toExtendedJSON;

    var ret = Backbone.sync.apply(this, arguments);

    this.toJSON = toJSON;

    return ret;
  }
}

// Mix our mixin
_.extend(Backbone.Model.prototype, mixin);


EXAMPLES AND RESOURCES

IN-PLACE EDITING IN D8 


IN-PLACE EDITING IN D8


  • Edit module
  • Uses AJAX instead REST
  • Overrides Model.sync() and Model.save()
    to work with AJAX

IN-PLACE EDITING in D8

(function ($, _, Backbone, Drupal, drupalSettings) {"use strict";
// ...Drupal.behaviors.edit = {
  attach: function (context) {
    // Initialize the Edit app once per page load.
    $('body').once('edit-init', initEdit);

    // Initialize models, collections and views.
    // Bind them to a DOM.
    // ...
  },  detach: function (context, settings, trigger) {
    if (trigger === 'unload') {
      // Deletes models, collections and views.
      deleteContainedModelsAndQueues($(context));
    }
  }
};// ...
})(jQuery, _, Backbone, Drupal, drupalSettings);

PANTHEON


  • Drupal dev end & cloud hosting
  • Runs on Drupal
  • Uses Backbone.js for
    the dashboard for over a year

TOOLS




RESOURCES



UPCOMING SESSIONS



WHAT DID YOU THINK?

Evaluate this session by clicking "Take the survey"

Thanks!

Using Backbone.js with Drupal 8 and 7

By Vadym Myrgorod

Using Backbone.js with Drupal 8 and 7

  • 5,479