Using Backbone.js
with drupal 8 and 7
Vadim Mirgorod aka @dealancer
ahoj!
VADIM MIRGOROD
Team Lead at Blink Reaction
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
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
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!
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');
// <script>alert('xss')</script>
- 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 was described by Roy Fielding in 2000:
http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm - Widely used as an API design model
- Client-server
- Uniform interface
- Stateless
- Cacheable
- Layered system
- Typically works over HTTP
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 callbacks
model.fetch({
success: function(collection, response, options) {
},
error: function(collection, response, options){
}
});
REST IN Drupal 7
Services module:
https://drupal.org/project/services
https://drupal.org/project/services
RESTful Web Services:
https://drupal.org/project/restws
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
http://drupal8.coderblvd.com
Bakbone app demo:
http://drupal8.coderblvd.com/sma
http://drupal8.coderblvd.com/sma
Backbone app sources:
http://github.com/dealancer/sma
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
-
Node resource:
- http://example.com/entity/node/<id>
- GET, PUT, DELETE
- http://example.com/entity/node
- POST
- Articles view:
- Headers:
-
Content-type: application/hal+json Accept: application/hal+json
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)
-
Used as a Web API
-
Is typically based on
the
JSON
- http://tools.ietf.org/id/draft-kelly-json-hal-01.html
{ _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" }],
// ...
}
_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 headers
var 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 mixin
var 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
-
http://backbonejs.org/ - official docs
-
http://backplug.io/ - bakbone plugins and extensions
-
https://groups.drupal.org/backbone-js - group
-
http://www.packtpub.com/backbonejs-mvc-rest-cookbook/book - Backbone.js Cookbook
- http://slid.es/dealancer/backbone-drupal - SLIDES!
UPCOMING SESSIONS
-
EVOLVING FRONTEND DEVELOPMENT; DANCING ON THE TIP OF A HURTLING ROCKET
@jessebeach, Tue, 14:15-15:15 -
REST AND SERIALIZATION IN DRUPAL 8
@linclark and @klausi, Wed, 13:00-14:00 -
DRUPAL 8, BACKBONE AND REST: BUILDING THE CLIENT-SIDE APPLICATION LAYER
BOF, Wed, 15:45-16:45
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,433