homeASCIIcasts

205: Javascript no intrusivo 

(view original Railscast)

Other translations: En Cn De It Tr

Written by Juan Lupión

En esta nueva entrega de la serie en la que estamos repasando las nuevas funcionalidades de Rails 3 vamos a ver cómo escribir Javascript no intrusivo. El objetivo del Javascript no intrusivo es separar el comportamiento de una aplicación web de su contenido, de manera similar a como CSS permite separar presentación de contenido. Antes de empezar a escribir Javascript no intrusivo en Rails veremos un ejemplo con un documento sencillo.

La captura de pantalla de abajo muestra una página web que contiene un enlace . Cuando se hace clic en dicho enlace aparece una alerta Javascript que dice "Hello world!".

El mensaje de alerta que se muestra desde Javascript.

El HTML de esta página es el siguiente:

<!DOCTYPE html>
<html>
  <head>
    <title>UJS Example</title>
  </head>
  <body>
    <h1><a href="#" onclick="alert('Hello world!'); return false;">Click Here</a></h1>
  </body>
</html>

En esta página tenemos un enlace con un atributo onclick que contiene algo de código Javascript. Está al estilo intrusivo dado que el script está incluido dentro del HTML, lo cual no es deseable porque estamos mezclando contenido y comportamiento. Allá por los años 90 las páginas web se diseñaban utilizando el elemento <font> para establecer la tipografía, tamaño y color del texto porque aún no teníamos CSS. Esto significaba que si queríamos cambiar el tamaño de todos los textos en párrafos de un sitio web tendríamos que hacer potencialmente cientos de cambios. Cuando los navegadores empezaron a soportar CSS pudimos pasar esta información a las hojas de estilos y por tanto el aspecto de los sitios web se hizo mucho más fácil de mantener.

Lo mismo ocurre con Javascript Si ponemos pequeños fragmentos de Javascript en los atributos de los elementos HTML de una página se mezclan dos funcionalidades que hacen más difíciles de mantener el código. Si se movemos este Javascript a archivos separados se reduce la duplicación, se facilita la refactorización y hace que sea mucho más sencillo escribir y depurar aplicaciones web complejas.

¿Cómo podríamos convertir en no intrusivo el Javascript de nuestro sencillo ejemplo anterior? El paso principal es mover el Javascript que está en el atributo onclick a un archivo separado y utilizar un framework Javascript (en este caso jQuery) para conectar estos scripts con eventos sobre los elementos. Veamos cómo queda la página con estos cambios:

<!DOCTYPE html>
<html>
  <head>
    <title>UJS Example</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="UTF-8"></script>
    <script type="text/javascript"charset="UTF-8">
      $(function () {
        $('#alert').click(function () {
          alert('Hello, world!');
          return false;
        })
      });
    </script>
  </head>
  <body>
    <h1><a href="#" id="alert">Click Here</a></h1>
  </body>
</html>

Lo primero que notamos es que a pesar de lo que dijimos más arriba no hemos trasladado el Javascript a su propio fichero, pero esto es sólo para que los cambios sean más fáciles de ver.

Hemos eliminado el atributo onclick del enlace y lo hemos cambiado por un id de forma que podamos localizar el enlace desde el código jQuery. Hemos añadido en la cabecera una referencia a la librería jQuery y justo después el script que debería escribirse en el fichero externo. Dicho script empieza invocando la función $ de jQuery con otra función como argumento. Dicha función se invocará cuando termine de cargar el DOM de la página y el código en él contiene una línea de código jQuery que encuentra el enlace por el identificador y asocia al evento de clic una función que muestra una alerta y devuelve false de forma que no se siga el enlace.

Si ahora recargamos la página se comportará como antes, mostrandose la alerta cuando se hace clic en el enlace.

Dado que hemos tomado una línea de código Javascript en línea y la hemos cambiado por seis bien podríamos pensar que hemos hecho mucho trabajo a cambio de más bien poco. El problema es que con un ejemplo tan sencillo como el que hemos visto no podemos apreciar realmente la ventaja del Javascript no intrusivo: pretendíamos más bien ver cómo se hace y compararlo. No empezaremos a comprobar realmente los beneficios de este estilo no intrusivo hasta que nuestra aplicación tenga mucho más código porque sólo entonces veremos que es mucho más ventajoso tener todo el Javascript en su propio archivo separado.

El problema que tenemos con este enfoque es que el Javascript aparece normalmente un un fichero Javascript estático. ¿Cómo podemos insertar contenido dinámico generado desde el servidor en este Javascript ahora que no podemos incluirlo en línea?

