Backbone.js

Clientside MVC Framework

TLP2k12

¿Quién soy?

¿Dónde trabajo?

arte-consultores
stat4you

¿A qué me dedicaba?

Java
Javascript

¿A qué me dedico?

Java
Javascript

Backbone.js

The magic of backbone

The magic of backbone, IMO is in its lack of magic. Slim, readable, non-magical source, easily extended for custom needs. #backboneconf

Hakim El Hattab @hakimel

Creador

Jeremy Ashkenas

Jeremy Ashkenas

Jeremy Ashkenas

Trabaja en el desarrollo de noticias interactivas para el NYTimes y en DocumentCloud. Es el creador de:

¿Quién utiliza Backbone.js?

Examples

ver lista completa

Tamaño

<6kb

Dependencias

  • Underscore.js
  • jQuery/Zepto.js
  • json2.js

¿Por qué utilizar Backbone.js?

"Yo con jQuery tengo suficiente"

Backbone.js no es un sustituto de jQuery. Depende de jQuery para manipular el DOM.

¿Por qué utilizar Backbone.js?

Spaghetti code

Backbone.js añade una capa de abstracción superior, permitiendo estructurar la aplicación.

Desvincular los datos del DOM permite hacer la aplicación más mantenible y testeable.

¿Qué es Backbone.js?

Backbone.js permite estructurar una aplicación web. Separación de conceptos:

  • Models
  • Collections
  • View
  • Router

MVC

MVC Backbone
Model Model / Collection
Controller View / Router
View Template

Persistencia

  • RESTFul
  • LocalStorage (vía plugin)
  • WebSockets (vía plugin)

¿Quieres aprender Javascript?

Annotated Source

Annotated Source

API

Model

Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control.

Basic set of functionality for managing changes: Events

Model / initialize

// constructor por defecto
var Book = Backbone.Model.extend({});

var book = new Book({
    title: "One Thousand and One Nights",
    author: "Scheherazade"
});

// extender el constructor
var Book = Backbone.Model.extend({
    initialize : function (attributes) {
        ...
    }
});
    

Model / accessors

var book = new Book({
    title: "One Thousand and One Nights",
    author: "Scheherazade"
});

book.get('title'); //"One Thousand and One Nights"

book.set({title: "March 20", content: "In his eyes she eclipses..."});
book.set("title", "A Scandal in Bohemia");
book object internals

Model / events

book.set('title', '...'); // Se lanza evento "change:title" y "change"

book.set({title : '', content : ''}); // Se lanza un evento change
// por cada atributo y un único "change"

book.set({title : ''}, {silent : true}); // No lanza eventos

Model / defaults

var Meal = Backbone.Model.extend({
    defaults: {
        "appetizer": "caesar salad",
        "entree": "ravioli",
        "dessert": "cheesecake"
    }
});
var meal = new Meal({dessert : 'ice cream'});

meal.get('dessert'); // ice cream
meal.get('entree'); // ravioli

Model / toJSON

Devuelve una copia de los atributos


var artist = new Backbone.Model({
    firstName: "Wassily",
    lastName: "Kandinsky"
});
artist.set({birthday: "December 16, 1866"});

artist.toJSON(); // { firstName: "Wassily", lastName: "Kandinsky", birthday: "December 16, 1866" }

Model / validate

var Chapter = Backbone.Model.extend({
    validate: function(attrs) {
        if (attrs.end < attrs.start) {
            return "can't end before it starts";
        }
    }
});

Model / Sync

Internamente los métodos "save", "fetch" y "destroy" llaman al método Backbone.sync pasándole los parámetros adecuados.

La implementación por defecto es Rest.

Backbone.sync se puede sobreescribir: LocalStorage, Socket.io

Model / Sync / Ajax

var Book = Backbone.Model.extend({
    urlRoot : '/books'
});

var book = new Book({
    title: "The Rough Riders",
    author: "Theodore Roosevelt"
});

book.save(); // HTTP POST /books
book.set('title', '...');
book.save({author: "Teddy"}); //HTTP PUT /books/3
book.fetch(); // HTTP GET /books/3
book.destroy(); //HTTP DELETE /books/3

Collection

Colletion / initialize

var Library = Backbone.Collection.extend({
    model: Book
});
var library = new Library([book1, book2, book3]);

Collection / modificar

var ship1 = Backbone.Model({name: "Flying Dutchman"});
var ship2 = Backbone.Model({name: "Black Pearl"});

var ships = new Backbone.Collection;

//trigger "add", "remove" events
ships.add([ship1, ship2]);
ships.remove(ship1);
ships.get(ship2.id);
ships.push(ship1);
ships.pop(ship1);
ships.unshift(ship1);
ships.shift(ship1);

