Backbone.js

Conceptos basicos de js

Al crear un objeto se hace así

 var Person = function(config) {
this.name = config.name;
this.age = config.age;
this.occupation = config.occupation;
};
Person.prototype.work = function() {
return this.name + ' is working.';
}

Nótese el config para recibir parametros, que lo que permite es que se puedan pasar parametro asi:

var person = new Person({ name: 'John Smith', age: 30, occupation: 'web developer' });

Y nótese también en el código como se hace para agregar métodos a la clase Persona. Al hacerlo a través de prototype, todos los objetos de la clase Persona, harán uso del método work, en vez de crear el método múltiples veces cada vez que se instancia la clase.

En el siguiente ejemplo se ve mejor, en el primer caso seria como un metodo de clase (static method) mientras que en el segundo caso seria un método de instancia. En JS las funciones son objetos de primera clase, es decir que las funciones pueden ser tratadas como cualquier objeto.

Class.method = function () { /* code */ }
Class.prototype.method = function () { /* code using this.values */ }

El siguiente es un ejemplo completo de esto a lo que nos referimos.

// constructor function
function MyClass () {
var privateVariable; // private member only available within the constructor fn
this.privilegedMethod = function () { // it can access private members
//..
};
}

// A 'static method', it's just like a normal function
// it has no relation with any 'MyClass' object instance
MyClass.staticMethod = function () {};

MyClass.prototype.publicMethod = function () {
// the 'this' keyword refers to the object instance
// you can access only 'privileged' and 'public' members
};

var myObj = new MyClass(); // new object instance

myObj.publicMethod();
MyClass.staticMethod();

Luego que se creo la clase para instanciarla hacemos lo siguiente:

var person = new Person({ name: "John", age : 30, occupation : "IT Manager" });

Modelos

var Person = Backbone.Model.extend({
defaults: {
name: 'John Doe',
age: 30,
occupation: 'IT Manager'
}
});

Siempre es conveniente poner valores por defecto, asi sea vacio. En backbone, una vez que instanciamos el objeto (en este caso persona), para acceder a los atributos debemos usar el método get.

var person = new Person();
person.get('name');

La razón por la cual Backbone requiere de estos métodos accesores, es porque el modelo lleva control de los cambios, eventualmente para poder asociar eventos ante los cambios en los datos.

Si queremos agregar el método work() ahora lo que debemos hacer es lo siguiente:

var Person = Backbone.Model.extend({
defaults: {
name: 'John Doe',
age: 30,
occupation: 'IT Manager'
},
work: function() {
return this.get('name') + ' is working';
}
});

Ahora, un detalle más es que defaults actua como llamar al constructor por defecto, por lo tanto si invocamos la creación del objeto, pasándole parámetros tal como hicimos anteriormente podremos sobreescribir los valores por defecto.

var person = new Person({ name: “Juan” });

Esto podemos hacerlo con uno de los atributos o para todos.

Si inspeccionamos este objeto vemos que para acceder a sus atributos, debemos inspeccionar _attributes. Por lo tanto, si queremos exponer el objeto basico lo que podemos usar es:

person.toJSON();
Object {name: "Juan", age: 30, occupation: "IT Manager"}

Ahora, si lo que deseamos es asignarle un valor a algún atributo, en vez de usar get, usamos el método set.

person.set('name','John');

Este método se puede usar tanto para asignar un atributo como para asignar varios

person.set({ nombre: 'Juan', age: 40 });

Validación

Para validar el modelo se debe reescribir el método validate. En la validación existen sin embargo algunos detalles a tener en cuenta. Primeramente la validación por defecto se dispara cuando se salva el modelo (save), a menos que se le pase un parametro al metodo set indicando que valide la asignacion.

Por otra parte, cuando se genera un error lo que sucede es que se dispara el evento “invalid” el cual debe ser capturado para hacer algo cuando ello suceda, de lo contrario no se verá ningun resultado.

El siguiente ejemplo muestra como sería su uso.

var Person = Backbone.Model.extend({
defaults: {
name: 'John Doe',
age: 30,
occupation: 'IT Manager'
},

validate: function(attrs) {
if( attrs.age < 0 ){
return 'La edad debe ser un valor positivo';
}

if(! attrs.name) {
return 'Debe ingresar un nombre';
}
},

initialize: function(){
this.on("invalid",function(model, error){
console.log(error);
})
},

work: function() {
return this.get('name') + ' is working';
}
});