En HTML 5 podemos utilizar atributos de datos personalizados para guardar datos relacionados con cualquier elemento de una página. Son iguales que cualquier otro atributo pero el nombre debe empezar por data-. Podemos almacenar el mensaje que se mostrará al hacer clic en el enlace de esta manera:

<a href="#" id="alert" data-message="Hello from UJS">Click Here</a>

Y luego en el Javascript podemos alterar la alerta para mostrar el texto a partir de este nuevo atributo:

$(function () {
  $('#alert').click(function () {
    alert(this.getAttribute('data-message'));
    return false;
   })
});

Si volvemos a cargar la página veremos el mensaje recuperado del atributo data-message.

El mensaje de alerta creado usando Javascript no intrusivo.

Cómo se usan los atributos de datos en Rails 3

Rails 3 puede utilizar estos atributos con datos personalizados como forma de pasar datos a Javascript. Vamos a ver cómo se aplica esto a una aplicación Rails. Supongamos que tenemos una aplicación sencilla de comercio electrónico que tiene una lista de productos sobre los que se pueden realizar búsquedas. También hay enlaces para editar y destruir productos y es cuando destruimos un producto cuando nos encontramos con el primer problema porque el enlace parece no funcionar.

La página de producto con el enlace que no funciona.

Este problema es frecuente en aplicaciones Rails 3. Si estamos creando o migrando un aplicación a partir de una versión antigua de Rails podemos encontrarnos con que hay partes de la aplicación que usan Javascript que han dejado de funcionar.

El código de la vista que genera el enlace “Destroy” es el método link_to con la opción :confirm para mostrar una alerta Javascript y la opción :method con el valor :delete de forma que la petición se envía como una petición DELETE en lugar de GET.

/app/views/products/show.html.erb

<%= link_to "Destroy", @product, :confirm => "Are you sure?", :method => :delete %>

Aquí lo interesante es ver el código fuente HTML generado por este código:

<a href="/products/8" data-confirm="Are you sure?" data-method="delete" rel="nofollow">Destroy</a>

En Rails 2, si usamos el método link_to para crear un enlace de borrado se generará una gran cantidad de código Javascript para crear el diálogo de confirmación y un formulario para simular una petición DELETE o PUT. En comparación, el código generado por Rails 3 es mucho más limpio y crea dos atributos de datos de HTML5: uno llamado data-confirm que contiene el mensaje de confirmación y otro llamado data-method que contiene el método propiamente dicho.

El motivo por el que el enlace no funciona es que no estamos haciendo referencia a los ficheros Javascript en la cabecera de la página por lo que el método se comportará como un enlace normal y efectuará una petición GET dado que nadie le está diciendo que haga otra cosa.

Para arreglar esto tan sólo tenemos que añadir las dos línea siguientes a la sección head del fichero de layout de nuestra aplicación:

/app/views/layouts/application.html.erb

<%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>

La primera línea ya debería resultarnos familiar: incluye los archivos Javascript estándar en una aplicación Rails. La segunda línea crea dos etiquetas meta donde se guarda el token de autenticación que es necesario para hacer peticiones DELETE. Si recargamos la página y vemos el código fuente comprobaremos el HTML generado por estas dos líneas.

<script src="/javascripts/prototype.js?1268677667" type="text/javascript"></script>
<script src="/javascripts/effects.js?1268677667" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1268677667" type="text/javascript"></script>
<script src="/javascripts/controls.js?1268677667" type="text/javascript"></script>
<script src="/javascripts/rails.js?1268677667" type="text/javascript"></script>
<script src="/javascripts/application.js?1268677667" type="text/javascript"></script>
<meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="9ImdFvbeW7ih9oKqBDQ3O889q/hJ1q5uajpT4DFDAoA="/>

Con esto estamos incluyendo todos los ficheros Javascript que necesita nuestra aplicación y definiendo las dos etiquetas meta que hacen falta para evitar peticiciones XSS falsas, que garantizan que las peticiones PUT y DELETE vienen del usuario correcto y no de un hacker u otro lugar.

Con estos dos elementos en su lugar nuestro enlace de borrado ya funciona como sería de esperar:

Ahora funciona el enlace de borrado.

Formulario de búsqueda con AJAX

A continuación vamos a modificar el formulario de búsqueda de la página principal para que utilice AJAX en lugar de hacer una petición GET normal. Veamos el código de la vista index que contiene el formulario:

/app/views/products/index.html.erb

<% title "Products" %>

<% form_tag products_path, :method => :get do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search", :name => nil %>
  </p>
<% end %>

