
El ejemplo consiste en una lista de tareas, pudiéndose introducir nuevas tareas y marcarlas como realizadas. También se podrá eliminar de la lista las tareas completadas y ver un resumen con el número de tareas completadas y de tareas totales.
En la parte cliente de la aplicación se hace uso de RequireJS para manejar la carga de los archivos javascript necesarios, de backbone para gestionar la lista de tareas y realizar la comunicación con el servicio REST del servidor y de Mustache como motor de plantillas para generar la vista (el html). En la parte del servidor usa Tapestry como framework web y RESTEasy como librería para implementar un servicio REST que proporcionará la persistencia en memoria de la lista de tareas.
Backbone permite organizar el código aplicando el patrón de diseño MVC (modelo-vista-controlador) ampliamente extendido en los frameworks web. Este patrón de diseño tiene la ventaja que separa la aplicación en tres partes diferenciadas:
- El modelo: que contiene los datos del dominio que maneja la aplicación que cuando es modificado lanza eventos para que el controlador y la vista actúen en consecuencia.
- El controlador: que reacciona ante los eventos que se produzcan en el modelo o por el usuario y modifica adecuadamente el modelo o la vista según el evento.
- La vista: que a partir del modelo produce la interfaz que se ofrece al usuario para que pueda manipularlos, los eventos producidos en la vista son manejados por el controlador.
En una aplicación con la combinación de Backbone, Mustache y un servicio REST el servidor devuelve el html de la página inicial, las plantillas y los datos del servidor y se delega en el navegador del usuario la tarea de renderizar el html final. Esto tiene la ventaja de que entre el navegador y el servidor viajan menos datos (los datos y las plantillas tendrán menos tamaño que los datos formateados a html) y la parte del servidor se simplifica (no es necesaria la lógica para formatear a html los datos que dependiendo del framework hacen los jsp, tml de tapestry o gsp de grails). Aunque el código de la parte cliente crecerá, el código de la parte del servidor se hará más simple. Una vez cargada la página inicial entre el servidor y el cliente solo viajan los datos en formato json.
Veamos el ejemplo a continuación empezando por el modelo. El modelo con Backbone se define con Backbone.Model.extend, donde podremos indicar las propiedades de los objetos, también podemos definir funciones de utilidad. En este caso el modelo estará formado por los objetos Tarea que tendrán las siguientes propiedades:
- Un id que identificará la tarea.
- La descripción de la misma.
- Y un indicador de si está completada.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Tarea = Backbone.Model.extend({ | |
urlRoot : 'rest/tareas/tarea', | |
defaults: { | |
id: null, | |
descripcion: '', | |
completada: false | |
}, | |
toPlantilla: function() { | |
var json = this.toJSON(); | |
json.attrs = { | |
checked: (this.get('completada')?'checked':null), | |
completada: (this.get('completada')?'completada':null) | |
}; | |
return json; | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Tareas = Backbone.Collection.extend({ | |
url: 'rest/tareas', | |
model: Tarea, | |
findCompletadas: function() { | |
return this.models.filter(function(o) { | |
return o.get('completada'); | |
}); | |
}, | |
removeCompletadas: function() { | |
_.each(this.findCompletadas(), function(o) { | |
o.destroy(); | |
}); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.todo.rest; | |
import java.util.Collection; | |
import javax.ws.rs.Consumes; | |
import javax.ws.rs.DELETE; | |
import javax.ws.rs.GET; | |
import javax.ws.rs.POST; | |
import javax.ws.rs.PUT; | |
import javax.ws.rs.Path; | |
import javax.ws.rs.PathParam; | |
import javax.ws.rs.Produces; | |
import javax.ws.rs.core.MediaType; | |
@Path("/tareas") | |
public interface TareasResource { | |
@Path("/tarea") | |
@POST | |
@Consumes(MediaType.APPLICATION_JSON) | |
@Produces(MediaType.APPLICATION_JSON) | |
public Tarea createTarea(Tarea tarea); | |
@Path("/") | |
@GET | |
@Produces(MediaType.APPLICATION_JSON) | |
public Collection<Tarea> readTareas(); | |
@Path("/tarea/{id}") | |
@PUT | |
@Consumes(MediaType.APPLICATION_JSON) | |
@Produces(MediaType.APPLICATION_JSON) | |
public Tarea updateTarea(Tarea tarea); | |
@Path("/tarea/{id}") | |
@DELETE | |
public void deleteTarea(@PathParam("id") Long id); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var TareaView = Backbone.View.extend({ | |
tagName: 'li', | |
className: 'tarea', | |
events: { | |
"change input[name='completada']": 'onChangeCompletada' | |
}, | |
initialize: function() { | |
_.bindAll(this); | |
this.model.on('change', this.render); | |
this.model.on('remove', this.remove); | |
this.render(); | |
}, | |
render: function() { | |
var texto = render('#tarea-template', this.model.toPlantilla()); | |
$(this.el).html(texto); | |
}, | |
// Eventos | |
onChangeCompletada: function() { | |
var completada = $("input[name='completada']", this.el).is(':checked'); | |
this.model.set('completada', completada); | |
this.model.save(); | |
} | |
}); |
En el caso de TareasApp en el método initialize además se inserta en el elemento «el» que se le pasa en el constructor la vista inicial de la aplicación. De esta forma podríamos crear varias instancias de TareasApp.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var TareasApp = Backbone.View.extend({ | |
events: { | |
"keypress input[name='nuevaTarea']": 'onKeypressNuevaTarea', | |
"click input[name='limpiar']": 'onClickLimpiar' | |
}, | |
initialize: function() { | |
_.bindAll(this); | |
this.tareas = new Tareas(); | |
this.tareas.on('add', this.render); | |
this.tareas.on('remove', this.render); | |
this.tareas.on('change', this.render); | |
this.tareas.on('reset', this.reset); | |
var texto = render('#tareas-template', null); | |
$(this.el).html(texto); | |
this.render(); | |
}, | |
render: function() { | |
var completadas = this.tareas.findCompletadas().length; | |
var total = this.tareas.length; | |
// Habilitar/deshabilitar el botón de limpiar tareas completadas | |
if (completadas == 0) { | |
$("input[name='limpiar']", this.el).attr('disabled', 'disabled'); | |
} else { | |
$("input[name='limpiar']", this.el).removeAttr('disabled'); | |
} | |
// Cambiar el mensaje de estado de las tareas | |
var texto = render('#estado-template', {completadas: completadas, total: total}); | |
$('#estado', this.el).html(texto); | |
}, | |
reset: function() { | |
this.tareas.each(function(o) { | |
var vista = new TareaView({model: o}); | |
$('#lista-tareas', this.el).append(vista.el); | |
}, this); | |
this.render(); | |
}, | |
// Métodos | |
addTarea: function(tarea) { | |
tarea.save(); | |
this.tareas.add(tarea); | |
var vista = new TareaView({model: tarea}); | |
$('#lista-tareas', this.el).append(vista.el); | |
}, | |
resetTareas: function(tareas) { | |
this.tareas.reset(tareas); | |
}, | |
fetch: function() { | |
this.tareas.fetch(); | |
}, | |
// Eventos | |
onKeypressNuevaTarea: function(event) { | |
// Comprobar si la tecla pulsada es el return | |
if (event.which == 13) { | |
var input = $("input[name='nuevaTarea']", this.el); | |
var descripcion = input.val(); | |
descripcion = trim(descripcion); | |
// Comprobar si se ha introducido descripción de la tarea | |
if (descripcion == '') { | |
return; | |
} | |
// Añadir la tarea y limpiar el input | |
var tarea = new Tarea({ descripcion: descripcion, completada: false }); | |
this.addTarea(tarea); | |
input.val(''); | |
} | |
}, | |
onClickLimpiar: function() { | |
this.tareas.removeCompletadas(); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script id="tareas-template" type="text/template"> | |
<![CDATA[ | |
<input type="text" name="nuevaTarea" value="" placeholder="Introduce una nueva tarea"/> | |
<ul id="lista-tareas" class="unstyled"></ul> | |
<div id="estado"></div> | |
<input type="button" name="limpiar" value="Limpiar" class="btn"/> | |
]]> | |
</script> | |
<script id="tarea-template" type="text/template"> | |
<![CDATA[ | |
<label class="checkbox"> | |
<input id="{{id}}" type="checkbox" name="completada" {{attrs.checked}}/> <span class="{{attrs.completada}}">{{descripcion}}</span> | |
</label> | |
]]> | |
</script> | |
<script id="estado-template" type="text/template"> | |
<![CDATA[ | |
{{#total}} | |
{{completadas}} tareas de {{total}} completadas | |
{{/total}} | |
{{^total}} | |
¡Muy bien! has completado todas las tareas | |
{{/total}} | |
]]> | |
</script> |
![]() |
Petición al crear una nueva tarea |
![]() |
Petición al marcar como completada una tarea |
![]() |
Petición al limpiar tareas completadas |
Esta es una captura del ejemplo:
Después de haber usado Backbone en este ejemplo simplemente tengo que decir que es una gran herramienta y que facilita y ayuda a organizar el código javascript en gran medida. Permite separar los datos de la aplicación que forman el modelo del controlador y la vista que reaccionan mediante los eventos producidos en el modelo. También permite desarrollar aplicaciones con un gran peso de javascript en la parte cliente sin que el código se convierta posteriormente en un infierno de mantenimiento aún con la ayuda de jQuery. Es una ayuda tan grande para hacer algunas cosas de la interfaz del cliente como lo es jQuery para manipular los elementos html.
Algunas partes no las he explicado como las plantillas de Mustache, el uso de RequireJS o la parte del servidor del servicio RESTEasy ya que ya lo que he hecho en entradas anteriores que puedes visitarlas mediante sus enlaces.
Como en el resto de entradas el código fuente completo lo puedes encontrar en mi repositorio de GitHub. Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada. Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip con el anterior enlace.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ git clone git://github.com/picodotdev/elblogdepicodev.git | |
$ cd elblogdepicodev/BackboneREST | |
$ ./gradlew tomcatRun | |
# Abrir en el navegador http://localhost:8080/BackboneREST/Index |
Referencia:
Introducción y ejemplo de RequireJS
Introducción y ejemplo de Mustache
Logging en Javascript con log4javascript
Capturar errores de Javascript
Optimizar módulos de RequireJS y archivos Javascript
Patrón de diseño MVC del lado cliente con Backbone.js