Luego cuando se instancia se valida de la siguiente manera:

var person = new Person();
person.set({age: -12}, {validate : true});
La edad debe ser un valor positivo main.js:21
false

También se podría validar al crear el objeto, lo único que en este caso, si bien se carga el error en el objeto y no se actualiza el dato, no se muestra ningún error porque el initialize se ejecuta después de creado el objeto, por lo tanto el evento debería capturarse en el constructor.

var person2 = new Person({name : 'qian', age : -12}, {validate : true});
person
r {cid: "c1", attributes: Object, _changing: false, _previousAttributes: Object, changed: Object…}
_changing: false
_events: Object
_pending: false
_previousAttributes: Object
attributes: Object
age: 30
name: "John Doe"
occupation: "IT Manager"
__proto__: Object
changed: Object
cid: "c1"
validationError: "La edad debe ser un valor positivo"
__proto__: s

Vistas

Las vistas en Backbone no sólo son vistas de un modelo MVC clásico, sino que por el contrario las vistas definen un area donde se presentarán los datos almacenados en el modelo y contienen código de Controller.

Para crear una vista se hace de la siguiente manera:

var PersonView = Backbone.View.extend({});

En la vista tenemos un atributo tagName, que corresponde al elemento del DOM en el que se mostrara lo que deseemos. Contamos con un atributo opcional className y otro id para asignarle un id o una clase al elemento.

Las vistas como los modelos cuentan con el metodo initialize donde podemos pre cargar valores. Al crear una vista se le puede pasar por parametro el modelo con el que trabajara.

Luego haciendo uso del método render, lo que hacemos es relacionar valores del modelo con la vista.

Por ejemplo:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Backbone Test</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-responsive.min.css">
<script src="js/jquery.min.js"></script>
<script src="js/handlebars.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.min.js"></script>

<!-- Setup our templates -->
<script id="personTemplate" type="text/x-handlebars-template">
<h2>{{name}} ({{age}})</h2>
</script>

<!-- End templates setup -->

<script>
// Stub out the person model
var Person = Backbone.Model.extend({

});

// Define the view for a single person
var PersonView = Backbone.View.extend({

render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());

$(this.el).html(html);

return this;
}
});

$(document).ready(function(){
// Create new instances of the person models
var person = new Person({name: "Tim", age: 5});

// Create instances of the views
var personView = new PersonView({
model: person
});

personView.el = $('#personContainer'),

personView.render();
});

</script>

</head>
<body>
<div id='personContainer'></div>
</body>
</html>

Analicemos el ejemplo:

Primero cargamos las bibliotecas

<script src="js/jquery.min.js"></script>
<script src="js/handlebars.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.min.js"></script>

Luego definimos el template que vamos a utilizar. Backbone soporta cualquier motor de procesamiento de plantillas (template engine) y viene por defecto con underscore, que no sólo tiene un template engine sino que además cuenta con una serie de funciones utilitarias para el acceso de los objetos y colecciones.

En este caso como template engine utilizamos uno alternativo llamado handlebars. Para su uso solamente hay que incluir el archivo js a la lista de los que se cargan, como se ve arriba.

Los templates en js no son otra cosa que un area de inclusion de codigo js tipica, a la que se le agrega un identificador unico de referencia y en el tipo se poner type/template o similar.

Dentro de esa seccion simplemente se coloca codigo HTML y entre las marcas que soporte el template engine, todo lo referente a dicho template.

En este caso vamos mostrar la variable nombre y edad. Handlebars usa doble llaves para encerrar los valores a ser procesados.

<script id="personTemplate" type="text/x-handlebars-template">
<h2>{{name}} ({{age}})</h2>
</script>

Luego de hacer esto, creamos el modelo que vamos a usar

var Person = Backbone.Model.extend({
});

Creamos la vista

var PersonView = Backbone.View.extend({
render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());

$(this.el).html(html);

return this;
}
});

Esta vista de ejemplo simplemente actuará como tal en su forma mas pura y no pondremos ninguna asociacion de eventos para mostrar como funciona.

Las vistas tienen por defecto algunos atributos como se dijo antes que pueden ser sobreasigandos (overriden) al extender o derivar la vista.

A saber, el elemento el, por defecto se refiere al elemento que contendra lo que se procese para mostrar en la vista. Es decir, el atributo el hace referencia al contenedor principal de la vista. Si no se define nada es el body. En nuestro caso definimos en nuestro documento HTML un contenedor con el id: personContainer.

<body>
<div id='personContainer'></div>
</body>

Entonces veremos como al crear mas adelante el objeto de la vista, le asignaremos el contenedor al elemento el. En principio, si seguimos analizando la vista veremos que la funcion render() que es la que concretamente procesa la vista en si, lo que hace es lo siguiente:

render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());
$(this.el).html(html);

return this;
}

En una variable source se asigna el html del contenedor con id personTemplate. Notese que este elemento es el definido en el template.

Ese elemento, ahora cargado en source, se le pasa al compilador de Handlebars lo cual devuelve un objeto de tipo template.

A dicho objeto que aqui esta cargado en la variable template, se le pasa por parametro el modelo (parseado a JSON ya que Handlebars solo espera una estrctura basica de JSON para sustituir los datos). Lo que hace el template engine es sustituir las ocurrencias de las variables por sus valores, a partir del modelo recibido.

Obviamente es importante que los nombres de las variables definidas en el template coincidan con el nombre de los atributos del modelo.

Finalmente, al HTML del contenedor de la vista (el), se le asigna el resultado del procesamiento del template.

Es importante siempre que la funcion render tenga un retorno del mismo objeto (return this) para poder realizar llamadas en cadena.

Por ultimo, analicemos que sucede cuando se carga la pagina:

$(document).ready(function(){
// Create new instances of the person models
var person = new Person({name: "Tim", age: 5});
// Create instances of the views
var personView = new PersonView({
model: person
});

personView.el = $('#personContainer'),

personView.render();
});

Se crea un objeto del modelo al cual le cargamos valores a los atributos. Luego creamos la vista a la que le pasamos con que modelo trabajara (en nuestro caso es Person).

Finalmente, asignamos al elemento el de la vista el contenedor donde se mostraran los resultados al realizar el render de la vista.

Y por ultimo, invocamos a la funcion render de la vista que ya vimos que usa el template engine para realizar las sustituciones pertinentes y asignar el resultado al html del contenedor.

Al finalizar el render, se agrega el resultado al DOM y vemos los datos en pantalla.

Tim (5)

Eventos en vistas

Ya vimos como crear un modelo, como hacer para mostrar una vista a través de un template. Ahora vamos a ver como interactuar con el DOM, trabajando con la asignacion de eventos.

Backbone desacopla el evento del selector y la funcion a invocar. Esto hace que sea sumamente practico para modelar cualquier escenario. ¿Que queremos decir con desacoplar? Veamos el siguiente ejemplo:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Backbone Test</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-responsive.min.css">
<script src="js/jquery.min.js"></script>
<script src="js/handlebars.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.min.js"></script>

<!-- Setup our templates -->
<script id="personTemplate" type="text/x-handlebars-template">
<h2>{{name}} ({{age}})</h2>
</script>

<!-- End templates setup -->
</head>
<body>
<div>
<div id='personContainer'>
<a href="#" id="boton">Show</a>
</div>
</div>
<script>
// Stub out the person model
var Person = Backbone.Model.extend({

});

// Define the view for a single person
var PersonView = Backbone.View.extend({

el: $("#personContainer"),

events: {
'click #boton' : 'mostrar'
},

render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());

$(this.el).append(html);

return this;
},

mostrar: function() {
this.render();
}
});

$(document).ready(function(){
// Create new instances of the person models
var person = new Person({name: "Tim", age: 5});

// Create instances of the views
var personView = new PersonView({
model: person
});
});
</script>
</body>
</html>

Primeramente en las aplicaciones con Backbone es bueno siempre hacer todas nuestras llamada javascript al final del documento HTML.

En el caso de este ejemplo lo que hicimos fue, dentro del contenedor que definimos en la vista (es decir, dentro del contenedor que asociamos a la variable el de la vista), agregamos un boton con llamado boton.

<div>
<div id='personContainer'>
<a href="#" id="boton">Show</a>
</div>
</div>