Collection / modificar / Underscore

Collection / Sync / Ajax

var Notes = Backbone.Collection.extend({
    url: '/notes'
});

var notes = new Notes();
notes.fetch(); // HTTP GET /notes
notes.fetch({data: {page: 3}})  //HTTP GET /notes?page=3

Router

Manipulando URLs con #hashFragments y History API

Router / Soporte de navegadores

For browsers which don't yet support the History API, the Router handles graceful fallback and transparent translation to the fragment version of the URL.

Router / routes

var Router = Backbone.Router.extend({
    routes: {
        "help":                 "help",    // #help
        "search/:query":        "search",  // #search/kiwis
        "search/:query/p:page": "search"   // #search/kiwis/p7
    },

    help: function() {...},
    search: function(query, page) {...}

});
var router = new Router();
Backbone.history.start();
Backbone.history.start({pushState : true});

router.navigate("help", {trigger: true});

View

View / Básica

var DocumentView = Backbone.View.extend({
    render: function() {
        var html = this.template(model.toJSON());
        this.$el.html(html);
    }
});

var document = new Backbone.Model({title : 'title'});
var documentView = new DocumentView({el : '#document', model : document});
documentView.render();

View / Templates / Underscore


    var DocumentView = Backbone.View.extend({
        template : _.template("<strong>Title<strong> : <%= title %>");
        render : function () {
            var html = this.template(model.toJSON());
            this.$el.html(html);
        }
    });
    

View / Templates / Underscore

El principal inconveniente de las plantillas de undescore es el código inline para hacer cosas sencillas.

Ej: Bucle


var list = "<% _.each(people, function(name) { %> <li><%= name %><li> <% }); %>";
_.template(list, {people : ['moe', 'curly', 'larry']});
    

¿Solución?

View / Templates / Handlebars


var template = Handlebars.compile(
   "{{#each people}}<li>{{ this }}<li>{{/each}}"
);
template({people : ['moe', 'curly', 'larry']});
    

View / Templates / Handlebars


var DocumentView = Backbone.View.extend({
    template : Handlebars.compile(
        "<strong>Title<strong> : {{title}}");

    render : function () {
        var html = this.template(model.toJSON());
        this.$el.html(html);
    }
});
    

View / Multiline templates

