Javascript MVC & Backbone Tips & Tricks
-
Upload
hjoertur-hilmarsson -
Category
Technology
-
view
5.941 -
download
0
Transcript of Javascript MVC & Backbone Tips & Tricks
MVC Frameworksin Javascript
Hjörtur Hilmarsson@hjortureh
Agenda
• Why MVC in Javascript ?
• Backbone & Spine
• Backbone fundamentals
• Backbone Tips & Tricks
Why MVC ?
“The world web is changed”
Evolution of web apps
Help!
Contact Us
Markup
<form>
! <!-- Name input -->! <input id="name" name="name" type="text" placeholder="What is your name?" required />
! <!-- Email input --> ! <input id="email" name="email" type="email" placeholder="What is your email?" required />
! <!-- Message input -->! <textarea id="message" name="message" placeholder="Hello!" required ></textarea>
! <!--Send button -->! <input id="submit" name="submit" type="submit" value="Send" />
! <!-- Message label -->! <span id="message" ></span>
</form>
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(); } });});
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");! ! }! );});
Model - MVC style
// constructorvar Enquiry = function( data ) {! this.data = data;};
// save methodEnquiry.prototype.save = function( success, error ) {
! // ajax request! $.ajax({! ! type: "post",! ! url: "/enquiry",! ! contentType: "application/json",! ! dataType: "json",! ! data: this.data,! ! success: success,! ! error: error! });
};
Backbone.js controller viewvar 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");! }
});
Backbone.js model
var Enquiry = Backbone.Model.extend({});
MVC Benefits
StructureClasses, inheritance, common patterns.
ModularCommunication via events, lousily coupled & testable components.
Common servicesBack and forward history, clients-side url resources, utilities.
Persistence layersRESTful sync, local storage, web sockets and more.
CommunityPatterns, mixins, conferences and more.
Challenges
• Going out of the box
• Nested models
• Complex ajax requests
• Understanding the limitations
• Its still hard
Challenges
TodoMVC - http://addyosmani.github.com/todomvc/
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
Web Apps
Backbone & Spine
• Created 2010 by Jeremy Ashkenas
• File size 5.4k
• Depends on Underscore.js ( 4k )
• Very popular
http://blog.fogcreek.com/the-trello-tech-stack/
https://engineering.linkedin.com/mobile/linkedin-ipad-using-local-storage-snappy-mobile-apps
• Inspired by Backbone
• Written in CoffeeScript by Alex McCaw
• File size 7k
• Introduced async UI concept
Spine
Text
http://hjortureh.tumblr.com/post/22117245794/spine-js-vs-backbone-js
Fundamentals
Modules
• Events
• Models
• Collections
• Views
• Routes
• History
Events
Events
• Consists of on, off & trigger methods
• All Backbone modules can trigger events
• All Javascript object can be extended with the Backbone events module
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");
Models
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
Model
var Todo = Backbone.Model.extend({
defaults: { done: false },
toggle: function() { this.save({done: !this.get("done")}); },
clear: function() { this.destroy(); }
});
TodoMVC - example
http://addyosmani.github.com/todomvc/architecture-examples/backbone/index.html
Collections
Collections
• List of models
• Fires events for collection and the models
• Keeps models sorted
• Includes many utility methods
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'); }
});
Views
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
Organizing views
1 : 1View Model
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(); }
...}
Template
<script type="text/template" id="item-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>
</script>
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);! }
! ...
}
Router & History
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 )
Be Careful!
• Its stateful !
• Its not easy
• Don’t set navigate trigger to true
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 ); }
});
History - example
// Start the historyBackbone.history.start();
// Start the historyBackbone.history.start({pushState: true});
Use html5 history API
Start listening for hash-change events
Demo
Backbone tips & tricks
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
Tip #1Bootstrapping data
Bootstrapping data
• Using fetch extends waiting time
• Possible to bootstrap the most important data when the page is rendered
• No loading spinners !
Bootstrapping Data
// Current userAPP.currentUser = new APP.Models.User(<%= @current_user.to_json.html_safe %>);
// NotesAPP.notes.reset(<%= @notes.to_json.html_safe %>);
The code
After render
// Current userAPP.currentUser = new APP.Models.User({ id: 1, username: "hjortureh", name: "Hjortur Hilmarsson", avatar: "avatar.gif" });
// NotesAPP.notes.reset([ { id: 1, text: "Note 1" }, { id: 1, text: "Note 2" }, { id: 1, text: "Note 3" }]);
Demo
Twitter demo
Tip #2Async User Interfaces
Importance of speedAmazon 100 ms of extra load time caused a 1% drop in sales (source: Greg Linden, Amazon).
Google500 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 - Basecamp500 ms increase in speed on basecamp.com resulted in 5% improvement in conversion rate.
Importance of speed
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 !
Demo
Tip #3Nested Models
Question
Has many
Answers
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
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() } ); }
});
Tip #4Custom ajax requests
Custom ajax request
• Sometimes RESTful methods are not enough
• Example: Sorting tasks in to-do list
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! ! }! });!}
Tip #5Send zombies to heaven
Zombies to heaven
• Its not enough to remove views from the DOM
• Events must be released so you don’t have zombies walking around
Zombies to heaven
// same as this.$el.remove();this.remove();
// remove all models bindings// made by this viewthis.model.off( null, null, this );
// unbind events that are// set on this viewthis.off();
Tip #6Use the toolbox
Use the toolbox
• Underscore has some wonderful methods
• isFunction, isObject, isString, isNumber, isDate & more.
• Underscore: http://documentcloud.github.com/underscore
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.
Tip #7Test, test, test
Testing
• Recommend Jasmine for testing
• Recommend Sinon to fake the server
• jQuery-jasmine to test views
• Use setDomLibrary method to fake jQuery
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();
});
Demo
Tip #8CoffeeScript
CoffeeScript
• Advanced programing language
• Compiles to javascript
• Same creator of Backbone and CoffeeScript
• Integrates well with Backbone
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 contextUse @ instead of thisLast line is the return value, returns this
Need to call super on parent constructors
Tip #9The Basics
The basics
• The basics still apply with MVC in place
• Minimize ajax requests
• Keep your views thin & models fat
• Understanding Javascript is the key
Tip #10Bonus
Bonus points
• Read the documentation
• Read the source code
• Just do it !
Tack så mycketHjörtur Elvar Hilmarsson
@hjortureh