class: middle, center # Backbone is supposed to give me structure, ## but everything is still just a mess --- class: middle, center Today we'll talk about # how we have improved the structure of our Backbone.js apps --- class: middle, center # Who are we? Kim Joar Bekkelund Hans Magnus Inderberg --- class: middle, center # What is Backbone? A small JavaScript library with less than 1000 lines of code (6.3kB packed and gziped) Have become really popular the last few years --- class: middle, center There is a wide range of apps that uses Backbone --- class: middle, center Disqus ![](images/disqus.png) --- class: middle, center Inatur ![](images/inatur.png) --- class: middle, center Trello ![](images/trello.png) --- class: middle, center Good practices in a set of reasonably small components with a goal of improving structure *"The essential premise at the heart of Backbone has always been to try and discover the minimal set of data-structuring (Models and Collections) and user interface (Views and URLs) primitives that are useful when building web applications with JavaScript."* --- class: middle, center *… Backbone does not get you there alone — that's why we are here today! * --- class: middle, center # 10 problems and misunderstandings --- class: middle, center, blue # \#1 # Responsibility of the building blocks --- class: middle, center # Model Communicates with the server + contains and manipulate the data --- class: middle, center ![](images/3overview.jpg) --- class: middle, center ** Has no knowlegde of the DOM ** --- class: middle, center Move state from `data`-attributes in the DOM to models --- class: middle, center # Collection Responsible for a list of models --- class: middle, center ![](images/6overview.jpg) --- class: middle, center ![](images/7overview.jpg) --- class: middle, center Contains functions to manipulate its models Example: Add, remove, sort and/or find --- class: middle, center ** Has no knowlegde of the DOM ** --- class: middle, center # View Responsible for a specific HTML-element and all its children --- class: middle, center ![](images/twitter.png) --- class: middle, center ![](images/10overview.jpg) --- class: middle, center ![](images/12overview.jpg) --- class: middle, center ![](images/13overview.jpg) --- class: middle, center Should not contain business logic Perform these operations in a model, collection or some other JavaScript object --- class: middle, center # Router Responsible for handling URL changes Think of it as a state machine for the applicaiton --- class: middle, center # Events A decoupled way for objects to communicate All built-in Backbone components has events and make use of events --- class: middle ```javascript var user = new User(); // call this function on every change of the user user.on('change', function() { console.log('changed!'); }, this); ``` --- class: middle, center, orange # \#2 # Too much stuff in `initialize` --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function() { // create a model this.user = new User(); var self = this; // fetch data this.user.fetch({ success: function() { // render view when data is received self.render(); } }); } }); ``` --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function(options) { this.user = options.user; var self = this; // fetch data this.user.fetch({ success: function() { // render view when data is received self.render(); } }); } }); ``` --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function(options) { this.user = options.user; this.user.on('change', this.render, this); // fetch data this.user.fetch( ); } }); ``` --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function(options) { this.user = options.user; this.user.on('change', this.render, this); } }); ``` --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function(options) { this.user = options.user; this.user.on('change', this.render, this); } }); ``` ```javascript var Router = Backbone.Router.extend({ routes: { 'user': 'user' }, user: function() { var user = new User(); var userView = new UserView({ user: user, el: $('.user') // inject where the view should be placed }); user.fetch(); // we fetch data outside the view's constructor! } }); ``` --- class: middle, center ![](images/18overview.jpg) --- class: middle, center, brown # \#3 # Less logic in views --- class: middle ```javascript var TransactionsView = Backbone.View.extend({ initialize: function(options) { this.transactions = options.transactions; this.transactions.on('reset', this.render, this); }, render: function() { var sortedTransactions = this.transactions.sortBy( function(transaction) { var timestamp = Date.parse(transaction.get('createdAt')) * 1000; if (transaction.get("type") === transactionType.ONE_TYPE) { timestamp += "B"; } else if (transaction.get("type") === transactionType.ANOTHER_TYPE) { timestamp += "A"; } return timestamp; }); var data = _.map(sortedTransactions, function(transaction){ return transaction.toJSON(); }); this.$el.html(Mustache.to_html(this.template, data)); return this; } }); ``` --- class: middle ```javascript var TransactionsView = Backbone.View.extend({ initialize: function(options) { this.transactions = options.transactions; this.transactions.on('reset', this.render, this); }, render: function() { var sorted = this.transactions.getSortedByDateAndType(); var data = sorted.toJSON(); this.$el.html(Mustache.to_html(this.template, data)); return this; } }); ``` --- class: middle, center, green # \#4 # My views are too large --- class: middle, center ![](images/store-views.png) --- class: middle, center ![](images/store-views2.png) --- class: middle, center They always start out small but they grow and grow and become increasingly complex so it becomes difficult to understand them and it becomes difficult to test them --- ```javascript var ProductsView = Backbone.View.extend({ events: { 'click .choose': 'chooseProduct' }, initialize: function(options) { this.products = options.products; this.products.on('reset', this.render, this); }, render: function() { // too much code here }, chooseProduct: function(e) { e.preventDefault(); // HOW DO WE FIND THE CORRECT MODEL? product.doSomething(); } }); ``` --- ```javascript var ProductsView = Backbone.View.extend({ events: { 'click .choose': 'chooseProduct' }, initialize: function(options) { this.products = options.products; this.products.on('reset', this.render, this); }, render: function() { // too much code here }, chooseProduct: function(e) { e.preventDefault(); // HOW DO WE FIND THE CORRECT MODEL? // we have to go looking in the DOM var productId = $(e.currentTarget).attr('data-product-id'); product.doSomething(); } }); ``` --- ```javascript var ProductsView = Backbone.View.extend({ events: { 'click .choose': 'chooseProduct' }, initialize: function(options) { this.products = options.products; this.products.on('reset', this.render, this); }, render: function() { // too much code here }, chooseProduct: function(e) { e.preventDefault(); // HOW DO WE FIND THE CORRECT MODEL? // we have to go looking in the DOM var productId = $(e.currentTarget).attr('data-product-id'); // and then we have to find it in the products collection var product = this.products.get(productId); product.doSomething(); } }); ``` --- class: middle, center The solution? Split your view into **subviews** --- class: middle, center ![](images/subviews.png) --- # A view per model in the collection ```javascript var ProductsView = Backbone.View.extend({ initialize: function(options) { this.products = options.products; this.products.on('reset', this.render, this); }, render: function() { this.$el.html(Mustache.to_html(this.template)); var els = this.products.map(function(product) { var productView = new ProductView({ product: product }); return productView.render().el; }, this); this.$('.products').html(els); return this; } }); ``` --- # A view per model in the collection Now the `ProductView` is very simple: ```javascript var ProductView = Backbone.View.extend({ tagName: 'li', events: { 'click .choose': 'chooseProduct' }, initialize: function(options) { this.product = options.product; }, chooseProduct: function(e) { e.preventDefault(); // NOW WE ALREADY KNOW WHICH MODEL TO USE this.product.doSomething(); } }); ``` --- class: middle, center Think about performance How does this work for larger collection? Does it depend on the amount of logic in every subview? Write for structure, optimize when needed --- # Simplify by splitting views ```javascript var PersonalInformationView = Backbone.View.extend({ initialize: function(options) { this.addressView = new AddressView({ address: options.address }); this.personView = new PersonView({ person: options.person }); }, render: function() { this.$el.html(Mustache.to_html(this.template)); return this; } }); ``` --- # Simplify by splitting views ```javascript var PersonalInformationView = Backbone.View.extend({ initialize: function(options) { this.addressView = new AddressView({ address: options.address }); this.personView = new PersonView({ person: options.person }); }, render: function() { this.$el.html(Mustache.to_html(this.template)); this.personView.setElement(this.$('.person')).render(); this.addressView.setElement(this.$('.address')).render(); return this; } }); ``` --- class: middle, center, purple # \#5 # My code is not DRY --- class: middle ```javascript var UserView = Backbone.View.extend({ initialize: function(options) { this.user = options.user; this.address = options.address; }, render: function() { // Build json var data = _.extend({}, this.user.toJSON(), this.address.toJSON() ); // Compile template and add it to the DOM this.$el.html(Mustache.to_html(this.template, data)); return this; } }); ``` --- class: middle ```javascript var UserView = BaseView.extend({ initialize: function(options) { this.user = options.user; this.address = options.address; }, render: function() { return this.renderTemplate( this.user.toJSON(), this.address.toJSON() ); } }); ``` --- class: middle Can use inheritance and make a `BaseView`: ```javascript var BaseView = Backbone.View.extend({ }); ``` Make new views inherit from `BaseView`: ```javascript var UserView = BaseView.extend({ }); ``` --- class: middle Can use inheritance and make a `BaseView`: ```javascript var BaseView = Backbone.View.extend({ renderTemplate: function() { // merges all arguments into one object var data = merge(arguments); // Compile template and insert in into the DOM this.$el.html(Mustache.to_html(this.template, data)); return this; } }); ``` Make new views inherit from `BaseView`: ```javascript var UserView = BaseView.extend({ }); ``` --- class: middle Can use inheritance and make a `BaseView`: ```javascript var BaseView = Backbone.View.extend({ renderTemplate: function() { // merges all arguments into one object var data = merge(arguments); // Compile template and insert in into the DOM this.$el.html(Mustache.to_html(this.template, data)); return this; } }); ``` Make new views inherit from `BaseView`: ```javascript var UserView = BaseView.extend({ this.render: function() { return this.renderTemplate( this.user.toJSON(), this.address.toJSON() ); } }); ``` --- class: middle # Be aware of Too much inheritance You have no nice way to call a parent Can only inherit from one object at the time Another way to share functionality between objects is to use mixins --- class: middle # Mixins Mixins is an alternative to inheritance where the objects are combined ```javascript var pagination = { next: function() { // ... } }; ``` --- class: middle # Mixins Mixins is an alternative to inheritance where the objects are combined ```javascript var pagination = { next: function() { // ... } }; var Transactions = Backbone.Collection.extend({}); _.extend(Transactions.prototype, pagination); ``` --- class: middle # Mixins Mixins is an alternative to inheritance where the objects are combined ```javascript var pagination = { next: function() { // ... } }; var Transactions = Backbone.Collection.extend({}); _.extend(Transactions.prototype, pagination); var transactions = new Transactions(); transactions.next(); ``` --- class: middle, center, yellow # \#6 # Backbone plugins = jQuery plugins --- class: middle, center Don't pull in too large plugins too early — get to know Backbone first Understand the value they add --- class: middle # Plugin antipatterns Lots of plugins override code on the `Backbone` namespace For example `Backbone.localStorage`: ```javascript // Override 'Backbone.sync' to default to localSync, // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' Backbone.sync = function(method, model, options) { return Backbone.getSyncMethod(model).apply(this, [method, model, options]); }; ``` --- class: middle, center Don't become to dependent on a plugin, the development might suddenly stop Plugins can be magical, but remember that the magic is easily destroyed --- class: middle, center Remember the Hollywood principle: .huge[Don't call me, I'll call you] --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself So, you need one to many relations? --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself So, you need one to many relations? .javascript var Inbox = Backbone.Model.extend({ constructor: function() { this.messages = new Messages(); Inbox.apply(this, arguments); }, }); --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself So, you need one to many relations? .javascript var Inbox = Backbone.Model.extend({ constructor: function() { this.messages = new Messages(); Inbox.apply(this, arguments); }, parse: function(resp) { }, toJSON: function() { } }); --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself So, you need one to many relations? .javascript var Inbox = Backbone.Model.extend({ constructor: function() { this.messages = new Messages(); Inbox.apply(this, arguments); }, parse: function(resp) { this.messages.set(resp.messages, { parse: true }); delete resp.messages; return resp; }, toJSON: function() { } }); --- # Do you really need this plugin? For example, Backbone-relational is larger than Backbone itself So, you need one to many relations? .javascript var Inbox = Backbone.Model.extend({ constructor: function() { this.messages = new Messages(); Inbox.apply(this, arguments); }, parse: function(resp) { this.messages.set(resp.messages, { parse: true }); delete resp.messages; return resp; }, toJSON: function() { var json = _.clone(this.attributes); json.messages = this.messages.toJSON(); return json; } }); --- class: middle, center, red # \#7 # Backbone is used when it is not needed --- class: middle, center It's easy to create a model, collection or view for everything … use vanilla JavaScript when you don't need the functionality --- class: middle, center # Components An attempt to make plugins more manageable --- class: middle, center Keep them as small as possible Solve simple problems Prevent duplication of code Few dependencies Unix Philosophy --- # Global events `Backbone.Events` is a component which can be mixed into whatever object Gives the component the ability to bind and trigger events (for example models) ```javascript // The global events can be used to decouple different // parts and modules in the application define(['underscore', 'backbone'], function(_, Backbone) { return _.clone(Backbone.Events); }); ``` Usage: ```javascript define(['component/events'], function(events) { events.on('user:login', function() { console.log('user logged in'); }); }); ``` --- # Validator ```javascript define(['underscore'], function(_) { return { validate: function(attributes, validationRules) { var errors = []; _.each(validationRules, function(rule, name) { // ... }); if (errors.length) { return errors; } } }; }); ``` --- # SubViewHandler ```javascript define(['underscore'], function(_) { var SubViewHandler = function() { this.subViews = []; }; _.extend(SubViewHandler.prototype, { addSubView: function(subView) { if (!_.contains(this.subViews, subView)) { this.subViews.push(subView); } }, destroySubViews: function() { _.invoke(this.subViews, 'destroy'); this.subViews.length = 0; } }); return SubViewHandler; }); ``` --- class: middle, center, dark-gray # \#8 # Files, dependencies and mess --- # Folder structure ```sh $ tree . ├── app.js ├── components │ ├── subViewHandler.js │ └── validation.js ├── index.html ├── libs │ ├── backbone-0.9.10.js │ ├── jquery-1.9.1.js │ └── underscore-1.4.4.js └── modules └── payments ├── payment.js ├── payment.mustache ├── paymentView.js ├── payments.js └── paymentsView.js ``` --- class: middle It can seem trivial, but folder structure is important We prefer feature folders over having `views`, `models` and `collections` folders `=>` Collect modules in their own folders Code that belongs together should be close to each other. What about including the tests in the same folder? --- # Require.js File/module loader Dynamically load files in development and easily create a minified file for production ```javascript define(['backbone'], function(Backbone) { var Cart = Backbone.Model.extend({ url: '/api/cart' }); return Cart; }); ``` Use `cart`: ```javascript define(['./cart'], function(Cart) { new Cart(); }); ``` --- class: middle Less need for globals Makes dependencies more visible Easier to see the need for one file per object --- # Templates with Require.js Require.js has support for plugins There are several plugins for loading and compiling templates An example with a Handlebars plugin: ```javascript define([ 'hb!./payment' // finds the file ./payment.hb ], function(paymentTemplate) { // paymentTemplate is now a compiled template function }); ```   [https://github.com/hinderberg/requirejs-handlebars-plugin](https://github.com/hinderberg/requirejs-handlebars-plugin) --- # Modules and Require.js ```javascript define([ 'backbone', './paymentView', 'hb!./payments' ], function(Backbone, PaymentView, paymentsTemplate) { var PaymentsView = Backbone.View.extend({ template: paymentsTemplate, render: function() { // uses this.template this.renderTemplate(); // render paymentView for every payment return this; } }); return PaymentsView; }); ``` --- class: middle, center, dark-blue # \#9 # Don't blindly follow
Backbone conventions --- class: middle ```javascript var TransactionsView = Backbone.View.extend({ initialize: function() { this.collection.on('reset', this.render, this); }, render: function() { // what is `this.collection`? var sorted = this.collection.getSortedByDateAndType(); return this.renderTemplate(sorted.toJSON()); } }); // Somewhere else in the code: new TransactionsView({ collection: transactions }); ``` Must look for where it's initialized to find out Or maybe `console.log(this.collection)` and refresh the browser What if I need to pass in two models and a collection? --- class: middle ```javascript var TransactionsView = Backbone.View.extend({ initialize: function(options) { this.transactions = options.transactions; this.transactions.on('reset', this.render, this); }, render: function() { var sorted = this.transactions.getSortedByDateAndType(); return this.renderTemplate(sorted.toJSON()); } }); ``` Explicit is sometimes better than implicit Easier for others to understand what's going on --- class: middle, center, dark-green # In conclusion --- class: middle, mediumtext simplify your views with subviews keep business logic out of the view beware of plugins and use components build decoupled and focused modules testability ≈ structure --- class: middle, center, black # Questions? # Do you disagree? http://git.io/acbEqA