#370 Ransack
- Download:
- source codeProject Files in Zip (59.2 KB)
- mp4Full Size H.264 Video (29 MB)
- m4vSmaller H.264 Video (12.7 MB)
- webmFull Size VP8 Video (15 MB)
- ogvFull Size Theora Video (30.7 MB)
En la aplicación de ejemplo que se muestra a continuación tenemos un listado de productos, y queremos que los usuarios puedan realizar búsquedas sobre cualquier columna del listado. Si bien esta funcionalidad se puede desarrollar desde cero en este episodio vamos a utilizar una gema llamada Ransack. Es la sucesora de MetaSearch, que ya vimos en el episodio 251, y nos permite crear fácilmente formularios complejos de búsqueda.
Ransack se instala añadiéndolo al Gemfile
y ejecutando bundle
.
gem 'ransack'
Tras instalar Ransack podemos usarlo en la acción a la que queramos añadir búsquedas, en este caso la acción index
de ProductsController
, en la que crearemos un objeto de búsqueda invocando a Product.search
y pasándole el parámetro q
(que contiene un hash con los parámetros de búsqueda que haya pasado el usuario). Así, para recuperar los productos ejecutamos result
en este objeto.
class ProductsController < ApplicationController def index @search = Product.search(params[:q]) @products = @search.result end end
A continuación tenemos que construir el formulario. Podemos crearlo con el constructor de formularios que incorpora Ransack y que se llama search_form_for
. El método recibe un bloque en el cual definimos los campos sobre los que queremos realizar las búsquedas. Los nombres que escojamos para estos campos tienen significado y son importantes para definir la funcionalidad. Por ejemplo si el campo de texto del formulario se llama name_cont
Ransack buscará los productos cuyo nombre contenga el valor introducido en este campo.
<%= search_form_for @search do |f| %> <div class="field"> <%= f.label :name_cont, "Name contains" %> <%= f.text_field :name_cont %> </div> <div class="actions"><%= f.submit "Search" %></div> <% end %>
Tras recargar la página veremos que ahora aparece un formulario de búsqueda que filtra los productos.
Podemos añadir más funcionalidad a estas búsquedas añadiendo más campos en la vista, no nos hace falta escribir más lógica. Por ejemplo para poder filtrar por precio podemos añadir dos campos:
<%= search_form_for @search do |f| %> <div class="field"> <%= f.label :name_cont, "Name contains" %> <%= f.text_field :name_cont %> </div> <div class="field"> <%= f.label :price_gteq, "Price between" %> <%= f.text_field :price_gteq %> <%= f.label :price_lteq, "and" %> <%= f.text_field :price_lteq %> </div> <div class="actions"><%= f.submit "Search" %></div> <% end %>
Ahora podemos buscar en los productos dentro de cierto rango de precios.
Si miramos en la sección acerca de búsquedas básicas del wiki veremos una lista de los predicados que se pueden pasar para personalizar la forma en que se realiza la búsqueda, y el SQL que se genera en cada caso. Ransack también nos facilita incluir los enlaces para los diferentes tipos de ordenación, así que haremos que las cabeceras de la tabla sean enlaces que permitan ordenar los resultados de la búsqueda. En la vista podemos usar un método helper de Ransack llamado sort_link
, lo que cambiaremos el texto estático por enlaces.
<tr> <th><%= sort_link(@search, :name, "Product Name") %></th> <th><%= sort_link(@search, :released_on, "Release Date") %></th> <th><%= sort_link(@search, :price, "Price") %></th> </tr>
Al recargar la página veremos que ya tenemos listos los enlaces para ordenar la tabla de resultados por cada uno de los campos.
Cómo crear un formulario de búsqueda dinámica
A continuación veremos algunas de las prestaciones más avanzadas de Ransack. Las búsquedas pueden ser más complejas de lo que parece a simple vista y podemos crear formularios de búsqueda completamente dinámicos de forma que el usuario tenga total control sobre las columnas y predicados a usar. En lugar de tener campos concretos de búsqueda en la página vamos a crear algo más dinámico. Podemos llamar a un método llamado condition_fields
en el constructor del formulario, que a su vez creará otro constructor de formulario para cada una de las condiciones de búsqueda. Tras esto recorremos cada uno de los campos de attribute_fields
y mostramos el attribute_select
para cada uno de ellos, tras lo que mostramos los campos para el predicado y el valor.
<%= search_form_for @search do |f| %> <%= f.condition_fields do |c| %> <div class="field"> <%= c.attribute_fields do |a| %> <%= a.attribute_select %> <% end %> <%= c.predicate_select %> <%= c.value_fields do |v| %> <%= v.text_field :value %> <% end %> </div> <% end %> <div class="actions"><%= f.submit "Search" %></div> <% end %>
El código parece bastante complicado pero es valioso por la cantidad de funcionalidad que aporta si bien tenemos que hacer un pequeño cambio en el controlador para que esto funcione. Por defecto las búsquedas no tienen condiciones así que tenemos que añadir una vacía.
class ProductsController < ApplicationController def index @search = Product.search(params[:q]) @products = @search.result @search.build_condition end end
Tras recargar la página veremos que tenemos tres campos donde podemos escoger el campo que queremos filtrar, el predicado y la condición. Al enviar el formulario veremos que se filtran los productos y también tenemos otro grupo de campos para añadir otra búsqueda.
Lo mejor de todo es que podemos incluso incluir asociaciones. El método attribute_select
puede recibir una asociación, y como en nuestro caso un producto pertenece a una Category
podemos añadirlo.
<%= a.attribute_select associations: [:category] %>
Si ahora recargamos la página podremos hacer la búsqueda tmabién por los campos de categoría de producto.
Cómo añadir y quitar condiciones dinámicamente
Sería muy cómodo poder añadir y quitar condiciones dinámicamente mediante JavaScript. La implementación es un poco complicada pero el patrón funciona de manera similar a los formularios anidados, por lo que haremos algo parecido a lo que vimos en el episodio 196. Lo primero que tenemos que hacer es mover los campos de condiciones a un parcial.
<%= search_form_for @search do |f| %> <%= f.condition_fields do |c| %> <%= render "condition_fields", f: c%> <% end %> <div class="actions"><%= f.submit "Search" %></div> <% end %>
Luego tenemos que renombrar la variable del campo del formulario en el nuevo parcial y añadir un enlace para quitar la búsqueda.
<div class="field"> <%= f.attribute_fields do |a| %> <%= a.attribute_select associations: [:category] %> <% end %> <%= f.predicate_select %> <%= f.value_fields do |v| %> <%= v.text_field :value %> <% end %> <%= link_to "remove", '#', class: "remove_fields" %> </div>
En el ApplicationHelper
escribiremos un método llamado link_to_add_fields
que funciona de forma parecida al del episodio 196.
module ApplicationHelper def link_to_add_fields(name, f, type) new_object = f.object.send "build_#{type}" id = "new_#{type}" fields = f.send("#{type}_fields", new_object, child_index: id) do |builder| render(type.to_s + "_fields", f: builder) end link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")}) end end
Este método será llamado en la plantilla index
para generar el enlace que crea las condiciones.
<%= search_form_for @search do |f| %> <%= f.condition_fields do |c| %> <%= render "condition_fields", f: c%> <% end %> <p><%= link_to_add_fields "Add Conditions", f, :condition %> <div class="actions"><%= f.submit "Search" %></div> <% end %>
El último paso es el JavaScript para que todo esto funcione.
jQuery -> $('form').on 'click', '.remove_fields', (event) -> $(this).closest('.field').remove() event.preventDefault() $('form').on 'click', '.add_fields', (event) -> time = new Date().getTime() regexp = new RegExp($(this).data('id'), 'g') $(this).before($(this).data('fields').replace(regexp, time)) event.preventDefault()
Con esto ya podemos añadir y quitar enlaces dinámicamente. Al recargar la página veremos los enlaces para añadir y eliminar condiciones de búsqueda.
Todo esto funciona bien pero si el usuario añade demasiadas condiciones puede ser que lleguen al límite de los datos que se pueden enviar sobre una petición GET. Una forma de resolver esto es enviar un POST, así que lo implementaremos de esta manera. Primero en el fichero de rutas añadiremos un bloque al recurso de productos y añadiremos una ruta search
que recibe peticiones POST y que mapea con la acción index
.
Store::Application.routes.draw do resources :products do collection { post :search, to: 'products#index' } end root to: 'products#index' end
Tenemos que cambiar el formulario de búsqueda para que apunte a esta acción.
<%= search_form_for @search, url: search_products_path, method: :post do |f| %> <%= f.condition_fields do |c| %> <%= render "condition_fields", f: c %> <% end %> <p><%= link_to_add_fields "Add Conditions", f, :condition %> <div class="actions"><%= f.submit "Search" %></div> <% end %>
Cuando ahora realicemos una búsqueda en los productos los datos serán enviados mediante una peticíon POST en lugar de usar parámetros en la URL. Todavía tenemos el problema con los enlaces de ordenación, porque siguen enviando una petición GET a la ruta search
, lo que no funciona. En lugar de gestionar la ordenación mediante enlaces podríamos mover esta funcionalidad al formulario, y para facilitar esto Ransack incluye métodos para generar los select
necesarios para ordenar por los distintos campos.
<div class="field"> Sort: <%= f.sort_fields do |s| %> <%= s.sort_select %> <% end %> </div>
Como no hemos especificado ninguna ordenación por defecto en el controlador podemos construir una igual que hicimos con las condiciones.
class ProductsController < ApplicationController def index @search = Product.search(params[:q]) @products = @search.result @search.build_condition @search.build_sort if @search.sorts.empty? end end
En la página aparecerán campos de ordenación en los que podemos escoger el campo y la dirección.
Puede ser que en otros casos tengamos enlaces en los que tengamos que arrastrar las condiciones de búsqueda pero no podemos hacerlo mediante el formulario, como por ejemplo en los enlaces de paginación. Hay un par de soluciones a este problema. Una es utilizar JavaScript para enviar el enlace como una peticíon POST, y otro es persistir los parámetros de búsqueda en la base de datos, como hicimos en el episodio 111.
Con esto terminamos nuestro episodio sobre Ransack. Se puede ver un formulario de búsqueda complejo en la demo de Ransack, que incluye una funcionalidad de búsqueda avanzada más completa con múltiples campos de ordenación e incluso grupos de condiciones.