#286 Draper
- Download:
- source codeProject Files in Zip (125 KB)
- mp4Full Size H.264 Video (27.2 MB)
- m4vSmaller H.264 Video (14.6 MB)
- webmFull Size VP8 Video (16.7 MB)
- ogvFull Size Theora Video (32.1 MB)
En este episodio presentamos Draper, una gema para añadir decoradores en las vistas de una aplicación Rails, al estilo del patrón presentador. Si en nuestras vistas y métodos helper tenemos mucha lógica compleja Draper nos puede servir para limpiar el código mediante un enfoque más orientado a objeto. Veremos cómo funciona en este episodio.
La aplicación con la que vamos a trabajar es la que aparece debajo, tiene una página para el perfil de usuario que muestra diferente información sobre los usuarios incluyendo el avatar, el nombre completo, una biografía breve en Markdown y enlaces a una web y a Twitter. Si el usuario nos ha dado la dirección de su web el avatar y el nombre enlazarán a dicho sitio.
Parece una página sencilla pero hay que gestionar también los usuarios que no han dado toda esta información, como por ejemplo “MrMystery”.
El usuario tan sólo ha introducido el nick por lo que será lo que mostraremos en lugar del nombre completo, así como un avatar por defecto y un par de textos fijos para el resto de campos. Esto hace que se complique la plantilla de esta página, puesto que hay múltiples condiciones if
para gestionar los usuarios según la información que hayan ido introduciendo. La plantilla quedaría mucho mejor si pudiésemos mover parte de esta lógica a otro sitio.
<div id="profile"> <%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %> <h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1> <dl> <dt>Username:</dt> <dd><%= @user.username %></dd> <dt>Member Since:</dt> <dd><%= @user.member_since %></dd> <dt>Website:</dt> <dd> <% if @user.url.present? %> <%= link_to @user.url, @user.url %> <% else %> <span class="none">None given</span> <% end %> </dd> <dt>Twitter:</dt> <dd> <% if @user.twitter_name.present? %> <%= link_to @user.twitter_name, "http://twitter.com/#{@user.twitter_name}" %> <% else %> <span class="none">None given</span> <% end %> </dd> <dt>Bio:</dt> <dd> <% if @user.bio.present? %> <%=raw Redcarpet.new(@user.bio, :hard_wrap, :filter_html, :autolink).to_html %> <% else %> <span class="none">None given</span> <% end %> </dd> </dl> </div>
Dado que esta lógica está relacionada con la vista no podemos extraerla al modelo. Una solución sería utilizar métodos helper. Ya usamos uno llamado image_tag
en la plantilla para mostrar el avatar. Veámoslo.
module UsersHelper def avatar_name(user) if user.avatar_image_name.present? user.avatar_image_name else "default.png" end end end
Este método helper determina si el usuario actual tiene un avatar y en caso contrario devuelve el nombre de una imagen por defecto. Podríamos extraer más lógica de la vista en métodos helper pero el problema con este enfoque es que se trata de métodos simples en el espacio de nombres global o lo que es lo mismo, la técnica no tiene nada de orientación a objetos.
Instalación de Draper
Este escenario es un buen sitio para utilizar un presentador (o un decorador, como Draper prefiere denominarlo) así que añadámoslo a la aplicación. La gema Draper se instala de la manera habitual, añadiéndola al Gemfile
y luego ejecutando bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.0' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', " ~> 3.1.0" gem 'coffee-rails', "~> 3.1.0" gem 'uglifier' end gem 'jquery-rails' gem 'redcarpet' gem 'draper'
Tras la instalación de Draper crearemos un decorador para nuestro modelo User
mediante el generador draper:decorator
.
$ rails g draper:decorator user create app/decorators create app/decorators/application_decorator.rb create app/decorators/user_decorator.rb
Como se trata del primer decorador que añadimos, se creará también un fichero llamado application_decorator
. Todos los decoradores que creemos heredarán de ApplicationDecorator por lo que podremos poner en este fichero la funcionalidad que queramos compartir entre todos nuestros decoradores.
La clase UserDecorator
es muy sencilla, y consiste sobre todo en cometarios que explican su funcionamiento. Empezamos a usarla para despejar nuestras plantillas.
Arreglando la página de perfil
Para usar Draper en nuestra página de perfil primero tenemos que hacer un cambio en la acción show
de UsersController
La acción recupera una instancia de User
en la forma habitual.
class UsersController < ApplicationController def index @users = User.all end def show @user = User.find(params[:id]) end end
Tenemos que envolver este usuario con nuestro decorador, lo que haremos cambiando User.find
por UserDecorator.find
.
def show @user = UserDecorator.find(params[:id]) end
El código devolverá una instancia de UserDecorator
, que por defecto delegará todos los métodos a User
por lo que la acción funcionará igual que antes aunque estemos trabajando con un UserDecorator
en lugar de un User
. Con esto ya podemos empezar a limpiar nuestras vistas y empezaremos con el código que muestra el avatar del usuario.
<%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>
Esto lo cambiaremos por:
<%= @user.avatar %>
Este código buscará el método avatar
en el UserDecorator
que escribiremos a continuación. Deberemos tener en cuenta varias cosas cuando escribamos este método. Cuando invoquemos a un método helper desde el decorador (como por ejemplo link_to_if
) tenemos que hacerlo a través del método h
. Cuando queramos referenciar al modelo invocaremos model
en lugar de @user
(en este caso).
El código que hemos copiado desde la vista en avatar
invoca el método helper avatar_name
, como lo estamos llamando desde el decorador lo quitaremos de UsersHelper
para llevarlo a UsersDecorator
. Con el método en la misma clase no tendremos que pasar un User
y podemos cambiar las llamadas por el modelo.
class UserDecorator < ApplicationDecorator decorates :user def avatar h.link_to_if model.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), model.url end private def avatar_name if model.avatar_image_name.present? model.avatar_image_name else "default.png" end end end
A continuación ordenaremos el código que muestra el nombre del usuario. Quitaremos el siguiente código de la vista:
<h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>
Y lo reemplazaremos por este otro:
<h1><%= @user.linked_name %></h1>
Tendremos que escribir el método linked_name
en UserDecorator
. Hay bastante similitud entre este método y el método avatar
que escribimos antes, porque ambos muestran un enlace cuyo contenido depende de que el usuario halla rellenado la url
. Como estamos en una clase es fácil refactorizar esta duplicidad.
Para gestionar el enlace crearemos un nuevo método privado llamado site_link
, que recibe el contenido como un parámetro. Luego se puede invocar a este método tanto en los métodos avatar
como en linked_name
para que queden más ordenados. Igual que antes, cambiaremos cualquier llamada a @user
por model
en el método linked_name
. Con todo esto, nuestro decorador queda así:
class UserDecorator < ApplicationDecorator decorates :user def avatar site_link h.image_tag("avatars/#{avatar_name}", class: "avatar") end def linked_name site_link(model.full_name.present? ? model.full_name : model.username) end private def site_link(content) h.link_to_if model.url.present?, content, model.url end def avatar_name if model.avatar_image_name.present? model.avatar_image_name else "default.png" end end end
Si recargamos la página del perfil de un usuario debería tener el mismo aspecto que antes.
Nuestra plantilla está quedando mucho más limpia, pero podemos hacer aún más. Lo siguiente que haremos será refactorizar un trozo grande del código de las vistas, el código que muestra un enlace a la web del usuario.
<dt>Website:</dt> <dd> <% if @user.url.present? %> <%= link_to @user.url, @user.url %> <% else %> <span class="none">None given</span> <% end %> </dd>
Que reemplazaremos por:
<dt>Website:</dt> <dd><%= @user.website %></dd>
Al igual que antes crearemos un método en la clase del decorador. Del código que acabamos de quitar de la vista podemos ver que si el usuario no tiene url
se muestra cierto texto en HTML. Podríamos devolverlo como una cadena pero no queremos poner HTML en crudo en una cadena Ruby. Otra solución sería mover el código a un parcial y mostrarlo, pero como sólo estamos mostrando un elemento de HTML tiene más sentido utilizar el método content_tag
.
def website if model.url.present? h.link_to model.url, model.url else h.content_tag :span, "None given", class: "none" end end
Podemos hacer lo mismo con las dos partes de la plantilla que muestran la información de Twitter y la biografía del usuario. Ahora no mostraremos todos los detalles, pero tras haber hecho los cambios nuestro código de la vista quedará mucho mejor.
<div id="profile"> <%= @user.avatar %> <h1><%= @user.linked_name %></h1> <dl> <dt>Username:</dt> <dd><%= @user.username %></dd> <dt>Member Since:</dt> <dd><%= @user.member_since %></dd> <dt>Website:</dt> <dd><%= @user.website %></dd> <dt>Twitter:</dt> <dd><%= @user.twitter %></dd> <dt>Bio:</dt> <dd><%= @user.bio %></dd> </dl> </div>
Los nuevos métodos twitter
y bio
del decorador quedarían así:
def website if model.url.present? h.link_to model.url, model.url else h.content_tag :span, "None given", class: "none" end end def twitter if model.twitter_name.present? h.link_to model.twitter_name, "http://twitter.com/#{model.twitter_name}" else h.content_tag :span, "None given", class: "none" end end def bio if model.bio.present? Redcarpet.new(model.bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe else h.content_tag :span, "None given", class: "none" end end
Los dos nuevos métodos tienen un aspecto similar y tienen también el método website
que escribimos anteriormente. Hay mucho código duplicado en estos tres métodos, especialmente en la cláusula else
, por lo que estaría bien extraer esta parte a su propio método.
Esto lo podemos resolver con un bloque, extrayendo la cláusula else
a su propio método que llamaremos handle_none
. Pasaremos el valor del que queremos comprobar su presencia a este método y un bloque. Si el valor existe, se ejecutará el fragmento del bloque, de lo contrario se creará la etiqueta span
. Podemos usar este método handle_none
para refactorizar los métodos website
, twitter
y bio
.
def website handle_none model.url do h.link_to model.url, model.url end end def twitter handle_none model.twitter_name do h.link_to model.twitter_name, "http://twitter.com/#{model.twitter_name}" end end def bio handle_none model.bio do Redcarpet.new(model.bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe end end private def handle_none(value) if value.present? yield else h.content_tag :span, "None given", class: "none" end end
Otro cambio que se puede hacer es extraer el procesado de Markdown al ApplicationDecorator
para poder invocarlo desde cualquier otro decorador que utilicemos. Crearemos el método markdown
que mostrará el texto que se le pase.
class ApplicationDecorator < Draper::Base def markdown(text) Redcarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe end end
Ahora, en UserDecorator
podemos modificar el método bio
para que invoque markdown
.
def bio handle_none model.bio do markdown(model.bio) end end
Modificación del modelo
Con el decorador puesto en su sitio es buena idea examinar la capa del modelo para ver si hay código relacionado con la vista que podamos llevar al decorador correspondiente. Por ejemplo, en nuestro modelo User
tenemos el método member_since
que formatea el atributo created_at
. Esto código podría considerarse de la vista porque lo único que hace es devolver una cadena, por lo que lo movemos al decorador.
class User < ActiveRecord::Base def member_since created_at.strftime("%B %e, %Y") end end
Tan sólo tenemos que mover el método al decorador y poner model
delante de la llamada a created_at
.
def member_since model.created_at.strftime("%B %e, %Y") end
Acceso restringido al modelo con el método allows
.
Draper ofrece otra funcionalidad que vamos a demostrar a la vez que seguimos modificando UserDecorator
: el método allows
. El UserDecorator
, tal y como está ahora, delegará todos sus métodos al objeto User
que contiene, pero podemos escoger cuáles mediante allows
y pasando el nombre de los métodos que queremos que delegue.
class UserDecorator < ApplicationDecorator decorates :user allows :username # Other methods omitted end
Sólo vamos a permitir que se delegue username
al modelo User
. Es el único método del modelo que necesitamos delegar porque es el único que se invoca desde la vista que no es implementado por el decorador, de esta manera tenemos más control sobre la interfaz expuesta por el decorador.
Una vez concluida esta refactorización, intentemos cargar el perfil de un usuario para ver que todo sigue teniendo el mismo aspecto.
También podemos comprobar el otro usuario y ver que tiene el mismo aspecto, aunque el código de la vista ahora es mucho más claro.
Mediante el uso del decorador la plantilla de la vista ha bajado de 1050 bytes en 34 lines a 382 bytes en 16 líneas, una reducción de casi dos tercios. También ha quedado mucho más claro y la hemos hecho mucho más fácil de editar si queremos modificar la estructura de la página.