The magic of backbone, IMO is in its lack of magic. Slim, readable, non-magical source, easily extended for custom needs. #backboneconf
Trabaja en el desarrollo de noticias interactivas para el NYTimes y en DocumentCloud. Es el creador de:
Backbone.js no es un sustituto de jQuery. Depende de jQuery para manipular el DOM.
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.
Backbone.js permite estructurar una aplicación web. Separación de conceptos:
MVC | Backbone |
---|---|
Model | Model / Collection |
Controller | View / Router |
View | Template |
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
// 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) {
...
}
});
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.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
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
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" }
var Chapter = Backbone.Model.extend({
validate: function(attrs) {
if (attrs.end < attrs.start) {
return "can't end before it starts";
}
}
});
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
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
var Library = Backbone.Collection.extend({
model: Book
});
var library = new Library([book1, book2, book3]);
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);
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
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.
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});
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();
var DocumentView = Backbone.View.extend({
template : _.template("<strong>Title<strong> : <%= title %>");
render : function () {
var html = this.template(model.toJSON());
this.$el.html(html);
}
});
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']});
var template = Handlebars.compile(
"{{#each people}}<li>{{ this }}<li>{{/each}}"
);
template({people : ['moe', 'curly', 'larry']});
var DocumentView = Backbone.View.extend({
template : Handlebars.compile(
"<strong>Title<strong> : {{title}}");
render : function () {
var html = this.template(model.toJSON());
this.$el.html(html);
}
});
<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>
var DocumentRow = Backbone.View.extend({
events: {
"click .icon": "open",
"click .button.edit": "openEditDialog",
"click .button.delete": "destroy"
},
render: function() {...},
open: function() {...},
openEditDialog : function() {...},
});
model.on("change", view.render);
view.on("addUser", function (user) {
this.collection.add(user)
});
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>');
});
});
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));
}
});
Prueba que la plantilla se renderiza correctamente añadiendo usuarios
App.users.reset([{name : 'user1'}, {name : 'user2'}]);
App.usersView.render();
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
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
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();
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
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();
}
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}}">
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);
...
En Handlebars la triple llave {{{ }}}
no escapa las variables.
<p>{{{ htmlMsg }}}</p>
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);
}
}
});
Conecta los eventos
App.inputView.on("newMessage", function (msg) {
App.messages.add(msg);
});
App.Message = Backbone.Model.extend({
validate : function (attributes) {
if(_.isUndefined(attributes.msg) ||
attributes.msg.length === 0)
{
return "el mensaje debe tener contenido";
}
}
});
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);
}
}
}
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'));
});
OS X : open -a /Applications/Google\ Chrome.app --args --disable-web-security
Windows : chrome.exe --disable-web-security