sisè / segona

Soy Víctor Rodríguez, programador web freelance con más de 10 años de experiencia

Modulo de paginación con Ruby on Rails 4

Hoy os voy a mostrar cómo añadir una sencilla paginación a vuestros interminables listados en Rails. Sí, no uso ninguna librería como will_paginate u otras que debes de conocer. La idea es aprender haciendo las cosas uno mismo, si no, nos volvemos memos.

Para empezar, ¿qué necesitamos?

Lo primero y principal, un listado de alguno de nuestros modelos. Pongamos que tenemos un listado de usuarios, así que nuestro modelo se llamará ¡’User’! (te lo has currado…)

Ya, claro, pero hasta ahí todos lo sabemos hacer, solo hay que hacer un scaffolding y Rails lo tiene hecho, pintado y funcionando.

Vale, no me peguéis todos a la vez…

Las páginas

Una paginación necesita varios datos para funcionar de manera correcta:

  • Número de página
  • Número de usuarios por página
  • Número de páginas totales

Con estos datos podemos montar la query que necesitamos para sacar los usuarios que tocan en cada página.

A montar la página

Para montar la página debemos hacer, como en cualquier otro caso, definir la ruta o url a la que llamar. Sin esto no tenemos manera de crear los bonitos links que la hagan funcionar. Muy bien, vayamos a nuestro fichero routes.rb y añadimos la ruta:

get 'users/page/:page_id', to: 'users#index', as: 'users_pagination'

Lo que hay aquí es la ruta a la acción index del controlador UsersController con un parámetro llamado page_id que será la página que hay que mostrar. Y el método usado es GET ya que se trata de una consulta o lectura del modelo, no estamos grabando/borrando nada que necesite otro método más apropiado.

Ya tenemos el primero de los 3 datos que nos hacen falta. Para el segundo, Número de usuarios por página, lo podemos definir directamente en la acción (al final de todos os enseño a limpiar el código con un módulo, vayamos paso a paso).

Y el tercer dato, viene dado por las queries que tenemos que montar para realizar la paginación.

El código quedaría algo así:

class UsersController < ApplicationController
  def index
    # Número de usuarios por página
    users_per_page = 30 
    @page = params[:page_id].to_i
    @page = 1 unless @page > 0
    # Primer registro que debemos sacar según la pagina
    offset = (@page - 1) * users_per_page
    # Número de paginas totales
    @pages_count = (User.all.count.to_d / users_per_page).ceil
    @users = User.all.limit(users_per_page).offset(offset)
  end
  ... #resto de acciones te las puedes imaginar
end

¿Qué estamos haciendo?

Primero definimos el número de usuarios por página, en este caso 30.

Luego calculamos la página que tenemos que mostrar. O bien la sacamos del parámetro o lo inicializamos si este no existe. Como es casi seguro que tienes una línea en tu fichero routes.rb muy parecida esta:

resources :users

Puede ser que lleguemos a la acción index del UsersController sin parámetro alguno ;)

Luego calculamos el primer registro que debemos sacar, es decir el offset de la query. Montamos una query para sacar el número de páginas totales (es necesario para tener los límites de la paginación en la vista, podría funcionar sin, pero entonces llegaríamos a tener infinidad de páginas vacías de registros… queda poco cuidado)

Y para acabar, sacamos los registros de usuarios de la página que toca con la última línea de código.

Si te has fijado, hay 3 variables que son de instancia (pista, empiezan por @). @page, @pages_count y @users son las variables que necesitamos para la vista, la tabla en views/users/index.html.haml

La de @users es fácil saber por qué, sin la lista de usuarios no hay lista.

@page y @pages_count nos ayudarán a montar los enlaces a la página siguiente y anterior, siempre y cuando sean necesarios.

El código para montar los enlaces es el siguiente:

%ul.pager
  %li{ class: pagination_previous_class(@page) }
    =link_to 'Previous', users_pagination_path(@page - 1)
  %li{ class: pagination_next_class(@page, @pages_count) }
    =link_to 'Next', users_pagination_path(@page + 1)

Explicación:

Montamos una lista sin numerar con 2 enlaces a la página anterior y a la página siguiente. (Por cierto uso HAML, por si no te has dado cuenta hasta ahora).

Determinamos la clase que va a tener el list item (li) mediante 2 helpers que podemos guardar en el fichero application_helper.rb

module ApplicationHelper
  def pagination_previous_class(page)
    "disabled" if page <= 1
  end
  
  def pagination_next_class(page, pages_count)
    "disabled" unless page < pages_count
  end
end

Lo que hacen es añadir una clase 'disabled' si estamos en la primera página (para el enlace a la página anterior) o si estamos en la última página (para el enlace a la página siguiente, YA NO HAY MÁS PÁGINAS! :P) Esto es solo el maquillaje, para deshabilitarlos debéis usar Javascript del bueno. Os lo dejo de ejercicio.

¿Ya está? ¿Eso es todo?

NOOOO! os he dicho que limpiaríamos un poco el código, ¿no?, pues allá vamos...

Módulo ModelPaginator

Crea un fichero llamado model_paginator.rb dentro de la carpeta lib. Y añade el código de la paginación sin la query específica para nuestro modelo (User.all). De esta manera podemos reusar el módulo con otros modelos. Quedará algo así:

module ModelPaginator
  def paginate(resource)
    items_per_page = 30
    @page = params[:page_id].to_i
    @page = 1 unless @page > 0
    offset = (@page - 1) * items_per_page
    @pages_count = (resource.count.to_d / items_per_page).ceil
    resource.limit(items_per_page).offset(offset)
  end
end

Hemos cambiado User.all por resource, y lo hemos pasado como parámetro. Finalmente nuestro UsersController debemos modificarlo de esta manera:

class UsersController < ApplicationController
  include ModelPaginator

  def index
    @users = paginate(User.all)
  end
  ... #resto de acciones te las puedes imaginar
end

Hemos incluido el módulo y hemos limpiazo el código de la acción index. Mucho más fácil de saber qué estamos haciendo ahora que antes, ¿no?

Una cosita más, para que el módulo lo podamos incluir en los controladores y funcione, tenemos que decirle a Rails que lo añada al lanzar la aplicación de esta manera en el fichero config/application.rb

config.autoload_paths << Rails.root.join('lib')

Así, todos los módulos o clases que añadas a la carpeta lib Rails los 'autocargará' an inicializar la aplicación.

Ahora sí, ya está. Si quieres ampliar funcionalidades, tu mismo. ¿Puedes tener un fichero externo de configuración con el número de items por página? ¿En formato YAML por ejemplo?