Tutorial de Ember: Screencast paso a paso

danii . lunes 15 de abril de 2013. a las 09:21

Ember Video Tutorial

Cuando presentamos el framework JavaScript Ember.js ya comentamos que uno de los principales problemas de este framework era la dificultad de hallar buenos recursos de aprendizaje actualizados y por tanto los inicios eran (por decirlo suavemente) un tanto duros. Sin embargo, también advertimos de que los creadores del framework eran conscientes de ello y por tanto podíamos esperar interesantes mejoras y novedades al respecto. Pues bien, Tom Dale, uno de los autores del framework, se ha puesto manos a la obra y ha grabado un fantástico screencast de introducción a la programación de una Web App JavaScript en Ember.

En este screencast se programa «en directo» una mini aplicación de blogging con Ember.js partiendo prácticamente desde cero. En este post vamos a seguir el screencast paso a paso, comentando el código y las funcionalidades que se van añadiendo. Este tutorial es un ejemplo muy práctico, así que os recomendamos que intentéis seguir el video a la vez que programáis la App y podéis utilizar este post como guía de consulta adicional.

Ver Demo Finalizada

Como referencia, os mostramos el que será el resultado final de la aplicación que se construye en el screencast, más un par de adaptaciones y mejoras que hemos decidido añadir nosotros y que comentaremos durante el desarrollo:

ver demo

Os recomendamos que no “hagáis trampas” y echéis mano del código fuente que os proporcionamos y que intentéis seguir el screencast y nuestro step by step programando vosotros cada paso. La idea es aprender Ember siguiendo un ejemplo práctico relativamente sencillo.

ScreenCast

Archivos iniciales

Hemos preparado un zip Starting Point con los archivos que necesitamos para empezar con este tutorial. Hemos tomado como base el Starter Kit de Ember.js al que le hemos añadido los siguientes archivos que se van a utilizar para el desarrollo de la aplicación:

  • El ubicuo twitter bootstrap CSS para tener una base de estilos CSS agradable sin esfuerzo,

  • La genial librería moment.js para formatear fácilmente datetimes de JavaScript de forma friendly para personas humanas,

  • La librería showdown.js, para parsear texto en formato markdown y convertirlo a HTML, ya que utilizaremos este formato para almacenar nuestros posts en el modelo,

  • Y por último Ember data en su última versión Revision 12, ya que sorprendentemente no está incluído en el Starter Kit antes mencionado.

Además, hemos eliminado el código inicial de prueba con el que viene la app del Starter Kit y hemos dejado una única línea:

  App = Ember.Application.create();

Y tambien hemos substituido las templates iniciales del Starter Kit por la template inicial estática que se muestra en el video.

Descarga aquí el zip Starting Point, descomprímelo y abre el archivo index.html en el navegador (inicialmente no es necesario cargarlo desde un servidor web). Deberías encontrarte el menú de navegación superior y el resto de la pantalla en blanco, pero que todos los archivos necesarios están cargados. Este será nuestro punto de partida:

Starting point en el navegador

En este momento podemos empezar a apreciar la magia de Ember: no hemos programado más quen una línea de código de la aplicación, en la que simplemente la hemos inicializado, y hemos añadido una template sin ningún tipo de vinculación a este objeto JavaScript App, y sin embargo el framework Ember.js es lo suficientemente inteligente para saber que esta única plantilla que encuentra es la que tiene que renderizar.

Como se observa en el menú de navegación, nuestar App va a tener dos secciones o páginas principales:

  • Posts, que incluirá un listado de posts siguiendo un patrón Master-Detail. No es casualidad que nuestra app de intro a Ember y esta estén ambas basadas en Master-Detail, ya que el Router de Ember se adapta muy bien a las características de este patrón tan utilizado, como veremos más adelante.

  • About, una página con información estática donde presentaremos a los autores del blog, que en este caso vamos a personalizar con nuestras biografías 😉

La Sección About

