Slide 1

Slide 1 text

MVC Frameworks in Javascript Hjörtur Hilmarsson @hjortureh

Slide 2

Slide 2 text

Agenda • Why MVC in Javascript ? • Backbone & Spine • Backbone fundamentals • Backbone Tips & Tricks

Slide 3

Slide 3 text

Why MVC ?

Slide 4

Slide 4 text

“The world web is changed”

Slide 5

Slide 5 text

Evolution of web apps

Slide 6

Slide 6 text

Help!

Slide 7

Slide 7 text

Contact Us

Slide 8

Slide 8 text

Markup ! ! ! ! ! ! ! ! ! !

Slide 9

Slide 9 text

Javascript - Old style $("form").submit(function( e ) { ! ! ! ! ! e.preventDefault(); ! // get values var $form = $(this); var data = { name: $form.find("[name=name]").val(), email: $form.find("[name=email]").val(), message: $form.find("[name=message]").val() }; // ajax request $.ajax({ type: "post", url: "/enquiry", contentType: "application/json", dataType: "json", data: data, success: function() { $form.find("#message").text("Message posted").fadeIn(); }, error: function() { $form.find("#message").text("Sorry, there was an error").fadeIn(); } }); });

Slide 10

Slide 10 text

Controller - MVC style $("form").submit(function( e ) { ! ! ! ! ! e.preventDefault(); ! // get values ! var $form = $(this); ! var data = { ! ! name: $form.find("[name=name]").val(), ! ! email: $form.find("[name=email]").val(), ! ! message: $form.find("[name=message]").val() ! }; ! // model ! var enquiry = new Enquiry( data ); ! ! enquiry.save( ! ! function() { ! ! ! $form.find("#message").text("Message posted"); ! ! }, ! ! function() { ! ! ! $form.find("#message").text("Sorry, there was an error"); ! ! } ! ); });

Slide 11

Slide 11 text

Model - MVC style // constructor var Enquiry = function( data ) { ! this.data = data; }; // save method Enquiry.prototype.save = function( success, error ) { ! // ajax request ! $.ajax({ ! ! type: "post", ! ! url: "/enquiry", ! ! contentType: "application/json", ! ! dataType: "json", ! ! data: this.data, ! ! success: success, ! ! error: error ! }); };

Slide 12

Slide 12 text

Backbone.js controller view var ContactUs = Backbone.View.extend({ ! ! // local variables ! el: $("form").get(0), ! events: { "submit": "submit" } ! model: new Enquiry, ! // constructor ! initialize: function() { ! ! this.model.bind("create", create, this );! ! ! this.model.bind("error", error, this );! ! }, ! // submit event ! submit: function( e ) { ! ! e.preventDefault(); ! ! ! ! var data = { ! ! ! name: this.$("[name=name]").val(), ! ! ! email: this.$("[name=email]").val(), ! ! ! message: this.$("[name=message]").val() ! ! }; ! ! this.model.save(); ! }, ! // success callback ! create: function() { ! ! this.$("#message").text("Message posted"); ! }, ! // error callback ! error: function() { ! ! this.$("#message").text("Sorry, there was an error"); ! } });

Slide 13

Slide 13 text

Backbone.js model var Enquiry = Backbone.Model.extend({});

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

MVC Benefits Structure Classes, inheritance, common patterns. Modular Communication via events, lousily coupled & testable components. Common services Back and forward history, clients-side url resources, utilities. Persistence layers RESTful sync, local storage, web sockets and more. Community Patterns, mixins, conferences and more.

Slide 16

Slide 16 text

Challenges • Going out of the box • Nested models • Complex ajax requests • Understanding the limitations • Its still hard

Slide 17

Slide 17 text

Challenges TodoMVC - http://addyosmani.github.com/todomvc/

Slide 18

Slide 18 text

To mvc, or not to mvc ? Use for one page apps Use for complex client-side UIs & crud Use not only for UI sugar Use not for just rendering HTML Use not for inflexible backends

Slide 19

Slide 19 text

Web Apps

Slide 20

Slide 20 text

Backbone & Spine

Slide 21

Slide 21 text

• Created 2010 by Jeremy Ashkenas • File size 5.4k • Depends on Underscore.js ( 4k ) • Very popular

Slide 22

Slide 22 text

http://blog.fogcreek.com/the-trello-tech-stack/

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

https://engineering.linkedin.com/mobile/linkedin-ipad-using-local-storage-snappy-mobile-apps

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

• Inspired by Backbone • Written in CoffeeScript by Alex McCaw • File size 7k • Introduced async UI concept Spine

Slide 27

Slide 27 text

Text http://hjortureh.tumblr.com/post/22117245794/spine-js-vs-backbone-js

Slide 28

Slide 28 text

Fundamentals

Slide 29

Slide 29 text

Modules • Events • Models • Collections • Views • Routes • History

Slide 30

Slide 30 text

Events

Slide 31

Slide 31 text

Events • Consists of on, off & trigger methods • All Backbone modules can trigger events • All Javascript object can be extended with the Backbone events module

Slide 32

Slide 32 text

Event example user.on("change:name", function( name ) { ! alert( "Name changed to " + name ); }); Bind to a name change event Event triggered inside User class when name is changed this.trigger("change:name", "Mr Hilmarsson");

Slide 33

Slide 33 text

Models

Slide 34

Slide 34 text

Models • Wrapper for JSON & syncing via JSON • RESTful by default. Overwrite sync method to change persistence logic. • Communicates via events ( create, change, destroy, sync, error, add , remove ) • Can handle validation

Slide 35

Slide 35 text

Model var Todo = Backbone.Model.extend({ defaults: { done: false }, toggle: function() { this.save({done: !this.get("done")}); }, clear: function() { this.destroy(); } });

Slide 36

Slide 36 text

TodoMVC - example http://addyosmani.github.com/todomvc/architecture-examples/backbone/index.html

Slide 37

Slide 37 text

Collections

Slide 38

Slide 38 text

Collections • List of models • Fires events for collection and the models • Keeps models sorted • Includes many utility methods

Slide 39

Slide 39 text

Collection var TodoList = Backbone.Collection.extend({ model: Todo, done: function() { return this.filter(function(todo){ return todo.get('done'); }); }, remaining: function() { return this.without.apply(this, this.done() ); }, comparator: function(todo) { return todo.get('order'); } });

Slide 40

Slide 40 text

Views

Slide 41

Slide 41 text

Views • Bridge the gap between the HTML and models • DOM element ( this.el ) represents the context • Uses jQuery / Zepto / ender for DOM manipulation • Listens for UI events & model events • Use render method to create view

Slide 42

Slide 42 text

Organizing views 1 : 1 View Model

Slide 43

Slide 43 text

Todo view var TodoView = Backbone.View.extend({ tagName: "li", template: _.template($('#item-template').html()), events: { "click .check" : "toggleDone" }, initialize: function() { _.bindAll(this, 'render' ); this.model.bind('change', this.render ); }, render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; }, toggleDone: function() { this.model.toggle(); } ... }

Slide 44

Slide 44 text

Template <div class="todo <%= done ? 'done' : '' %>"> <div class="display"> <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> /> <label class="todo-content"><%= content %></label> <span class="todo-destroy"></span> </div> <div class="edit"> <input class="todo-input" type="text" value="<%= content %>" /> </div> </div>

Slide 45

Slide 45 text

App view var AppView = Backbone.View.extend({ ! el: $("#todoapp"), ! ! initialize: function() { ! ! _.bindAll(this, 'addOne', 'addAll', 'render' ); ! ! Todos.on('add', this.addOne); ! ! Todos.on('reset', this.addAll); ! ! Todos.fetch(); ! }, ! addOne: function(todo) { ! ! var view = new TodoView({model: todo}); ! ! this.$("#todo-list").append(view.render().el); ! }, ! addAll: function() { ! ! Todos.each(this.addOne); ! } ! ... }

Slide 46

Slide 46 text

Router & History

Slide 47

Slide 47 text

Router & History • Provides a way to map URL resources • Enables client-side back & forward navigation • Use Hash-change by default. Supports push state ( History API )

Slide 48

Slide 48 text

Be Careful! • Its stateful ! • Its not easy • Don’t set navigate trigger to true

Slide 49

Slide 49 text

Router APP.Router = Backbone.Router.extend({ routes: { "new": "newNote", ":id": "editNote", "": "home" }, home: function() { APP.appView.home(); }, newNote: function() { APP.appView.newNote(); }, editNote: function( id ) { APP.appView.editNote( id ); } });

Slide 50

Slide 50 text

History - example // Start the history Backbone.history.start(); // Start the history Backbone.history.start({pushState: true}); Use html5 history API Start listening for hash-change events

Slide 51

Slide 51 text

Demo

Slide 52

Slide 52 text

Backbone tips & tricks

Slide 53

Slide 53 text

Tips & Tricks • Tip #1 - Bootstrapping data • Tip #2 - Async user interfaces • Tip #3 - Nested models • Tip #4 - Custom ajax requests • Tip #5 - Zombies to heaven • Tip #6 - The toolbox • Tip #7 - Test, test, test • Tip #8 - CoffeeScript • Tip #9 - Remember the basics • Tip #10 - Bonus points

Slide 54

Slide 54 text

Tip #1 Bootstrapping data

Slide 55

Slide 55 text

Bootstrapping data • Using fetch extends waiting time • Possible to bootstrap the most important data when the page is rendered • No loading spinners !

Slide 56

Slide 56 text

Bootstrapping Data // Current user APP.currentUser = new APP.Models.User(<%= @current_user.to_json.html_safe %>); // Notes APP.notes.reset(<%= @notes.to_json.html_safe %>); The code After render // Current user APP.currentUser = new APP.Models.User({ id: 1, username: "hjortureh", name: "Hjortur Hilmarsson", avatar: "avatar.gif" }); // Notes APP.notes.reset([ { id: 1, text: "Note 1" }, { id: 1, text: "Note 2" }, { id: 1, text: "Note 3" } ]);

Slide 57

Slide 57 text

Demo

Slide 58

Slide 58 text

Twitter demo

Slide 59

Slide 59 text

Tip #2 Async User Interfaces

Slide 60

Slide 60 text

Importance of speed Amazon 100 ms of extra load time caused a 1% drop in sales (source: Greg Linden, Amazon). Google 500 ms of extra load time caused 20% fewer searches (source: Marrissa Mayer, Google). Yahoo! 400 ms of extra load time caused a 5–9% increase in the number of people who clicked “back” before the page even loaded (source: Nicole Sullivan, Yahoo!). 37 Signals - Basecamp 500 ms increase in speed on basecamp.com resulted in 5% improvement in conversion rate.

Slide 61

Slide 61 text

Importance of speed

Slide 62

Slide 62 text

Async user interfaces • Models are optimistic by default • UI is updated before server response • Use cid as a unique identifier on the client • No loading spinners !

Slide 63

Slide 63 text

Demo

Slide 64

Slide 64 text

Tip #3 Nested Models

Slide 65

Slide 65 text

Question Has many Answers

Slide 66

Slide 66 text

Nested models • Nested models are common • No official way of doing it • Overwrite parse after ajax request • Overwrite toJSON before ajax request • Backbone-relational mixin could help

Slide 67

Slide 67 text

Nested models var Question = Backbone.Model.extend({ initialize: function() { // collection instance this.answers = new Answers; }, parse: function(resp, xhr) { // fill nested model if( _.isArray( resp.answers ) ) { this.answers.reset( resp.answers ); } return resp; }, toJSON: function() { // send nested models return $.extend( this.attributes(), { answers: this.answers.toJSON() } ); } });

Slide 68

Slide 68 text

Tip #4 Custom ajax requests

Slide 69

Slide 69 text

Custom ajax request • Sometimes RESTful methods are not enough • Example: Sorting tasks in to-do list

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

Sorting - Custom request saveOrder: function() { ! ! var ids = this.pluck("id"); ! ! window.$.ajax({ ! ! url: "/tasks/reorder", ! ! data: { ! ! ! ids: ids ! ! }, ! ! type: "POST", ! ! dataType: "json", ! ! complete: function() { ! ! ! // Handle response ! ! } ! }); ! }

Slide 72

Slide 72 text

Tip #5 Send zombies to heaven

Slide 73

Slide 73 text

Zombies to heaven • Its not enough to remove views from the DOM • Events must be released so you don’t have zombies walking around

Slide 74

Slide 74 text

Zombies to heaven // same as this.$el.remove(); this.remove(); // remove all models bindings // made by this view this.model.off( null, null, this ); // unbind events that are // set on this view this.off();

Slide 75

Slide 75 text

Tip #6 Use the toolbox

Slide 76

Slide 76 text

Use the toolbox • Underscore has some wonderful methods • isFunction, isObject, isString, isNumber, isDate & more. • Underscore: http:// documentcloud.github.com/underscore

Slide 77

Slide 77 text

Underscore // Underscore methods that we want to implement on the Collection. var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Collection.prototype[method] = function() { return _[method].apply(_, [this.models].concat(_.toArray(arguments))); }; }); Line 865 from the Backbone.js code.

Slide 78

Slide 78 text

Tip #7 Test, test, test

Slide 79

Slide 79 text

Testing • Recommend Jasmine for testing • Recommend Sinon to fake the server • jQuery-jasmine to test views • Use setDomLibrary method to fake jQuery

Slide 80

Slide 80 text

Jasmine with fake server & spy it('Should sync correctly', function () { // mockup data var note = new APP.Models.Note({ text: "Buy some eggs" }); // fake server this.server = sinon.fakeServer.create(); // fake response this.server.respondWith( "POST", "/notes", [ 200, {"Content-Type": "application/json"}, '{ "id": 1, "text": "Remember the milk" }' ] ); // spy on sync event var spy = sinon.spy(); note.on("sync", spy ); // save model note.save(); // server repsonse this.server.respond(); // assert expect( spy ).toHaveBeenCalledOnce(); expect( spy ).toHaveBeenCalledWith( note ); expect( note.get("text") ).toEqual( "Remember the milk" ); // restore fake server this.server.restore(); });

Slide 81

Slide 81 text

Demo

Slide 82

Slide 82 text

Tip #8 CoffeeScript

Slide 83

Slide 83 text

CoffeeScript • Advanced programing language • Compiles to javascript • Same creator of Backbone and CoffeeScript • Integrates well with Backbone

Slide 84

Slide 84 text

Coffee Script example class TodoList extends Backbone.View _.bindAll( this, 'render' ) render: => @$el.html( @template( @.model.toJSON() )) @ initialize: -> ! super Extending Backbone module Double arrow to bind to the context Use @ instead of this Last line is the return value, returns this Need to call super on parent constructors

Slide 85

Slide 85 text

Tip #9 The Basics

Slide 86

Slide 86 text

The basics • The basics still apply with MVC in place • Minimize ajax requests • Keep your views thin & models fat • Understanding Javascript is the key

Slide 87

Slide 87 text

Tip #10 Bonus

Slide 88

Slide 88 text

Bonus points • Read the documentation • Read the source code • Just do it !

Slide 89

Slide 89 text

Tack så mycket Hjörtur Elvar Hilmarsson @hjortureh