Seguimos donde lo dejamos, rápidamente nos dedicaremos a preparar el layout general de la página y empezaremos a poner Ajax por todas partes.
Se suele comentar que Rails y Ajax se llevan bien. Vamos a demostrarlo creando una pequeña aplicación pseudo GTD para mantener una lista de TODO’s.
En Rails un layout es una vista especial: es la parte común en un grupo de controladores/acciones. En muchas aplicaciones, con uno o dos layouts nos bastará, por ejemplo: uno para usuarios sin identificar y otro para los miembros, etc. En nuestra aplicación microtodo sólo necesitaremos uno. El generador scaffold nos generó el único fichero que podemos encontrar en app/views/layouts: todo_items.html.erb.
Muy típico de Rails: Convention over Configuration. Si no decimos nada, el controlador TodoItemsController buscará si existe un layout llamado todo_items, y si no lo encuentra después buscará uno llamado application. Podemos sobreescribir estas convenciones poniendo layout :nuevo_layout en el controlador, pero ahora no nos interesa.
Lo primero que haremos con este layout es cambiarle el nombre precisamente a application.html.erb, para que sirva para posibles nuevos controladores. Después le haremos un pequeño lavado de cara dejándolo así:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Micro Todo</title>
<%= stylesheet_link_tag 'scaffold' %>
<%= javascript_include_tag :defaults %>
</head>
<body>
<% flash.each do |key, msg| %>
<%= content_tag :div, msg, :id => key %>
<% end %>
<%= yield %>
</body>
</html> |
Es bastante sencillo: stylesheet_link_tag 'scaffold' usa un helper que nos ofrece Rails para insertar la etiqueta <link> y el fichero CSS en cuestión (scaffold.css). Usar este helper tiene algunas ventajas que ahora no estudiaremos, como por ejemplo cachear los ficheros de estilo. Lo mismo sirve para javascript_include_tag, aunque este usa la palabra defaults que se traduce en los ficheros necesarios para usar Prototype y Scriptaculous (Los usaremos para el Ajax).
Finalmente hay este bloque Ruby tan chulo y que puede parecer difícil de entender, aunque hace una cosa bien sencilla: Para cada flash[:algo] que exista, ponemos un <div id="algo">Con su contenido.</div>. Esto nos servirá tanto para flash[:error] como para flash[:notice], o cualquier otro que nos inventemos.
La última palabra Ruby del layout es la palabra mágica yield. Se sustituirá por el contenido de la vista de la acción que se esté ejecutando (index, edit, add, …). Espero no ir demasiado lento profundizando en los detalles, pero al ser un tutorial para principiantes… Pido paciencia a los iniciados que lo estén siguiendo.
Queremos tener el formulario para añadir tareas en la página principal, y que al pulsar el botón Add o la tecla Enter se añada a la lista la tarea mediante Ajax. Volvemos a modificar app/views/todo_items/index.html.erb para usar partials y añadir el formulario:
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- index.html.erb -->
<h1>Things To Do</h1>
<div id="todos">
<%= render @todo_items %>
</div>
<br />
<% form_remote_for TodoItem.new do |f| -%>
<%= f.text_field :task %>
<%= f.submit "Add" %>
<% end -%> |
Como podéis observar, hemos simplificado mucho el código que lista las tareas usando render @todo_items. Rails es muy listo, y observa que @todo_items es un array de todo_item, y piensa: Si me piden hacer un render de un todo_item, a lo mejor es que hay un partial para este… Y (oh! qué casualidad) justo acabo de crear un fichero llamado _todo_item.html.erb en app/views/todo_items/. Para que os funcione teneis que crearlo y dejarlo así:
1
2
3
4
5
6
7
| <!-- _todo_item.html.erb -->
<% div_for todo_item do %>
<%= h todo_item.task %>
(<%= link_to 'Edit', edit_todo_item_path(todo_item) %>)
<small>(<%= link_to 'Destroy', todo_item, :confirm => 'Are you sure?', :method => :delete %>)</small>
<span><%= todo_item.completed_at %></span>
<% end %> |
Seguramente habréis observado que he puesto form_remote_for en lugar de form_for, y aquí empieza el Ajax: Esto mandará el contenido del formulario en una llamada Ajax, y en lugar de refrescar el navegador ejecutará lo que el controlador le diga. Así que vamos al controlador y cambiamos un poco el método create.
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| def create
@todo_item = TodoItem.new(params[:todo_item])
respond_to do |format|
if @todo_item.save
flash[:notice] = 'Todo Item added!.'
format.html { redirect_to(todo_items_url) }
format.xml { render :xml => @todo_item, :status => :created, :location => @todo_item }
format.js
else
format.html { render :action => "new" }
format.xml { render :xml => @todo_item.errors, :status => :unprocessable_entity }
end
end
end |
Hemos añadido la línia format.js en el bloque respond_to do |format|. Esto lo que hará es responder tamién al método create si se pide con Content-Type: text/javascript, que es lo que hará la llamada Ajax, pero responderá usando el view create.js.rjs. Hasta ahora todos los views tenian la doble extensión .html.erb, que significa que se trata de ficheros HTML procesados con ERB (Embedded Ruby). Ahora ofrecemos un fichero JavaScript usando RJS, un lenguaje muy simple para generar código JavaScript escribiendo Ruby. Este fichero estará obviamente en app/views/todo_items/ y será así:
1
2
3
4
5
| # create.js.rjs
page.insert_html :bottom, :todos, :partial => 'todo_item', :object => @todo_item
page.visual_effect :highlight, "todo_item_#{@todo_item.id}"
page[:new_todo_item].reset
flash.discard |
Resumiendo: La primera línea generará el partial con el objeto recién creado en el controlador y lo añadirá al final de <div id="todos">. La segunda hará un efecto de sobresalto en la tarea recién añadida, la tercera vaciará el formulario y con la última descartaremos el mensaje flash que se añade en el controlador. La magia de Ajax y Rails es que este código Ruby se convertirá en JavaScript, y gracias a form_remote_for se mandará de vuelta al cliente, donde se ejecutará dejándonos asombrados a nosotros mismos de lo que sómos capaces de hacer.
Ahora (sólo porque me conviene que se vean las cosas un poco mejor) añadiremos un poco de CSS al final de public/stylesheets/scaffold.css
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| .todo_item
{
display: block;
padding: 5px 0;
background: #DADADA;
margin: 10px;
}
.todo_item span
{
font-size: 0.7em;
color: #747474;
}
body
{
margin: 0 auto;
width: 60%;
} |
Lo último que haremos en este capítulo es completar y descompletar las tareas mediante un checkbox que usando Ajax actualizará el estado de la tarea. Dado que estamos usando un controlador REST, tenemos que modificar el fichero config/routes.rb para permitir una nueva acción: toggle, que cambiará de completado a pendiente una tarea. Modificamos el fichero:
1
2
3
4
5
6
| ActionController::Routing::Routes.draw do |map|
map.resources :todo_items, :member => { :toggle => :put }
map.root :todo_items
end |
Ahora añadimos el método toggle en TodoItemsController:
87
88
89
90
| def toggle
@todo_item = TodoItem.find(params[:id])
@todo_item.update_attribute :completed_at, (@todo_item.completed_at.nil? ? Time.now : nil)
end |
Añadimos el checkbox al parcial _todo_item.html.erb
1
2
3
4
5
6
7
8
9
| <!-- _todo_item.html.erb -->
<% div_for todo_item do %>
<%= check_box_tag :completed_at, "1", !todo_item.completed_at.nil?,
:onclick => remote_function(:url => toggle_todo_item_path(todo_item), :method => :put) %>
<%= h todo_item.task %>
(<%= link_to 'Edit', edit_todo_item_path(todo_item) %>)
<small>(<%= link_to 'Destroy', todo_item, :confirm => 'Are you sure?', :method => :delete %>)</small>
<span><%= todo_item.completed_at %></span>
<% end %> |
Y sólo nos falta crear otro fichero RJS: toggle.js.rjs junto a las otras vistas de views/todo_items:
1
2
3
| # toggle.js.rjs
page.replace "todo_item_#{@todo_item.id}", :partial => 'todo_item', :object => @todo_item
page.visual_effect :highlight, "todo_item_#{@todo_item.id}" |
Esto es todo por hoy. Os podéis bajar el código de Github usando Git o directamente un fichero comprimido. Y para los más perezosos que tienen ganas de ver cómo funciona esto del Ajax pero no quieren probar el código o instalar Ruby on Rails, podéis ver este pequeño screencast de 30 segundos (sin audio):

Dejo para el próximo capítulo separar las tareas pendientes de las completadas, eliminarlas usando ajax, cambiarles el texto in-situ y reordenarlas! (Recordáis el campo position:integer de nuestra migración?).