En este primer paso vamos a centrarnos en la sección About. Primero, crearemos un template de Handlebars con el contenido estático de la sección (en este caso, una presentación del blog y unas mini biografias nuestras como autores) y la añadimos al HTML:

  <script type="text/x-handlebars" data-template-name="about">
    <div class="about">
      <h3>Sobre el blog</h3>
      Los tiempos cambian pero no nuestra suerte... o sí! En un mundo como el actual donde todo evoluciona tan rápido (especialmente la tecnología) y las <em>buzzwords</em> surgen y decaen casi a diario, es cada vez más necesario formarse día a día. Cada día.
      ...
    </div>

    <div class="about">
      <h3>Autores del blog</h3>
      <strong>Elad Rodriguez</strong>
      <a href="http://www.twitter.com/eladnts" target="_blank"> @eladnts</a>
      <ul>
        <li>Culé del 1982. Ingeniero Superior en Informática por la Universidad de Alicante (UA). Fundador de la empresa de desarrollo web <a href="http://www.nitsnets.com" target="_blank">nitsnets | studios</a> Director de proyectos web 2.0 y RIAs.</li>
        ...
      </ul>

      <strong>Daniel G. Nebot</strong>
      <a href="http://www.twitter.com/daniinebot" target="_blank"> @daniinebot</a>

      <ul>
        <li>También nacido en el 1982, aunque sensiblemente antes. Ingeniero Superior en Informática por la Universidad de Alicante (UA).</li>
        ...
      </ul>
    </div>  
  </script>

Nota: Aunque en el screencast se utiliza el atributo id=»about» para asociar el template a su ruta, nosotros hemos preferido utilizar el más explícito data-template-name=»about» ya que ambos tienen exactamente el mismo funcionamiento y vemos dos ventajas: primero, nos parece más explicativo y además, preferimos evitar un riesgo de colisiones con otros elementos del DOM.

Para que Ember muestre un template, tenemos que tener una ruta asociada a éste, así que vamos a a definir un objeto Route de Ember super simple, con una única ruta que representa esta sección:

  App.Router.map(function() {
    this.resource('about');
  });

Y aquí entra otra vez en juego la magia de Ember: al haber definido una ruta con nombre ‘about’ y un template con ese mismo nombre ‘about’, Ember ya sabe que esta ruta y este template están asociados, sin necesidad de que yo tenga que definir esta relación en escribiendo código.

