#258 Token Fields
- Download:
- source codeProject Files in Zip (144 KB)
- mp4Full Size H.264 Video (23.9 MB)
- m4vSmaller H.264 Video (13.7 MB)
- webmFull Size VP8 Video (29.9 MB)
- ogvFull Size Theora Video (29.8 MB)
Supongamos que estamos desarrollando una aplicación para una tienda de libros. Nos encontramos todavía muy al principio y lo único que podemos hacer con un libro es darle un nombre.
Queremos poder asignar varios autores a un libro dado, y ya tenemos código en la aplicación para hacerlo: se define una relación de muchos a muchos entre Book
y Author
a través de un modelo llamado Authorship
. Veamos el modelo Book
.
class Book < ActiveRecord::Base attr_accessible :name has_many :authorships has_many :authors, :through => :authorships end
Se trata de una relación has_many :through
estándar.
¿Cómo gestionar esta relación de muchos a muchos en el formulario? Una posibilidad sería utilizar casillas de selección, enfoque que ya vimos en el episodio 17 [verlo, leerlo] pero el problema es que como tenemos un gran número de autores entre los que escoger no sería práctico utilizar una lista de casillas. Sería mucho mejor poder elegir de entre los autores mediante una caja de texto que autocompletase del listado de autores según escribamos, y nos permita escribir múltiples autores en cada libro. Sería similar a la interfaz utilizada por Facebook para enviar mensajes y funcionaría muy bien con la relación de muchos a muchos.
En este episodio veremos como implementar esta funcionalidad en una aplicación Rails mediante el uso de un plugin de jQuery.
Tokeninput
Una posible solución sería emplear el plugin Autocomplete de jQuery UI pero habría que escribir mucho código personalizado para gestionar las entradas por términos prefijados. jQuery Tokeninput es una alternativa mejor y hace justo lo que necesitamos, pudiendo escoger su aspecto de entre varios temas.
Los campos de texto que usen este plugin deberán permitir que se envíe su contenido como una lista de id
numéricos separados por comas, lo cual será fácil de analizar por el servidor (pronto veremos de dónde salen estos identificadores).
El plugin se compone del archivo jquery.tokeninput.js
que es el que tendremos que copiar en el directorio public/javascripts
de nuestra aplicación y los archivos del directorio styles
que son los que copiaremos en public/stylesheets
.
Aún no hemos configurado la aplicación para que utilice jQuery así que vamos a añadir la gema jquery-rails
al Gemfile
y ejecutaremos bundle
para instalarla.
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'jquery-rails'
Podemos instalar jQuery con
$ rails g jquery:install
Por último tenemos que incluir los ficheros JavaScript y CSS de Tokeninput en el layout de nuestra aplicación:
<!DOCTYPE html> <html> <head> <title><%= content_for?(:title) ? yield(:title) : "Untitled" %></title> <%= stylesheet_link_tag "application", "token-input" %> <%= javascript_include_tag :defaults, "jquery.tokeninput" %> <%= csrf_meta_tag %> <%= yield(:head) %> </head> <body> <!-- Resto del fichero... -->
Con todo esto ya lo tenemos todo configurado para añadir el campo de autores a los libros en nuestra aplicación. Primero tenemos que añadir un nuevo campo de texto en el formulario y llamarlo author_tokens
.
<%= form_for @book do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :author_tokens, "Authors" %><br /> <%= f.text_field :author_tokens %> </p> <p><%= f.submit %></p> <% end %>
En el modelo Book
no existe un atributo author_tokens
por lo que añadiremos sus métodos de lectura y escritura:
class Book < ActiveRecord::Base attr_accessible :name, :author_tokens has_many :authorships has_many :authors, :through => :authorships attr_reader :author_tokens def author_tokens=(ids) self.author_ids = ids.split(",") end end
Para la función que lee el valor podemos utilizar attr_reader
pero la función de escritura es más complicada porque tiene que analizar la lista de identificadores separados por comas que se reciben desde el campo de texto. En este método separaremos la lista de identificadores y luego estableceremos los author_ids
con lo que efectivamente se asignarán los autores del libro. Por último tenemos que añadir este nuevo campo a la lista de attr_accessible
para poder aceptar ambos campos desde el formulario.
Si ahora recargamos el formulario tendrá un nuevo campo Authors
. Podríamos insertar manualmente los identificadores en dicho campo pero obviamente queremos utilizar el plugin Tokeninput, que vamos a configurar para que funcione sobre este nuevo campo.
La página de Tokeninput tiene documentación acerca del uso del plugin. Lo único que tenemos que hacer es llamar a tokenInput
sobre el campo de texto y pasar una URL. La URL deberá devolver cierto JSON con el siguiente formato para que los elementos puedan aparecer en la lista de autocompletado según el usuario vaya tecleando.
[ {"id":"856","name":"House"}, {"id":"1035","name":"Desperate Housewives"}, ... ]
Si queremos filtrar la lista, el texto que aparece actualmente en la caja de texto se pasa en la cadena de la URL como el parámetro q
.
Apliquemos esto en nuestra aplicación. Lo primero que tenemos que hacer ese escribir el JavaScript que añade la funcionalidad Tokeninput al campo del autor. Esto irá en el fichero application.js
.
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false }); });
Este código encuentra la caja de texto de nombres de autor por su id
y luego invoca tokenInput
para activar el plugin en ella pasándole la URL que devolverá el JSON para rellenar las opciones. Esta URL será /authors.json
. Escribamos el código para gestionar todo esto. Podemos pasar algunas opciones a la función tokenInput
como segundo argumento, y por lo que parece hay que establecer crossDomain:false
si queremos evitar que los resultados se envíen usando JSONP, para evitarlo y hacer que se envíe en JSON estándar debemos establecer la opción anterior.
Lo siguiente es hacer que la URL funcione. Ya tenemos un AuthorsController
por lo que sólo tenemos que hacer que la acción index
sea capaz de responder peticiones JSON. Para esto añadimos un respond_to
con dos formatos en el interior del bloque, uno de HTML y otro de JSON que devuelve la lista de autores en formato JSON.
class AuthorsController < ApplicationController def index @authors = Author.all respond_to do |format| format.html format.json { render :json => @authors } end end end
Si visitamos http://localhost:3000/authors.json
podremos examinar el JSON devuelto por dicha URL.
Podemos devolver el listado de autores en JSON pero entonces cada elemento aparecerá anidado dentro de un objeto author
, que no es lo que espera Tokeninput, sino una lista de parámetros id
y name
. Tendríamos también que escribir una función que devolviese el nombre si nuestro modelo no tuviese un atributo con ese nombre, pero en nuestro caso sólo nos tenemos que preocupar de eliminar el objeto author
de la raíz de cada autor que aparece el listado. Hay varias formas de hacer esto globalmente pero como arreglo rápido tan sólo tenemos que mapear el array con el listado de atributos de cada autor.
def index @authors = Author.all respond_to do |format| format.html format.json { render :json => @authors.map(&:attributes) } end end
Si ahora recargamos la página veremos que ya no aparece el objeto author
alrededor del nombre y el id
de cada autor, con lo que el listado ya está en el formato con el que trabaja Tokeninput.
Tokeninput ignorará cualquier otro atributo que no sea id
o name
. En una aplicación de producción deberíamos considerar eliminar los otros atributos para minimizar el uso de ancho de banda.
Ya podemos visitar nuestro formulario de creación de libro para probar el plugin. Si empezamos a escribir en el campo de autores vereos un listado que contiene todos los autores.
Pero en lugar hacer que se devuelvan todos los autores sólo queremos devolver aquellos que casan con nuestro término de búsqueda. En el controlador AuthorsController
tenemos que filtrar el listado de autores según el parámetro q
, que es el contenido del texto introducido en la caja.
def index @authors = Author.where("name like ?", "%#{params[:q]}%") respond_to do |format| format.html format.json { render :json => @authors.map(&:attributes) } end end
Cambiaremos Author.all
por Author.where
para buscar sobre los autores cuyo nombre sea similar al término recibido. Nótese que dicho término se encuentra rodeado por signos de porcentaje para que pueda ser aceptado en cualquier posición del campo de nombre. Si ahora buscamos un autor, sólo se devolverán los nombres correspondientes.
Ahora que funciona el campo autocompletado con su filtrado podemos intentar añadir un libro para comprobar que si creamos un libro con dos autores éste se guardará correctamente y veremos los autores listados en la página del libro a la que seremos redirigidos automáticamente.
Edición de libro
Pero cuando editemos un libro nos encontraremos con un problema porque el formulario no muestra correctamente los autores. Tenemos que pre-rellenar el formulario con los nombres de los autores del libro.
El plugin Tokeninput soporta la opción prePopulate
donde si le pasamos un fragmento de JSON rellenará la lista basándose en su contenido. Podemos añadir esta opción en la llamada a tokenInput
en application.js
pero ¿cómo pasarle los datos en cuestión? Una forma de hacerlo es añadir un atributo de datos HTML5 en el campo de texto y leer los datos de ahí:
<%= form_for @book do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :author_tokens, "Authors" %><br /> <%= f.text_field :author_tokens, "data-pre" => @book.authors.map(&:attributes).to_json %> </p> <p><%= f.submit %></p> <% end %>
Al atributo lo llamaremos data-pre
. Su valor será establecido a los atributos de los autores del libro de forma similar a como creamos el JSON del listado de autocompletado.
Podemos leer esto datos en el fichero JavaScript y utilizarlo para pre-rellenar el listado de autores.
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false, prePopulate: $('#book_author_tokens').data('pre') }); });
Si ahora recargamos la página de edición veremos que el listado de los autores se rellena correctamente.
Si actualizamos los autores (por ejemplo quitando uno y añadiendo otro) todo se guardará correctamente.
Temas
Hasta ahora hemos venido usando el tema por defecto que se incluye con Tokeninput. Si queremos cambiarlo tenemos que hacer dos cosas. Primero tenemos que ir a nuestro fichero de layout y cambiar el fichero de CSS de Tokeninput por otro que no sea el incluido por defecto (token-input
). Existe un tema Facebook disponible que pasaremos a usar.
<%= stylesheet_link_tag "application", "token-input-facebook" %>
A continuación modificaremos el JavaScript que creará el campo Tokeninput para establecer la opción de tema:
/public/javascripts/application.js
$(function () { $('#book_author_tokens').tokenInput('/authors.json', { crossDomain: false, prePopulate: $('#book_author_tokens').data('pre'), theme: 'facebook' }); });
Así podemos añadir el tema que queramos y adaptarlo a nuestra aplicación. Si ahora recargamos la página de edición veremos el nuevo tema aplicado.
Con esto concluimos este episodio. Tokeninput es una magnífica solución para manejar relaciones de muchos a muchos en formularios Rails, esperamos que haya quedado bien explicado.