Luego, cambiamos un poco la logica de nuestra vista. Especificamos dentro de la vista cual sera el contenedor del atributo el. Cambiamos la funcion render para que agregue el procesamiento del template a lo que ya tenga el HTML del contenedor. De esta forma no perdemos el boton que definimos dentro del contenedor.

Agregamos entonces una funcion llamada mostrar a la vista. Esta funcion es la que al ejecutarse llama al render de la vista.

Y por ultimo y muy importante, agregamos events, donde lo que hacemos es asociar a un evento (en este caso el evento click), un selector (boton) y una accion (mostrar).

La estructura es

‘evento selector(es)’ : accion’

Veamos el codigo como quedo:

  // Define the view for a single person
var PersonView = Backbone.View.extend({
el: $("#personContainer"),

events: {
'click #boton' : 'mostrar'
},

render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());

$(this.el).append(html);

return this;
},

mostrar: function() {
this.render();
}
});

IMPORTANTE: Un detalle muy importante de esta asociacion o binding de eventos ya acciones es que suceden dentro del espacio definido por el atributo el. Este detalle no esta explicado en la mayoria de las documentaciones existentes y se toma como algo obvio pero si el boton por ej se colocara por fuera del contenedor container-narrow, no funcionaria.

Es muy importante saber que cada vista atiende a lo que sucede dentro de su contexto. Ni que hablar que podemos crear mas vistas y encerrar vistas dentro de vistas en una aplicación de mayor porte pero debemos tener claro este punto.

Colecciones

Backbone, ademas de contar con modelos, cuenta con colecciones que estan disponibles y exponen una serie de métodos para su tratamiento.

Esta clase en Backbone cuenta con algunas características interesantes. En primer lugar generalmente a la colección se le asocia un modelo que es el que se almacena. Pero cuenta con una serie de eventos que son escuchables y asociables a acciones.

Básicamente lo que hace es escuchar por cambios en la estructura, ante la inserción o remosión de algún elemento y ante ello ejecutar alguna acción o método asociado.

Las funciones utilitarias para trabajar con las colecciones Backbone las saca principalmente de Underscore pero se pueden usar otras bibliotecas como lodash.

Cualquiera de los eventos que se disparen en el model de la colección serán directamente disparados en la colección, por conveniencia. Esto permite escuchar por cambios de atributos específicos en cualquier modelo de la colección. Ej. document.on(“change:selected”, …).

En el siguiente ejemplo vamos a ver paso a paso como cargar elementos en una colección a través de un formulario simple usando estos conceptos de escuchar por cambios en la misma.

El resultado va a ser algo asi

La idea es que se ingresa un nombre y una edad y a medida que va creciendo la coleccion se actualiza la lista que se despliega debajo con los valores agregados.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Backbone Test</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-responsive.min.css">
<script src="js/jquery.min.js"></script>
<script src="js/handlebars.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.min.js"></script>

