Rails Cells. Renderizado por componentes “as it should be”

Sigo vivo.

Voy a tocar un proyecto que es nuevo para mí, pero me ha encantado. He tenido la oportunidad de charlar con el creador porque había encontrado un bug, y lo resolvimos (lo resolvió él, yo solo lo localicé) en cuestión de dos horas. Excelentemente mantenido.

El proyecto no es otro que Rails Cells (Github), que intenta traer de entre los muertos la renderización por componentes (aquel método #render_component) que ha sido abandonada por ser poco elegante y poco eficiente.

Las células (cells) pretenden ser entidades de las vistas más o menos independientes y fácilmente reusables a través de toda una aplicación. Son además una herramienta que logra simplificar el código de las vistas, hacer inecesarios muchos métodos helpers, o al menos simplificarlos muchos.
Llegados a este punto uno pensará: “Eso ya existe, se llaman parciales“, pero no.

Sirven para lo mismo (encapsular y reusar), pero funcionan de forma diferente. Hay quien las define como parciales con esteroides, pero en realidad con controladores con acciones.

Nacen basándose en la convención de que las vistas han de ser vistas y sólo vistas, y toda la lógica debe estar encapsulada en niveles inferiores. No hay que ser un histérico con esto, y Dios no mata un gatito cada ves que alguien escribe un if-else en una vista, pero todos hemos visto (y escrito) vistas parecido a esta:

.toolbox
	- if user.webmarster?
		= render "webmaster_tools"
	- elsif user.admin?
		= render "admin_tools"
	- elsif user.registered?
		= render "registered_user_tools"
	- else
		= link_to "Regístrate", sign_up_path	

Dios no mata a un gatito, pero feo es un rato. Lo habitual es escribir un helper parecido a este:

# application_helper.rb
def render_users_tools(us)
	partial = us.webmaster?  ? "webmaster" : (us.admin?  ? "admin" :  (us.registered_user? ?  "registered_user" : nil))
	partial ? render("#{partial}_tools") : link_to("Regístrate", sign_up_path)	
end

#la vista
.toolbox= render_users_tools(user)

La vista queda muy limpia, y escribir un helper no es un gran problema, aunque no nos olvidemos de que estamos escribiendo también 3 parciales, en las que es probable que parte del código pueda reusarse de una a otra.
Pero el asunto puede, incluso suele, ser más complejo.

Supongamos que a mayores de las herramientas queremos que si un usuario está registrado se muestre algo del estilo “Bienvenido, Fulanito”, y además, si hace más de 10 días que no se conecta le escriba “Te echabamos de menos”, y si tiene un mensaje privado (y sólo si lo tiene) le aparezca un enlace a su buzón de entrada. Si es del un administrador le muestre una lista de temas que aun no ha moderado, pero sólo de la sección del foro de la que es moderador, y el mismo mensaje de “Te echabamos de menos” si hace días que no se conecta. Si es el webmaster las estadísticas de ganancias de adSense y GoogleAnalytics, en enlace a su buzón si tiene correo nuevo, pero no se le saluda y si hace días que no se conecta un mensaje que diga “Tengo que trabajar más, la web no se mantiene sola”.

En este ejemplo rápido, hay muchas características que dependen de que tipo de usuario es, el tiempo que lleva sin conectarse, y otras circunstancias, y además algunas son comunes a varios pero no todos los tipos de usuarios. Si metemos condicionales en la vista para modelar todo esto, sí que Dios matará a un león.

Las celdas nos permiten extraer la funcionalidad a un mini-controlador, en la que cada acción encapsule un pedazo de funcionalidad y su representación, y además aprovechar características como la herencia para lograr un código lo más DRY posible. Los componentes se han de comportar como cajas negras en que tu las invocas, y ellos se encargan del resto.

Como de costumbre, para instalar la gema tan solo ponemos en el gemfile la linea gem 'cells', y ejecutamos $ bundle

Antes de ver cómo usarlas, explicar lo que son las cells.
Una cell es una clase ruby que hereda de Cell::Rails. Dado que Cell::Rails hereda de ActionController::Base, una cell no deja de ser un controlador como otro cualquiera pero retocado, y que se aloja por defecto en app/cells.
Dentro de ese controlador, podemos crear acciones. La diferencia entre estos métodos y los de un controlador normal, es que la llamada a render ha de ser explícita, o de lo contrario no se buscará su vista.
Así pues, la celda que va a lidiar con la renderización de la toolbox , llamémosla ToolboxCell, estará en el archivo app/cells/toolbox_cell.rb, y las vistas de sus métodos en app/cells/toolbox/. Estas vistas pueden usar cualquier lenguaje de plantillas que pueda usar rails, como por ejemplo Haml, mi preferido.

La gema nos provee además de unos generadores para crear cells, así que la mejor manera de ver cómo usarlas es seguir paso a paso un ejemplo desde 0.
El ejemplo que propongo es puede que no sea el ideal, pero es el que se me ha ocurrido, y lo he diseñado para hacer uso de la herencia de vistas y otras características bastante desconocidas de las ultimas versiones de rails.
En mi aplicación hay muchos formularios, y esos formularios pueden contener entradas de tipos de datos muy variados, con estéticas muy diversas, agrupados en fieldsets.
Tal vez queremos que el fieldset agrupe campos uno debajo del otro, que sean la típica pareja label+textfield, pero puede también que queramos que se agrupen en columnas de a dos, que si van a contener fotos tengan otra estructura, que si contienen los campos de un determinado tipo tengan otra distinta, que algunos de ellos contengan un elemento distinto al principio o al final, o no tengan label o cualquier otra cosa, y que todos ellos puedan mostar errores en el formulario y sepan automáticamente que tipo de entrada han de poner sin que tu expecifiques si es un textfield, un select, o lo que sea.

Vamos primero a crear una cell llamada fieldse usando Haml
rails g cells:cell Fieldset --haml
Listo, se encuentra en app/cells/fieldset_cell.rb y ahora vamos a crear states en ella. Los states son como las acciones de un controlador. Idénticas, salvo que tienes llamar a render expresamente.

class FieldsetCell < Cell::Rails
  helper ApplicationHelper
  # STATES
  def show(args)       # vista en app/cells/fieldset/show.haml
    @form     = args[:form]
    @section  = args[:section]
    @fields   = @form.object.class.fieldsets[@section]
    render
  end

  def field(fld,index)  # vista en app/cells/fieldset/field.haml
    @field = fld
    @errors   = @form.object.errors
    @value    = @fields.map{ |f| @form.object.send(f) }[index]
    @field_class = field_class
    @input_class = @form.object.class.columns_hash[@field.to_s].type
    render
  end

  def header          # vista en app/cells/fieldset/header.haml
    @explanation = @form.object.class.fieldsets_explanations[@section]
    render
  end

  def ending          # vista en app/cells/fieldset/ending.haml
    @section==:general ? render : ''
  end
  # HELPERS
  def field_class
    @errors[@field].empty? ? nil : 'has_errors'
  end

end

Si uno no se fija en la declaración de clase, pensaría que está viendo un controlador.
No tenéis que entender lo que estoy haciendo en cada método, sólo que cada acción tiene su propia vista.
Si queremos invocar esta cell, lo hacemos con la llamada render_cell. La sintaxis es

= render_cell :fieldset, :show, :form => @form, :section => section
# render_cell :nombre_cell, :nombre_state, :parametros_auxiliares
# Los parámetros auxiliares se encapsulan en un hash llamado args, que recibe el estado como parámetro.

Ya está. La lógica de este elemento está encapsulada en este controlador, por lo que la vista de show.haml es simple.
render :state=>:nombre_estado llama a un estado de la cell actual, con lo que todas las variables de instancia se comparten.

%fieldset{:id=>"#{@section}_fieldset"}
  = render :state => :header
  - @fields.each_with_index do |field, index|
    = render({:state => :field},field,index)
  = render :state => :ending

El estado ending simplemente se invoca siempre, y es en el controlador donde se evalúa el contexto para decidir si hay que mostrar algo. La estructura de las vistas es esta:

app
|--cells
|--fieldset
| |--show.haml
| |--header.haml
| |--field.haml
| |--ending.haml
|--fieldset_cell.rb

Hasta ahora la verdad es que exceptuando que el código es mucho más limpio, puesto que no hay en las vistas lógica, no hemos visto ninguna ventaja a este pradigma. Creas nuevas controladores con nuevos métodos, con nuevas variables, añades un nivel más de indirección, y ese mísmo código limpio podía haberse logrado con algunos helpers y sin tantas vueltas.
La chicha viene ahora.
En rails 3.0.4 y superiores existe la llamada view inheritance, que es exactamente lo que parece. Si nosotros tenemos una segunda cell llamada BicolumnFieldsetCell, que queremos que presente lo mismo que FieldsetCell pero en dos columnas, hacemos que una herede de la otra, y aprovechamos la herencia para lograr un código DRY.
Cell

# bicolumnfieldset_cell.rb
# Nada que poner. Heredo toda la lógica
class BicolumnfieldsetCell < FieldsetCell        
end

Vistas. Sólo existe show.haml

%fieldset{:id=>"#{@section}_fieldset" }
  = render(:state => :header)
  .bicolumn
    .half.left
      - @fields.slice(0, @fields.size/2).each_with_index do |lf,index|
        = render({:state => :field},lf,index)
    .half.right
      - @fields.slice(@fields.size/2, @fields.size/2).each_with_index do |rf,index|
        = render({:state => :field},rf,index)

Arbol de archivos

app
|--cells
|--fieldset
| |--show.haml
| |--header.haml
| |--field.haml
| |--ending.haml
|--bicolumnfieldset
| |--show.haml
|--fieldset_cell.rb
|--bicolumn_fieldset.rb

Aquí el código empieza a brillar por su ausencia. No existe código más claro y mejor documentado que aquel que no se escribe. La clase no necesita nueva lógica, hereda de la clase padre, y solo tenemos que cambiar la vista show.haml, puesto que el resto se buscan por herencia. Rails busca en la carpeta bicolumnfieldset las vistas que necesita, y si alguna no la encontrase, sube la cadena de herencia hasta el padre para buscar la suya.
Creamos una tercera cell para el fieldset que deba contener un mapa.

# mapfieldset_cell.rb
class MapfieldsetCell < FieldsetCell
  def field_class         # Aquí sí cambio la lógica, pero puedo aprovecharme de código reusable del padre usando super.
    " hidden #{super}" 
  end
end

Vistas. Sólo existe header.haml

%h3.legend= t(@section).humanize
%h6.superindex.right-text= @explanation
.edit_map_buttons.float-left{:style=>'width: auto;'}
  #capture_sv.center-text.round-corners=image_tag "camera_white_mini.png",:alt=>"Guardar coordenadas de StreetView"
  #put_marker.center-text.round-corners=image_tag "google-marker-white-mini.png",:alt=>"Colorcar marcador"
#photo_effect.medium_map
#map_canvas.medium_map.edit_map

Arbol de archivos

app
|--cells
|--fieldset
| |--show.haml
| |--header.haml
| |--field.haml
| |--ending.haml
|--bicolumnfieldset
| |--show.haml
|--mapfieldset
| |--header.html
|--fieldset_cell.rb
|--bicolumnfieldset_cell.rb
|--mapfieldset_cell.rb

Otra vez aprovechamos casi todo del padre, y solo personalizamos una vista y una acción. show.haml es la de la clase padre, pero invoca a header.haml de mapfieldset.
Y todavía mejor. Como las cells son controladores, pueden testearse con normalidad, cosa que con la lógica de las vistas no puede hacerse.

Todavía podemos darle una vuelta de tuerca. Tal y como tenemos definido ahora las cells, en donde las invoquemos todavía tenemos que decidir si queremos invocar a un tipo de célula u otro.

- if bicolumn_fieldset_sections.include?(section) 
    = render_cell :bicolumnfieldset, :show, :form => @form, :section => section
- elsif map_fieldset_sections.include?(section)
    = render_cell :mapfieldset, :show, :form => @form, :section => section
-else
    = render_cell :fieldset, :show, :form => @form, :section => section

Podemos hacerlo mejor. Dejemos que sea el constructor de FieldsetCell quien decida si instanciarse a sí mismo o a uno de sus hijos más especializado. La declaración de la case queda así.

class FieldsetCell < Cell::Rails
  helper ApplicationHelper
  build do |args|
    case args[:section]
      when :geolocation then MapfieldsetCell
      when :services,:ratings,:recomendations then BicolumnfieldsetCell
      when :courses then CoursesfieldsetCell
      when :photos then PhotosfieldsetCell
      else FieldsetCell
    end
  end
  # STATES
  # ...
end

Ahora sí que el funcionamiento de la cell es totalmente transparente para nosotros.
Hemos definido media docena de tipos de fieldsets, y hemos definido una reglas (en este caso un tanto toscas) en la cell padre para decidir si llamar en su lugar a uno de sus hijos.
Desde este momento, nosotros en la vista de nuestro formulario escribimos

- @sections.each do |section|
      = render_cell :fieldset, :show, :form => @form, :section => section

y el resto se construye solo siguiendo las reglas que hemos definido. Trabajando en equipo, el del al lado no sabe cómo funciona por dentro el componente, ni le importa, simplemente funciona.
Por supuesto, podemos anidar cells dentro de cells, sin problema, y crear baterías de tests que lleguen a partes donde antes no llegábamos.

De la misma forma que las cells son herramientas potentes, también hay que calibrar donde merece la pena usarlas, y donde un helper y una parcial nos arreglan la vida sin complicaciones.

Podéis encontrar mucha más información sobre cells en su página, o bien en el blog de su creador.

Anuncios

, , , , , , ,

  1. Deja un comentario

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: