Después de la demo de Webapp en AngularJS y de la demo de Backbone vamos a hacer una pequeña introducción a un framework JavaScript relativamente nuevo y que tiene un nivel de hype altísimo debido a algunas características realmente interesantes y potentes: Ember.js
En el momento de escribir este post el framework Ember se encuentra ya en versión 1.0 Release Candidate y cada vez más cerca de una versión estable. Por desgracia, la información y ejemplos de código actualizados que se pueden encontrar sobre este framework son escasos, así que nos parece que este ejemplo puede resultar muy útil para alguien que quiera empezar con Ember a través de un ejemplo de App sencilla (pero funcional).
El proyecto
Hemos decidido modificar un poco el funcionamiento de la App para mostrar un poco de variación con ejemplos anteriores y aprovechar además algunas de las características más interesantes de Ember.
Por tanto, para esta iteración de la App vamos a implementar una interfaz Master-Detail, esquema muy común en aplicaciones software tradicionales (de escritorio)
Maste-Detail en iTunes
En esta App la vista Master mostrará una imágen reducida y tan solo los datos más importantes (en este caso el título y el autor del libro) y hará los efectos de menú de selección, mientras que la vista Detail será la ficha completa de datos del libro. Al igual que en las otras versiones, podemos filtrar por autor o por título del libro, y reordenar la vista Master.
Proyecto: listado de libros y acceso a su ficha
http://lostiemposcambian.com/blog/posts/ember-js/
The Ember Way
El framework Ember.js representa un cambio de paradigma con respecto a otros frameworks ya que para funcionar correctamente nos exige que respetemos totalmente una convenciones de nomenclatura (Naming conventions) a la hora de nombrar a cada uno de los objetos de la App. Para nuestro caso de objetos libro, por ejemplo, deberíamos usar los siguientes convenciones:
-
Router: App.LibroRoute
-
Controlador: App.LibroController
-
Modelo: App.Libro
-
Vista: App.LibroView
-
Template de la vista: libro
Si nuestros objetos, ya sean modelos, vistas o controladores se comportan de manera que el framework espera y hemos respetado estas convenciones, Ember.js se encargará de generar dinámicamente en memoria estos objetos cuando sean necesarios, técnica que se conoce como generación implicita de código. De esta forma no nos tenemos que preocupar de programar su código, ni tampoco de crearlos ni de instanciarlos.
Si lo pensamos tiene sentido, ya si un modelo se llama “Libro” es lógico que su vista asociada sea “LibroView” y su controlador “LibroController”. Además en una inmensa mayoría de casos el Controlador de un Modelo compuesto por un array de datos va a querer utilizar métodos de array y poco más, mientras que un controlador cuyo modelo sea un único objeto va a requerir diferentes métodos. Así que, ¿por qué no dotar al framework de un contexto suficiente para que genere estos controladores, modelos, vistas etc y su comportamiento implícitamente?
Este conocimiento del contexto el framework lo va a tener siempre que nosotros respetemos las convenciones aceptadas, característica que en inglés normalmente es referida como Convention over Configuration (convenciones más que configuraciones, por traducirlo de forma aproximada). Por esto, en Ember podemos conseguir muchísimas cosas programando muy poco código. Por supuesto, siempre tenemos la posibilidad de alterar este comportamiento por defecto implementando nosotros mismos cualquiera de estos objetos.
El Routing
Una de las características que más me han gustado del framework Ember.js es la posibilidad de definir rutas anidadas (o más bien resources, que son rutas que comparten un mismo recurso). En palabras de Yehuda Katz, uno de los creadores de Ember:
“If your user interface is nested, your routes should be nested”
En el caso de nuestra App, al ser tan sencilla tan solo tenemos una ruta anidada (el Detail dentro del Master) que hemos definido así:
App.Router.map(function()
{
this.resource('libros', function()
{
this.resource('libro', {path: ':libro_id'});
}); // ruta: '/#/libros/:libro_id'
});
Lo cual genera las siguientes rutas:
- index (hemos hecho que por defecto se redirija a libros)
- libros (master o lista principal de libros)
- libros/:libro_id (detail, ficha individual de un libro con identificador :libro_id)
Por poner un ejemplo con más chicha, un posible mapeo de rutas sacado de la documentación de Ember:
App.Router.map(function() {
this.resource('post', { path: '/post/:post_id' }, function() {
this.route('edit');
this.resource('comments', function() {
this.route('new');
});
});
});
Generaría las siguientes rutas:
- / index o home de la App
- /post/:post_id
- /post/:post_id/edit
- /post/:post_id/comments
- /post/:post_id/comments/new
Así como los objetos Route y Controller y View asociados a cada una de estas rutas. Como hemos comentado antes, si estos objetos van a tener un comportamiento por defecto, no tenemos que preocuparnos para nada de ellos ya que se generarán dinámicamente en memoria cuando se necesiten. Más cómodo y rápido, imposible.
Controladores
Una única página puede (y debería) tener varios controladores activos a la vez. Estos controladores se encargarán de actuar según las acciones del usuario sobre sus respectivas vistas, y de pasarles los modelos de datos necesarios para que estas se rendericen y se mantengan actualizadas. Ember instancia todos los controladores al crear la App y mientras que ésta permanezca abierta en el browser estos objetos controlador permanecen en memoria, lo que cambiarán serán los datos que manejen y las vistas que se muestren.
Vamos a ver el código del controlador de la lista principal de objetos libro de la App (LibrosController), con el comportamiento añadido para buscar por autor/título de libro y para la ordenación del array (modelo de datos). Nótese que si no hubiésemos necesitado estas funcionalidades, no hubiésemos ni tenido que declarar este controlador, ¡ya que Ember es capaz de entender por el contexto que LibrosController es un controlador con funciones de array!
//Controlador de los libros, con funciones para filtrar/ordenar modelo de datos
App.LibrosController = Ember.ArrayController.extend(
{
originalContent: [],
sortProperties: ['titulo'],
sortAscending: true,
sortBy: function(sortField, sortOrder )
{
this.set('sortProperties',[sortField]);
this.set('sortAscending',(sortOrder ==='asc'));
},
filterBy: function(letters)
{
if(letters === "")
{
this.set('content',this.get('originalContent'));
return;
}
if(this.get('originalContent').length===0)
this.set('originalContent',this.get('content'));
//filtramos por regexp, i flag para ignore case (no distinguir lowercase/uppercase)
var pattern = new RegExp(letters,'i');
var newArray = this.get('originalContent').filter(function(item)
{
return ( pattern.test(item.autor) || pattern.test(item.titulo) );
});
this.set('content',newArray);
}
});
De hecho, no busquéis en el código el controlador LibroController, ya que no está definido y es generado dinámicamente por el framework 🙂
Las Vistas
Ember está muy integrado con el motor de plantillas Handlebars, que es una extensión sobre Mustache (que ya comentamos en cuando hablamos del framework Backbone) totalmente compatible y que añada un montón de helpers realmente útiles como por ejemplo el indispensable {{partial}} para definir vistas parciales e incluirlas posteriormente en otros templates.
Por ejemplo, el template asociado a nuestra vista del array de libros sería:
<!-- Template para la vista libros -->
<script type="text/x-handlebars" data-template-name="libros">
<div>
<div class="menu">
<h2>Libros</h2>
{{ partial "ui"}}
<div id="libros">
{{ partial "librosList"}}
</div>
</div>
<div class="content">
{{ outlet }}
</div>
</div>
</script>
Existen varias maneras de renderizar las vistas dentro de un template, por ejemplo algunas de las más utilizadas son:
-
{{outlet}}: Por defecto, renderiza con la vista asociada al controlador actual
-
{{render “nombreTemplate”}}: renderiza un template asociado a un objeto vista con los datos del modelo del controlador de dicha vista.
-
{{view objectView}}: renderiza un objeto vista, que puede tener un template asociado o generarlo dinámicamente a partir de los datos de su controlador.
El Modelo
Sin lugar a dudas la parte que más quebraderos de cabeza nos ha dado incluso para este ejemplo sencillo ha sido el modelo de datos, o más bien la capa de cargado (y guardado, aunque en esta App no la llegamos a utilizar) de los mismos. Resulta que Ember viene con una capa de persistencia datos llamada Ember Data que, por decirlo suavemente, no está del todo terminada ni documentada. Además (al igual que todo el framework) ha sufrido muchas modificiaciones en los últimos tiempos así que la poca información que se encuentra, en muchos casos está totalmente outdated y resulta inservible.
// Models
App.Store = DS.Store.extend({
revision: 11//,
//FixtureAdapter se utilizará para cargar datos estáticos,
//x ej para pruebas en entorno de desarrollo
//adapter: 'DS.FixtureAdapter'
});
//definimos la ruta donde se cargarán los datos
DS.RESTAdapter.reopen({
namespace: 'blog/posts/ember-js/data'
});
//Modelo de datos de Libro
App.Libro = DS.Model.extend({
autor: DS.attr('string'),
titulo: DS.attr('string'),
editorial: DS.attr('string'),
img: DS.attr('string'),
descripcion: DS.attr('string')
});
</p>
No está de más clarificar que el framework Ember.js no tiene ninguna dependencia con Ember Data y somos libres de usar cualquier método que queramos para cargar datos del servidor y almacenarlos en nuestro modelo, incluso llamadas $.ajax “a pelo”. Sin embargo, nos parece que una parte tan crítica de una App como es la carga y guardado de datos desde el servidor debería estar más integrada y funcionar sin ningún tipo de fricción dentro del framework.
Por suerte, los creadores del framework son muy conscientes de estas limitaciones y creemos que habrá cambios y mejoras en este aspecto antes de la versión estable de Ember.
En el caso de esta App, la carga de datos inicial se realiza mendiante bootstrapping, técnica muy común en las aplicaciones JavaScript para ahorrar una llamada al servidor generando los datos iniciales dentro de una etiqueta script y recuperándolos al iniciar la aplicación. Para la carga individual de los datos de cada uno de los libros para la ficha se utiliza el comportamiento por defecto del controlador, por lo que el código no está en nuestra App al ser generado dinámicante de manera implícita por Ember 😉
Aprendiendo más sobre Ember.js
Por desgracia, como ya comentamos en la introducción, una de las peores características de Ember es la poca información y recursos para aprender y practicar que se pueden encontrar hoy en día actualizados y de forma gratuita. Ember tiene una curva de aprendizaje un tanto abrupta ya que es indispensable que respetemos todas las convenciones y el flujo de eventos del framework para aprovechar toda su potencia, por lo que inicialmente los primeros pasos pueden ser muy frustrantes.
Precisamente por el motivo de no toparnos con información no actualizada, vamos a recomendar dos recursos «oficiales», primero las guías dentro de site oficial de Ember.js que empieza desde un nivel muy básico a nivel conceptual y poco a poco van metiéndose en código más complejo. Una buena referencia que tener siempre a mano junto con la API oficial cuando estamos programando en Ember.
Una buena forma también de acceder a información actualizada es preguntarle directamente a la comunidad de desarrolladores, y por suerte existen dos opciones geniales para Ember:
- Stackoverflow, donde la etiqueta ember.js para preguntas sobre el framework Ember suele ser bastante activa y generalmente podemos encontrar respuestas a las preguntas por parte de muchos de los contribuidores al framework en Github
- El foro oficial de desarrolladores de Ember.js (que por cierto está basado en una app para creación de foros genial y además programada en Ember, discourse, que dará que hablar en el futuro, os lo aseguramos!) donde uno de los tópicos más frecuentes suele ser precisamente cómo ayudar a los primerizos a entender los conceptos más complejos del desarrollo en Ember, así como las best practices y los casos de uso del framework.
Maste-Detail en iTunes
http://lostiemposcambian.com/blog/posts/ember-js/
Router: App.LibroRoute
Controlador: App.LibroController
Modelo: App.Libro
Vista: App.LibroView
Template de la vista: libro
“If your user interface is nested, your routes should be nested”
{{outlet}}: Por defecto, renderiza con la vista asociada al controlador actual
{{render “nombreTemplate”}}: renderiza un template asociado a un objeto vista con los datos del modelo del controlador de dicha vista.
{{view objectView}}: renderiza un objeto vista, que puede tener un template asociado o generarlo dinámicamente a partir de los datos de su controlador.
Muchas gracias , todo muuy bien explicado. Ya conozco ember.js bastante bien con todas sus características pero estoy empezando con la librería ember-data
que de verdad esta cambiando demasiado rápido.
100% de acuerdo. Sin duda, el principal problema de Ember.js hoy en día es el no tener una versión estable de ember-data. Como comentamos en este artículo, nos parece que es una parte lo suficientemente crítica de toda App como para que esté perfectamente integrada dentro del framework.
Como puedo agregar mas libros
¡Muy buen aporte! Muy bien explicado, me ha ayudado con los conceptos básicos.
¡Muchas gracias y enhorabuena!