<div id="products">
  <%= render @products %>
</div>

<p><%= link_to "New Product", new_product_path %></p>

El formulario que hemos usado para las búsquedas utiliza la técnica vista en el episodio 37. En las versiones anteriores de Rails para que el formulario funcionase con AJAX cambiábamos form_tag por form_remote_tag. Este método genera demasiado Javascript en línea, que es precisamente lo que queremos evitar.

Muchos de los métodos helper remotos ya no están disponibles en Rails 3. Podemos recuperarlos instalando el plugin Prototype Legacy Helper pero en vez de esto vamos a utilizar el nuevo enfoque de Rails 3, que consiste en seguir utilizando form_tag en lugar de form_remote_tag, pero añadiendo un nuevo parámetro llamado :remote.

/app/views/products/index.html.erb

El parámetro :remote también se puede usar con otros métodos como link_to, button_to y form_for. Si recargamos la página y miramos el código fuente veremos cómo funciona el nuevo código del formulario.

<form action="/products" data-remote="true" method="get">  <p>
  <input id="search" name="search" type="text" />
    <input type="submit" value="Search" />
  </p>
</form>

El elemento form es el mismo que antes pero ahora tiene un nuevo atributo llamado data-remote. No hay Javascript en línea bastará con el nuevo atributo para decirle a Javascript en rails.js que el formulario debe enviarse usando AJAX.

El listado de productos aparece en un div con un id llamado products de forma que podemos actualizar este div para mostrar los resultados apropiados. El formulario se envía a la acción index de ProductController y lo único que tenemos que hacer es añadir una nueva vista para las peticiones Javascript llamada index.js.erb.

En esta vista podemos escribir el Javascript que queramos que se ejecute cuando sea devuelta al navegador. El código de esta nueva plantilla actualiza los contenidos de la capa con productos con el listado de productos.

/app/views/products/index.js.erb

$("products").update("<%= escape_javascript(render(@products))%>");

Si recargamos la página y hacemos una búsqueda, ésta se hará con una llamada AJAX y veremos que la URL de la página no cambia cuando se realice dicha búsqueda.

El formulari de búsqueda con AJAX.

Como vemos en Rails 3 es fácil de hacer AJAX de forma no obtrusiva utilizando el parámetro :remote y ejecutando el Javascript devuelto por el servidor.

Cambio de Frameworks

Terminaremos este episodio viendo cómo cambiar el framework Javascript utilizado por nuestra aplicación. Ahora mismo estamos usando Prototype, que viene por defecto con Rails pero, ¿y si queremos usar jQuery?

En primer lugar tendremos que cambiar esta línea en el layout de nuestra aplicación:

/app/views/layouts/application.html.erb

<%= javascript_include_tag :defaults %>

por esta:

/app/views/layouts/application.html.erb

<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", "jquery.rails.js" %>

El primer fichero de la lista es la última versión de jQuery servida desde Google. Esto por sí solo no es suficiente porque necesitamos el equivalente del fichero rails.js que hemos usado antes para manejar el Javascript no intrusivo. Podemos obtener la versión oficial del proyecto jquery-ujs en Github. Este proyecto publica un archivo rails.js podemos descargar y utilizar en nuestros pryectos. Lo hemos puesto en el directorio /public/javascripts de nuestra aplicación y lo hemos renombrado como jquery.rails.js. Este fichero gestinará todo el Javascript no intrusivo de Rails.

A continuación tenemos que modificar todo el código Javascript que hemos escrito para utilizar código compatible con jQuery. Tendremos que hacer un par de pequeños cambios en el código de index.js.erb utilizando #products en lugar de products com el selector de la capa de productos y cambiando el método update de Prototype por su equivalente jQuery html.

/app/views/products/index.js.erb

$("#products").html("<%= escape_javascript(render(@products))%>");

Ahora nuestra aplicación funcionará igual que antes pero utilizando jQuery en lugar de Prototype.

Degradación grácil

Si un usuario carga nuestra aplicación utilizando un navegador que no tiene Javascript activado el formulario degradará grácilmente y ejecutará una petición GET normal al enviarse. No funcionará por tanto el borrado de productos porque los enlaces HTML sólo pueden realizar peticiones GET (este es el motivo por el que Rails utiliza Javascript para simular la petición DELETE). Una solución a esto es cambiar el enlace con un botón utilizando button_to pero esto puede quedar poco atractivo por lo que preferimos seguir con un enlace. Una técnica preferible es la descrita en el episodio 77 que muestra una página de confirmación separada antes de borrar un elemento con un navegador que tiene desactivado Javascript.