<!-- Setup our templates -->
<script id="personTemplate" type="text/x-handlebars-template">
<ul>
{{#each personas }}
<li>{{this.nombre}} - {{this.edad}}</li>
{{/each}}
</ul>
</script>

<!-- End templates setup -->
</head>
<body>
<div>
<form>
<fieldset>
<label>Nombre</label>
<input type="text" id="nombre">
<label>Edad</label>
<input type="text" id="edad">
<button type="submit">Agregar</button>
</fieldset>
</form>
<div id='personas-container'></div>
</div>
<script>

//Objeto de la aplicacion
var app = {};

//Modelo persona
app.Persona = Backbone.Model.extend({
defaults: {
nombre: "",
edad: 0
}
});

//Coleccion de personas
var Personas = Backbone.Collection.extend({
model: app.Persona,
});

//Creo la instancia de la coleccion
app.Personas = new  Personas();

//Vista de persona
app.PersonaView = Backbone.View.extend({

el: $("#personas-container"),

render: function(){
var js = app.Personas.toJSON();
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template({personas: js});

$(this.el).append(html);
return this;
}
});

// Vista de personas
app.PersonasView = Backbone.View.extend({

el: $(".container-narrow"),

events: {
'click .btn' : 'crear'
},

initialize: function(){
this.listenTo(app.Personas, 'add', this.refrescar);
},

crear: function(e){
event.preventDefault();
app.Personas.add([new app.Persona({nombre: $('#nombre').val(), edad: $('#edad').val()})]);
$('#nombre').val('');
$('#edad').val('');
},

refrescar: function() {
$('#personas-container').html('');
var personaView = new app.PersonaView();
personaView.render();
}
});

//Creo la vista principal
var personasView = new app.PersonasView();
</script>
</body>
</html>

Antes de nada debemos aclarar que este es un ejemplo donde colocamos todo el codigo en una sóla página, a efectos de ver como funciona. Más adelante introduciremos otros conceptos que tienen que ver con al modularidad y ahí se verán otras herramientas.

Vamos a analizar el codigo paso a paso.

Primero que nada debemos entender que el orden de los elementos es fundamental. Primero que nada en el header como siempre cargamos las bibliotecas que se van a usar que son jquery, backbone, underscore y handlebars.

Luego definimos el o los templates que vamos a usar. En este caso el template contiene un comando de handlebars para recorrer una colección y mostrar todos sus elementos.

<script id="personTemplate" type="text/x-handlebars-template">
<ul>
{{#each personas }}
<li>{{this.nombre}} - {{this.edad}}</li>
{{/each}}
</ul>
</script>

En este template vemos que al each le pasamos una colección y para hacer referencia a cada elemento de la colección usamos la palabra this.

Luego de esta definición, comienza el codigo js que empieza por definir un objeto global al que le asignaremos cada vista y colección. Esta definición de un objeto que contenga la aplicación es una buena práctica que se ve en la mayoría de las soluciones y que incluso ayudará mucho a unificar luego cuando haya que modularizar la aplicación en diferentes archivos.

Después de definir la aplicación, creamos el modelo de Persona al que le creamos valores por defecto. Crear valores por defecto es una buena práctica en los modelos para evitar atributos undefined luego.

//Objeto de la aplicacion
var app = {};
//Modelo persona
app.Persona = Backbone.Model.extend({
defaults: {
nombre: "",
edad: 0
}
});

Luego creamos la colección, donde indicamos que su modelo es app.Persona. Aquí es importante notar que el modelo lo hacemos ser parte de nuestro objeto aplicacion. Pero la colección la definimos por fuera y luego a una variable de nuestro objeto aplicacion le asignamos una nueva instancia de dicha colección.

Nótese que inmediatamente de definir la colección, ya creamos una instancia y la dejamos disponible en la aplicación.

var Personas = Backbone.Collection.extend({
model: app.Persona,
});
//Creo la instancia de la coleccion
app.Personas = new  Personas();

Luego empezamos a trabajar con las vistas. Vamos a tener 2 vistas en el ejemplo. La primera es la que va a resolver el procesamiento y muestra de la colección.

La otra vista corresponde a la aplicación general es decir al elemento del DOM que contiene el formulario y la otra vista con los resultados.

Es muy importante saber que Backbone mantiene un definido scope de acción, esto quiere decir que en una vista escuchamos un evento de un elemento determinado para que se dispare una acción, dicho elemento deberá estar dentro del el de la vista para que sea escuchado.

Habiendo dicho esto, veamos como estan definidas las vistas.

La primera es la vista de la persona, donde populamos el template con la colección y renderizamos la misma.

//Vista de persona
app.PersonaView = Backbone.View.extend({
el: $("#personas-container"),

render: function(){
var js = app.Personas.toJSON();
var source = $('#personTemplate').html();
var template = Handlebars.compile(source);
var html = template({personas: js});

$(this.el).append(html);
return this;
}
});

Para desacoplar lo que se hace es que la vista concretamente lo que hace es cuando se crea y se invoca su renderización convierte la colección a JSON, asigna el template, llama al template engine con la fuente (handlebars) y le pasa una variable a la que se le asigna la colección. Dicha variable es la que se hace referencia dentro del propio codigo de Handlebars en el template definido previamente.

Por ultimo, se asigna el html resultante al elemento contenedor de la vista.

La otra vista es el contenedor principal y dado que como dijimos Backbone usa las vistas como controller tambien, es aqui donde esta la logica principal. En toda aplicación Backbone siempre hay una vista que es la principal.

 // Vista de personas
app.PersonasView = Backbone.View.extend({
el: $(".container-narrow"),

events: {
'click .btn' : 'crear'
},

initialize: function(){
this.listenTo(app.Personas, 'add', this.refrescar);
},

crear: function(e){
event.preventDefault();
app.Personas.add([new app.Persona({nombre: $('#nombre').val(), edad: $('#edad').val()})]);
$('#nombre').val('');
$('#edad').val('');
},

refrescar: function() {
$('#personas-container').html('');
var personaView = new app.PersonaView();
personaView.render();
}
});

La vista principal en nuestro caso se llama PersonasView. Esta vista lo primero que hace es asignar al el el contendor que la va a contener. En este caso se llama container-narrow.

Luego en la inicializacion de la vista lo que se hace es hacer un binding, esto es indicar que se desea escuchar cambios en la colección de personas perviamente creada. Entonces lo que va a suceder es que cuando se produzca algún cambio en la colección, (en este caso agregar un objeto), se disparará la accion refrescar.

También se asocian eventos,

events: {
'click .btn' : 'crear'
},

Entonces aqui se dice que al hacer click en el boton con la clase .btn se ejecuta el método crear.

Analicemos el método crear:

crear: function(e){
event.preventDefault();
app.Personas.add([new app.Persona({nombre: $('#nombre').val(), edad: $('#edad').val()})]);
$('#nombre').val('');
$('#edad').val('');
},

Lo primero es evitar que el botón haga lo que hace por defecto, es decir submit del form. Luego agregamos un objeto nuevo Persona a la colección, pasándole como nombre el valor del input nombre y como edad el valor del input edad del formulario.

Posteriormente, vaciamos los campos para un próximo ingreso.

Recordemos que previamente en el código de la vista agregamos un escuchador (listenTo) o sea que cuando se agregue algo a la colección se disparará el método refrescar.

refrescar: function() {
$('#personas-container').html('');
var personaView = new app.PersonaView();
personaView.render();
}

El método refrescar lo que hace es vaciar el contenedor donde se muestra la colección. Crea una nueva instancia de la vista y la renderiza. Esto muestra los elementos de la colección en pantalla.

Por último, es muy importante la creación del objeto de la vista principal, para que se dispare todo.

//Creo la vista principal
var personasView = new app.PersonasView();

Persistencia local (local storage)

Con el advenimiento de HTML5 se ha hecho popular el uso de almacenes locales de datos dentro del navegador para la persistencia de información recabada en la página.

Anteriormente se utilizaban cookies para persistir datos del lado del cliente pero la mismas tenian varias limitantes ya sabidas por todos. El concepto de local storage revolucionó la industria y vamos a ver siguiendo el ejemplo anterior como hacemos para que la lista de personas se mantenga aun cuando refrescamos el navegador.

Más adelante veremos como persistir la información con un servidor.

Para utilizar los repositorios locales vamos a hacer uso de una extensión de backbone que integra fácilmente los repositorios locales con el producto: backbone.localstorage.js

En este ejercicio lo que vamos a ver es como siguiendo con el ejemplo anterior, vamos cargando la colección pero cada vez que carguemos los datos en la colección, grabaremos dicha información en un repositorio local. De esta manera veamos los lugares donde agregamos código para que cuando recarguemos la página, si la colección tiene datos, la muestre.

//Coleccion de personas
var Personas = Backbone.Collection.extend({
model: app.Persona,

localStorage: new Backbone.LocalStorage("coleccion-personas"),
});

//Creo la instancia de la coleccion
app.Personas = new  Personas();
app.Personas.fetch();

Vemos que la colección es la misma de siempre pero en esta ocasión lo que hacemos es agregar un atributo llamado localStorage al cual le asignamos una instancia de la clase backbone.LocalStorage, a la cual identificamos con un nombre único: coleccion-personas.

El otro agregado es que luego de crear la colección, ejecutamos el comando fetch para que cargue los datos que puedan existir de persistencias anteriores en el repositorio.

Finalmente veamos que sucede cuando cargamos la colección y cuando inicializamos la vista principal.

// Vista de personas
app.PersonasView = Backbone.View.extend({
el: $(".container-narrow"),

events: {
'click .btn' : 'crear'
},

initialize: function(){
this.listenTo(app.Personas, 'add', this.refrescar);
this.refrescar();
},

crear: function(e){
event.preventDefault();
var persona = new app.Persona({nombre: $('#nombre').val(), edad: $('#edad').val()});
app.Personas.add([persona]);
persona.save();

$('#nombre').val('');
$('#edad').val('');
},

refrescar: function() {
$('#personas-container').html('');
var personaView = new app.PersonaView();
personaView.render();
}
});

Nótese que lo que hacemos es ejecutar el refresh para que renderice la vista que tiene la colección de personas, justo al inicializar la vista principal.

Posteriormente, agregamos la opcion person.save() luego de cada carga de objetos a la colección. Este paso es fundamental para poder persistir los datos en el repositorio local.

En este ejemplo asumimos que estamos trabajando con navegadores que soportan HTML5 y por tanto el local storage funciona. El problema es cuando queremos utilizar el mismo concepto de almacenamiento local en navegadores que no soporten esta nueva tecnología. Para ello es que se estila trabajar con bibliotecas que realicen lo que se llama “degradación”, es decir ante situaciones donde no se pueda cumplir con lo deseado, la aplicación resuelva como persistir la información.

Aquí entra en juega Lawnchair.

Haciendo uso del mismo ejemplo, veremos como integrar un adaptador llamado backbone.lawchair que hace uso de esta fantástica biblioteca integrándola con backbone.

Lo que hacemos en este caso es simplemente es bajar el proyecto de github y agregar la referencias a nuestro HTML en el siguiente orden:

<script type="text/javascript" src="js/json2.js"></script>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/bootstrap-min.js"></script>
<script type="text/javascript" src="js/handlebars.js"></script>
<script type="text/javascript" src="js/underscore.js"></script>
<script type="text/javascript" src="js/backbone.js"></script>
<script type="text/javascript" src="js/lawnchair.js"></script>
<script type="text/javascript" src="js/lawnchair-adapter-ie-userdata.js"></script>
<script type="text/javascript" src="js/backbone.lawnchair.js"></script>

Luego debemos cambiar la linea en nuestra colección, donde hacíamos referencia al store básico, para utilizar lawnchair en su lugar.

var Personas = Backbone.Collection.extend({
model: app.Persona,
lawnchair: new Lawnchair({ name: "coleccion-personas"}, new Function()),
});

El resto lo dejamos igual. Si lo probamos en el mismo navegador (Chrome por ejemplo que soporta local storage), veremos que funciona correctamente. La otra prueba es ejecutar lo mismo en un navegador como IE 8.

NOTA: Antes de probar en IE8, habria que hacer el siguiente ajuste adicional, ya que IE 8 no soporta la función event.preventDefault().

Entonces en la vista principal de personas realizamos el siguiente cambio

crear: function(e){
//event.preventDefault();
var persona = new app.Persona({nombre: $('#nombre').val(), edad: $('#edad').val()});
app.Personas.add([persona]);
persona.save();
$('#nombre').val('');
$('#edad').val('');
return false;
},

Esto es importante ya que evita que se dispare el evento submit al presionar el botón del formulario.

Rutas

Las aplicaciones web a menudo proveen de URLs enlazables y que pueden ser agregadas como favoritos para determinadas secciones de la aplicación. Imaginemos por ejemplo resultados paginados donde querríamos ir directamente a la página 4. Históricamente para emular esto dentro de la página se usaron hash tags (#) con anclas de HTML, pero con el advenimiento de la API History ahora es posible usar los clásicos /page para marcar las rutas.

Backbone.router hace uso de esto y provee de métodos para rutear páginas del cliente a efectos de conectarlas con acciones y eventos. Para aquellos navegadores que no soporten dicha API, el manejador de rutas degrada a una versión fragmentada de URLs.

Durante la carga de la página, luego que la aplicación haya finalizado de crear todas sus rutas, hay que asegurarse de llamar Backbone.history.start() or Backbone.history.start({pushState: true}) para rutear al URL inicial.

Veamos como hacemos en el nuestro ejemplo si quisieramos agregar rutas para que dado lo que se agregue a la URL sea la acción que se dispara:

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

help: function() {
console.log("help");
},

search: function(query, page) {
console.log("Query");
console.log("query " + query + "page " + page);
}

});

var workspace = new app.Workspace;

Backbone.history.start();

Es muy importante colocar la sentencia última de Backbone.history.start() cuando usamos rutas.

Aparte de eso básicamente la idea es que definimos nuestras rutas y establecemos que sucede cuando se ejecute la ruta, por ej.: http://localhost/#help