Para mostrar este template, debemos hacer dos adiciones en nuestro template principal:

  • Sustituimos el link estático
      <a href="#">About</a>
    

    por un un link a la sección about recien creada en el Router utilizando el helper de Handlebars {{#linkTo}}:

      {{#linkTo 'about'}}About{{/linkTo}}
    
  • y añadimos también el helper {{outlet}} que podríamos definir como simplemente un placeholder que indica a Ember en qué punto de la plantilla actual debe renderizar el contenido de la sección.

De esta manera, el template principal queda de momento así:

 <script type="text/x-handlebars">
    <div class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="#">Bloggr</a>
        <ul class="nav">
          <li><a href="#">Posts</a></li>
          <li>{{#linkTo 'about'}}About{{/linkTo}}</li>
        </ul>
      </div>
    </div>

    {{outlet}}

  </script>

y al entrar en la sección about (pinchando en el link del menú de navegación o escribiendo en la barra de dirección del navegador /#/about) veremos esto:

Sección About

Lista de Posts (Master)

Puesto que esta sección no es estática como la anterior, necesitaremos definir un modelo y un Store de datos:

  App.Store = DS.Store.extend({
    revision:12,
    adapter: 'DS.FixtureAdapter'
  });

DS.FixtureAdapter nos permite especificar todo nuestro modelo de datos de manera estática como objetos JavaScript. Es perfecto durante fases del desarrollo inicial para trabajar de manera ágil, podríamos decir que son una “simulación” de carga de datos y que posteriormente podemos susutiurlo sin ningún tipo de fricción por un RESTAdapter que cargue los datos dinámicos del server mediante un servicio REST.

Definimos el modelo en sí:

  App.Post = DS.Model.extend({
    title: Ds.attr('string'),
    author: Ds.attr('string'),
    intro: Ds.attr('string'),
    publishedAt: Ds.attr('date')
  });

Nota: Hemos simplificado un poco el modelo con respecto al que aparece en el screencast agrupando los atributos extended e intro en uno al que hemos llamado fullText

Y a continuación añadimos los FIXTURES es decir datos que van a cargar:

App.Post.FIXTURES = [{
    id:1,
    title: "Mejoras de usabilidad en el ecommerce. Panama Jack",
    author: "elad",
    publishedAt: new Date('4-8-2013'),
    fullText: "..."
},
{
    id:2,
    title: "Introducción al framework Ember.js",
    author: "danii",
    publishedAt: new Date('4-3-2013'),
    fullText: "..."
}];

Para que exista esta sección de posts, añadimos un resource ‘posts’ en el Router, que quedará así:

  App.Router.map(function() {
    this.resource('about');
    this.resource('posts');
  });

Tambien debemos especificar que este modelo de datos que acabamos de crear es el que debemos utilizar para esta ruta, para ello extendemos el objeto Ember.Route y le indicaremos que cargue el modelo:

App.PostsRoute = Ember.Route.extend({
    model: function() {
        //return all elements in model
        return App.Post.find();
    }
});

Y con esto ya hemos implementado toda la funcionalidad necesaria para esta sección en nuestra App, tan sólo nos queda definir su template y linkarla desde la navegación, para esto una vez más hemos cambiar el link estático del template principal por el helper {{linkto}}, en este caso a la sección ‘posts’.

A continuación tenemos que crear una template con template-data-name=»posts» y puesto que el nombre de la ruta y el nombre del template coinciden Ember ya sabe que ambos van asociados. Great!

En este template listaremos todos los posts que tenemos, para ello utilizaremos el helper de Handlebars {{#each}} que itera por cada elemento del modelo de datos y renderiza la parte del template que contiene por cada uno, utilizándolo como contexto. El template quedará por tanto así:

<script type="text/x-handlebars" data-template-name="posts">
    <div class="container-fluid">
      <div class="row-fluid">
        <div class="span3">
          <table class="table">
            <thead>
              <tr><th>Recent Posts</th></tr>
            </thead>
            {{#each model}}
            <tr><td>
              <a href="#">{{ title }}</a><small class="muted">por {{ author }}</small>
            </td></tr>
            {{/each}}
          </table>
        </div>
        <div class="span9">
          <!-- place holder donde irá la vista detail de cada post -->
        </div>
      </div>
    </div>
  </script>

Y ya tenemos la vista Master 🙂

Sección Posts: vista master

Vista Detail de los Posts

Ahora tan solo nos falta que se cargue la vista Detail de cada post individual al seleccionarlo del listado. Vamos a añadir al Router una ruta anidada (nested route) dentro de la ruta ‘posts’ que represente a cada post individual:

App.Router.map(function() {
  this.resource('about');
  this.resource('posts', function() {
    this.resource('post', { path: ':post_id' })
  });
});

Y ahora en el HTML creamos una plantilla template-data-name=»post» asociada a esta ruta:

  <script type="text/x-handlebars" data-template-name="post">
    <h1>{{title}}</h1>
    <h2>por {{author}} <small class="muted">({{publishedAt}})</small></h2>
    <hr>
    <div>
      {{fullText}}
    </div>
  </script>

A continuación tenemos que linkar desde el listado Master de posts a esta plantilla recién creada, para ello sustitumos la linea donde teníamos los links estáticos con el tag a que habíamos dejado por el helper {{#linkTo}}, que quedará así:

  {{#linkTo 'post' this}}{{title}}{{/linkTo}}

En este caso hemos de pasar un parámetro this al helper, que representa el contexto actual, es decir el modelo de datos que debe mostrar el template. El último paso será añadir el helper {{outlet}} en el lugar del placehoder que habíamos colocado en la plantilla Master, para que Ember sepa dónde debe renderizar la vista de la ruta anidada.

En este momento debemos poder clickar en cada uno de los posts de nuestra lista y se cargará el contenido asociado a este post:

Post individual vista Detail

Formato mediante Helpers de Handlebars

Por supuesto, hay un par de problemas obvios de formato actualmente:

  • La fecha de publicación del post se muestra algo parecido a: (Mon Apr 08 2013 00:00:00 GMT+0200 (CEST)) lo cual es un formato de fecha no demasiado legible para el usuario (horrible, vamos);

  • Y el contenido del post sale en formato Markdown, obviamente, ya que lo tenemos almacenado así en nuestras Fixtures, cuando lo ideal es que se renderice en el navegador como HTML.

Para dar formato a estos elementos utilizaremos las librerías JavaScript moment.js y showdown.js respectivamente, y lo haremos integrándolos dentro de un par de helpers propios de Handlebars que realizarán estas tareas y que posteriormente incluiremos en el template.

Primero, definimos un BoundHelper de Handlebars mediante Ember.Handlebars.registerBoundHelper, este tipo de Helper es perfecto para datos dinámicos ya que se comporta como un observer de los datos que muestra y se actualiza automáticamente si estos datos cambiasen. En el screencast podéis ver una demo de este comporamiento.

El primer parámetro de la función registerBoundHelper será el nombre del helper, y el segundo la función que dará formato a los datos que recibe como parámetro y posteriormente los devolverá ya formateados. Utilizaremos la función fromNow() de moment.js para mostrar el tiempo que ha pasado desde la fecha de publicación del post. Además, vamos a aprovechar la potencia de i18n de moment.js para mostrar el lapso de tiempo en castellano, ya que nuestros posts van a estar escritos en la lengua de Cervantes 😉

moment.lang('es',
{
  relativeTime: {
    past : 'hace %s',
    s : "unos segundos",
    m : "un minuto",
    mm : "%d minutos",
    h : "una hora",
    hh : "%d horas",
    d : "un día",
    dd : "%d días",
    M : "un mes",
    MM : "%d mes",
    y : "un año",
    yy : "%d años"
  }
});

Ember.Handlebars.registerBoundHelper('date', function(date) {
  return moment(date).fromNow();
});

Y substituimos {{publishedAt}} por la llamada al helper: {{date publishedAt}}

A continuación, seguimos los mismos pasos para mostrar el texto del post en formato HTML, con la particularidad de que tenemos que utilizar un Handlebars.SafeString ya que por defecto las plantillas en Ember van a escapar todo su contenido por razones obvias de seguridad:

//creamos objeto converter
//para pasar de markdown a HTML
var showdown = new Showdown.converter();

Ember.Handlebars.registerBoundHelper('markdown', function(input) {
  //devolvemos SafeString para que no escape el contenido HTML
  return new Ember.Handlebars.SafeString(showdown.makeHtml(input));
});

E invocamos al helper al mostrar el texto del post, es decir sustituimos {{text}} por: {{markdown text}}.

Y finalmente deberíamos ver el post con un formato correcto:

Post individual Detail con formato

Añadimos Modo Edición

Hasta este punto nuestra aplicación es Read-only, pero en este momento vamos a añadir un modo de edición para poder editar el texto y títlo de los posts on the fly y además mostrando un live-preview de estos cambios en la vista principal del post, ya que vincularemos los datos mediante Data Binding. ¡Brutal! 🙂

Lo primero será añadir un botón de edit en el template del post. Para distinguir entre los estados de edición y visualización utilizaremos los helpers de Handlebars {{#if}} que renderiza una parte del template de forma condicional, y {{action}} para invocar una función del controlador:

    {{#if isEditing}}
      <h3>Edit mode</h3>
      <button {{action 'doneEditing'}}>Done</button>
    {{else}}
      <button {{action 'edit'}}>Edit</button>
    {{/if}}

Post Detail botón edit

Y a continuación debemos crear esta variable de estado booleana isEditing, así como las funciones edit y doneEditing en el controlador. Fijaros que este controlador que se encarga de la vista Post no lo he tenido que implementar hasta que no he necesitado un comportamiento adicional. Esta es la base de la filosofía de Ember: implementar sólo los objetos que necesite, del resto ya se encarga el framework. Al crear un objeto con el nombre PostController, el framework ya sabe que estará asociado a la vista y los datos del Post.

En este caso extenderé a la clase Ember.ObjectController ya que este controlador tiene como modelo un único post (no un array de posts) e implementaré las funciones que hacen un toggle entre el modo de edición y no edición. Además vamos a hacer un commit de todos los cambios que se hagan en el modelo, de manera que cuando estemos listos para guardar los datos y sustituyamos el FixtureAdapter por un RESTAdapter se realicen las llamadas al server:

App.PostController = Ember.ObjectController.extend({
    isEditing: false,
    edit: function()
    {
      this.set('isEditing',true);
    },
    doneEditing: function()
    {
      this.set('isEditing',false);
      //save to server
      this.get('store').commit();
    }

});

El siguiente paso será mostrar una UI para editar el post cuando esté en modo edición. Para mantener nuestro template de post sencilla, vamos a hacerlo de manera modular incluyendo un parcial o template externo mediante el helper {{partial}}. Por convenio, los nombres de template de los parciales empiezan siempre por “_” para distinguirlos del resto de templates. En este parcial vamos a utilizar los helpers view que se encargan de renderizar una vista, y las vistas predefinidas de EmberEmber.TextField y Ember.TextArea, por supuesto vinculadas mediante Data Binding al título y al texto del post respectivamente:

  <script type="text/x-handlebars" data-template-name="post/_edit">
    <p>{{view Ember.TextField valueBinding='title'}}</p>
    <p>{{view Ember.TextArea valueBinding='text'}}</p>
  </script>

Y en volvemos al template del post para indicarle que debe incluir este partial cuando esté en modo edición:

  {{#if isEditing}}
    <h3>Edit mode</h3>
    {{partial 'post/edit'}}
    <button {{action 'doneEditing'}}>Done</button>
  {{else}}
    ...

¡Y ya lo tenemos! Casi sin esfuerzo hemos añadido un modo de edición en el que además, como todos los datos con lo que estamos trabajando están vinculados, el post que estamos mostrando se actualizará on the fly automáticamente.

Vista Post UI Edit

Una pequeña mejora de Usabilidad

Un pequeño detalle que podemos mejorar es que cuando entramos a la ruta posts, por ejemplo desde el botón de navegación superior, nos vamos a encontrar con una pantalla casi vacía como hemos visto anteriormente:

Sección Posts: vista master

Lo cual no es un para nada un ejemplo de buena usabilidad. Para solucionar este problema, debemos especificar a Ember el contenido que debe mostrar en la ruta por defecto de Posts, es decir cuando no hay ningún post seleccionado. Esta ruta en Ember se llama por convenio /index, así que en este caso por ejemplo debemos llamar a nuestro template posts/index.

Una opción sería crear una plantilla placeholder que indique al usuario lo que debe hacer:

  <script type="text/x-handlebars" data-template-name="posts/index">
    <p class="text-warning">Por favor, seleccione un post</p>
  </script>

Y quedaría así:

Vista posts placeholder por defecto

Lo cual es ciertamente mucho mejor y es la opción que se toma en el screencast, pero a nosotros tampoco nos ha convencido.

En nuestra opinión, el comportamiento más adecuado para la usabilidad sería que se cargara automáticamente por defecto el post más reciente. Para ello, lo que vamos a hacer un redirect de esta ruta posts/index a la ruta posts/:post_id con los valores del último post del modelo de datos. Por tanto, puesto que estamos alterando el comportamiento por defecto, tenemos que definir el objeto PostsIndexRouter y dentro haremos las siguientes operaciones:

  • Tomaremos el primer elemento del modelo de los posts, que será el último post publicado,

  • A continuación haremos una transitionTo a la ruta ‘post’ con el modelo que antes hemos seleccionado.

El código quedaría así:

App.PostsIndexRoute = Ember.Route.extend({
  redirect: function () {
    var posts = this.modelFor('posts');
    var post = posts.get('firstObject');

    this.transitionTo('post', post);
  }
});

Pero no está todo bien aún, ya que nos vamos a topar con otro problema: con este código, es posible que el router de Ember intente hacer un redirect antes de que el modelo esté completamente cargado, con lo cual saltará un error en tiempo de ejecución y el redirect fallará. Para solucionar este problema, hemos tomado la decisión de tener una precarga de datos mediante bootstrapping de forma que si el primer post no ha sido cargado a la hora de hacer un redirect, lo tome del bootstrap. Añadiendo esta mejora en el código anterior, quedaría así:

App.PostsIndexRoute = Ember.Route.extend({
  redirect: function () {
    var posts = this.modelFor('posts');
    var post = posts.get('firstObject');

    if(!post)
    {
        console.log("LOADING bootstrap'ed DATA");
        DS.get('defaultStore').load(App.Post, bootstrap);
        post = App.Post.find(1);
    }

    this.transitionTo('post', post);
  }
});

Donde bootstrap es simplemente un objeto JavaScript donde hemos incluido los datos del último post. En producción esta variable será creada con datos dinámicos extraídos del server, claro 😉

Redireccinamos Index

Relacionado con el paso anterior, al entrar a la index de la aplicación lo ideal sería que nos redireccionara a la lista de posts directamente (y si habéis implementado nuestra mejora, mostrara la vista Detail del primer post). Para esto, simplemente vamos a definir la ruta por defecto de la aplicación para que vaya a la ruta ‘posts’ similarmente a como hemos hecho el redirectTo en el paso anterior:

  App.IndexRoute = Ember.Route.extend({
    redirect: function () {
      this.transitionTo('posts');
    }
  });

Final: Conectando con server REST

Este último paso no lo vamos a terminar en este ejemplo ya que no tenemos un servicio REST implementado en el servidor de este blog, pero podéis observar en el video que simplemente cambiando ‘DS.FixtureAdapter’ por DS.RESTAdapter y especificando a qué URL tiene que atacar para que el server le pase los datos, ¡ya debería estar funcionando sin tocar ni una línea de código más! El resto de problemas, para el equipo de backend 😉

Conclusión

Esperamos que este step by step así como el código que os mostramos os resulte útil y os ayude a seguir el screencast y a sumergiros en el mundo de Ember. Como ya comentamos en nuestra anterior intro a este framework, nos parece que Ember tiene un potencial enorme y aunque muchas cosas todavía «crujen» al estar el framework en fase de desarrollo, estamos convencidos de que se puede apostar por él ya como una alternativa más que viable para producción de Web Apps JavaScript profesionales.

Etiquetas: , ,

5 Comentarios
» Feed RSS de los Comentarios

  1. javier dice:

    Hola, quisiera saber si en ember se pueden descargar temas o plantillas así como con Boostrap y si tiene en que pagina los puedo conseguir.

    Gracias

  2. danii dice:

    Buenas Javier, mira te recomiendo que pegues un vistazo a este proyecto open source: Bootstrap for ember https://github.com/ember-addons/bootstrap-for-ember, basado en los geniales Ember components http://emberjs.com/guides/components/ y Bootstrap v3 http://getbootstrap.com/

    Espero que te sea de utilidad!

  3. Tony Medrano dice:

    Hola chicos muy buen tutorial en castellano para ember.js, pero en el código DS.Model.extend los atributos deben ser DS y DS. Un abrazo

  4. Tony Medrano dice:

    Perdón, quiero decir DS y no Ds (minúscula).

  5. Cornelio dice:

    Como hago para mantener los templates en archivos separados?

Enviar comentario