<script type="text/html" id="#documentView">
    <ul>
    {{#each people}}
        <li>{{ name }}</li>
    {{/each}}
    </ul>
</script>
<script>
var DocumentView = Backbone.View.extend({
    template : Handlebars.compile($('#documentView').html()),
    render : function() {
        this.$el.html(this.template(this.collection.toJSON()));
    }
});
</script>

View / Events


var DocumentRow = Backbone.View.extend({

    events: {
        "click .icon":          "open",
        "click .button.edit":   "openEditDialog",
        "click .button.delete": "destroy"
    },

    render: function() {...},
    open: function() {...},
    openEditDialog : function() {...},

});
    

Conectar eventos


        model.on("change", view.render);
        view.on("addUser", function (user) {
            this.collection.add(user)
        });
    

Testing

Testing Backbone applications with Jasmine and Sinon

describe("TodoView", function() {
    beforeEach(function() {
        this.model = new Backbone.Model({title: "My Todo",
                                         id: 1});
        this.view = new TodoView({model:this.model});
    });
    it("produces the correct HTML", function() {
        this.view.render();
        expect(this.view.el.innerHTML)
            .toEqual('<a href="#todo/1"><h2>My Todo</h2></a>');
    });
});

Chat Example

Identificar elementos

Diagrama de eventos

UsersView

Esta vista renderiza la plantilla con la lista de usuarios conectados

App.UsersView = Backbone.View.extend({

    template : Handlebars.compile($('#usersTmpl').html()),

    render : function () {
        var context = {
            users : this.collection.toJSON()
        };
        this.$el.html(this.template(context));
    }
});

UsersView

Prueba que la plantilla se renderiza correctamente añadiendo usuarios

App.users.reset([{name : 'user1'}, {name : 'user2'}]);
App.usersView.render();

UsersView

Conecta los eventos para que al modificar la lista de usuarios, se renderice la vista automáticamente


App.users.on('add reset', function () {
    App.usersView.render();
});
App.users.reset([{name : 'user1'}, {name : 'user2'}]);
    

También se debería conectar el evento "remove", pero en nuestro caso el servidor devuelve la lista completa de usuarios. El único evento que se lanza es el reset

MessagesView

App.MessagesView = Backbone.View.extend({
    msgTemplate : Handlebars.compile($('#msgTmpl').html()),
    render : function () {
        var view = this;
        this.collection.each(function(message) {
            view.appendMsg(message);
        });
    },
    appendMsg : function (msg) {
        var context = msg.toJSON();
        this.$el.append(this.msgTemplate(context));
    }
});

En este caso en vez de renderizar la lista completa, se renderiza cada uno de los elementos porque el servidor devuelve mensajes individualmente

MessagesView

Prueba la vista añadiendo algunos mensajes.

App.messages.reset([
    {username : 'username', timestamp: '01-01-2012', msg : 'hola'},
    {username : 'username2', timestamp: '01-01-2012', msg : 'mundo'}
]);
App.messagesView.render();

MessagesView

Conecta los eventos

App.messages.on('add', function (message) {
    App.messagesView.appendMsg(message);
});
App.messages.on('reset', function () {
    App.messagesView.render();
});
var msg1 = new App.Message(
  {username : 'username', timestamp: '01-01-2012', msg : 'hola'});
var msg2 = new App.Message(
  {username : 'username2', timestamp: '01-01-2012', msg : 'mundo'});
App.messages.reset([msg1]);
App.messages.add(msg2);

En este caso tenemos dos eventos distintos porque se llamará a "reset" cuando se conecte al servidor y a "add" cuando llegue un mensaje nuevo

MessagesView / scrollBottom

Cuando llegue un mensaje se debería ver

scrollBottom : function () {
    this.$el.scrollTop(this.$el.prop('scrollHeight'));
},

appendMsg : function (msg) {
    var context = msg.toJSON();
    this.$el.append(this.msgTemplate(context));
    this.scrollBottom();
}

MessagesView / bubble color

initialize : function (options) {
    this.config = options.config;
},
appendMsg : function (msg) {
    var context = msg.toJSON();
    context.color = context.username === this.config.get('username') ? "me" : "other";
    this.$el.append(this.msgTemplate(context));
    this.scrollBottom();
}
<script type="text/html" id="msgTmpl">
        <div class="chat-bubble {{color}}">

MessagesView / smiles

getHtmlMsg : function (msg) {
    var smiles = [":)", ":'(", ":P", ";)", ":*", ":D", ":@", "¬¬", "8|"],
    codes  = ["85", "358", "348", "344", "363", "86", "361", "353", "352"];

    var result = msg;
    for(var i = 0; i < smiles.length; i++){
        var smile = smiles[i], code = codes[i];
        result = result.replace(smile, '<i class="icon-' + code + '"></i>');
    }
    return result;
},

appendMsg : function (msg) {
    var context = msg.toJSON();
    context.htmlMsg = this.getHtmlMsg(context.msg);
...

MessagesView / smiles / template

En Handlebars la triple llave {{{ }}} no escapa las variables.

<p>{{{ htmlMsg }}}</p>

InputView

App.InputView = Backbone.View.extend({

    initialize : function (options) {
        this.$input = this.$el.find('input');
    },

    events : {
        "keypress" : "keypress"
    },

    keypress : function (e) {
        if (e.which == 13) { // enter
            var val = this.$input.val();
            this.$input.val('');

            var msg = new App.Message({msg : val});
            this.trigger("newMessage", msg);
        }
    }
});

InputView

Conecta los eventos

App.inputView.on("newMessage", function (msg) {
    App.messages.add(msg);
});

InputView / validation

App.Message = Backbone.Model.extend({
    validate : function (attributes) {
        if(_.isUndefined(attributes.msg) ||
           attributes.msg.length === 0)
        {
            return "el mensaje debe tener contenido";
        }
    }
});

InputView / validation

keypress : function (e) {
    if (e.which == 13) { // enter
        var val = this.$input.val();

        var msg = new App.Message({msg : val});
        if(msg.isValid()){
            this.$input.val('');
            this.trigger("newMessage", msg);
        }
    }
}

Socket.io server

Socket.io sync

var socket = io.connect('http://chat-tlp2k12.herokuapp.com');
socket.on('connect', function () {
    var username = prompt("What's your name?");
    App.config.set('username', username);
    socket.emit('adduser', username);
});
socket.on("initchat", function (response) {
    App.messages.reset(response);
});
socket.on('updatechat', function (message) {
    App.messages.add(message);
});
socket.on('updateusers', function (data) {
    var users = _.map(_.keys(data), function (name) {
        return {name : name};
    });
    App.users.reset(users);
});
App.inputView.on("newMessage", function (message) {
    socket.emit('sendchat', message.get('msg'));
});

Ajax Cross Domain

OS X : open -a /Applications/Google\ Chrome.app --args --disable-web-security

Windows : chrome.exe --disable-web-security

Competencia

The Top 10 Javascript MVC Frameworks Reviewed

Recursos

Gracias