Ordenando <tables> con N parámetros

En el siguiente artículo les cuento como la cultura de trabajo con hojas de cálculo provocó el desarrollo de una nueva funcionalidad aplicable a cualquier sistema

Juan F. Ferrari
May 13, 2019 · 5 min read

Update May/14/2019: Ya está lista la gema ‘sort_n_params’ en Github para su uso, si querés saber como llegue a desarrollarla continuá leyendo.

Desde mis primeros pasos en Snappler me he topado con sistemas que tienen reminiscencias a la famosa herramienta de Microsoft, Excel.

A estos me gusta llamarlos sistemas basados en Excel , en su funcionamiento denotan que el cliente quería una forma nueva de trabajo pero a la vez, se rehusaba a abandonar por completo la que ya tenía adoptada utilizando las hojas de cálculo.

Este tira y afloje conlleva, en la mayoría de los casos, que nuestro sistema cuente con muchas tablas; por lo general nuestro cliente quiere que estas reaccionen de la misma forma que lo harían en Excel ¿Para qué desarrollar un nuevo sistema si voy a perder las funcionalidades que considero básicas? Se preguntan.

El Problema:

Hace unos días surgió la problemática de que las tablas sólo respetaban un orden; es decir, al clickear la cabecera la tabla se ordenaba. Pero si hacías click en otra, el orden se reemplazaba.

Nuestro cliente, que tiene bastante arraigada la Cultura Excel, nos solicitó que las tablas permitan múltiple orden. Cuando esto fue planteado dije, -¡Claro! ¿Cómo nuestro sistema no va a permitir esto?

Al desarrollar la funcionalidad me surgió la necesidad de compartir el conocimiento que había adquirido en el proceso, porque humildemente considero, que todo sistema debería permitir ordenamiento múltiple en sus tablas.

Hablando ya de implementación, en varios de nuestros sistemas se utilizaba el siguiente helper para ordenar tablas por un solo parámetro.

def single_sort(column, title = nil, url_params = {})
title ||= column.titleize

Este método fue mi Piedra Fundacional para realizar el que me permitiría un múltiple orden, para ello empecé a hacer modificaciones, probarlo, volver a modificar, lo normal en cualquier desarrollo.

Sin embargo, cuando lo terminé pasaron dos cosas: el ordenamiento múltiple funcionaba, pero el método del helper había quedado inmantenible, ilegible y muy extenso. Para mejorar mi solución decidí refactorizarlo, quería que fuese posible su uso en otros proyectos y que en el futuro no me rompiera la cabeza intentando entender que hice.

“Con un código ilegible olvido lo que programé en 8 minutos” — Bart Simpson

Un poco de Refactoring:

En nuestro ApplicationHelper.rb

def sortable(column, title = nil)
data
= Sortable.new(column, title, params).call

link_to "#{data.title} <i class='#{data.icon}'".html_safe, data.sort_params, class: data.css
end

Creé una clase nueva que contiene todo el comportamiento del ordenamiento múltiple, Sortable.rb

Posee dos métodos públicos, su constructor y el método call.

def initialize(column, title, params)
@column
= column
@title = title || column.titleize
@params = params
@sort_params = { order: [], filter: @params[:filter] }
end

Opté por utilizar un Array, este se encuentra dentro de @sort_params, un Hash que tiene los parámetros de orden y de filtrado. Este Array contiene en las posiciones pares la columna a ordenar y en las impares la dirección del orden para esa columna. Por ejemplo:

array = [‘id’,’desc’,’description’,’asc’]

array[2] retorna ‘description’ y array[3] retorna ‘asc’, utilizar esta estructura me permite que el orden se respete siempre. Luego este array será transformado en un string y lo utilizaré para ordenar mis datos en una consulta SQL.

def call
add_previous_order
if @params[:order].present?
@sort_params[:order].include?(@column) ? revert_order : add_order
build_data
end

call es el método principal, en él se realizan todos los llamados a métodos privados que gestionan la lógica del ordenamiento, vamos por partes:

def add_previous_order
@sort_params[:order] << @params[:order]
@sort_params[:order].flatten!
end

add_previous_order se encarga de guardarnos en nuestra variable @sort_params los parámetros de orden que viajan en cada request, esto se hace sólo si están presentes.

def revert_order
column_index
= @sort_params[:order].find_index(@column)
direction = @column == @sort_params[:order].detect { |e| e == @column } && @sort_params[:order][column_index + 1] == 'asc' ? 'desc' : 'asc'
@sort_params[:order][column_index + 1] = direction

revert_order es llamado si nuestro array[:order] ya incluye la columna por la cual queremos ordenar, esto sucede a partir del segundo click en la misma cabecera de una tabla. Al existir el orden lo que se hace es invertir su dirección.

def add_order 
@sort_params[:order] << @column
@sort_params[:order] << DEFAULT_ORDER
end

add_order agrega nuestra columna con su dirección default al arreglo de ordenes, en mi caso opté por guardar ‘asc’ en la constante DEFAULT_ORDER.

def build_data
OpenStruct.new(css: set_css, icon: set_icon, title: @title, sort_params: @sort_params)
end

El último método llamado en el call es build_data, este se encarga de instanciar un OpenStruct que nos permitirá manejar de una forma más flexible los datos para utilizarlos en nuestro helper a la hora de devolver el link_to.

Aquí se llaman a más métodos, estos son:

def set_css
@column == @sort_params[:order].detect { |e| e == @column } ? 'current' : nil
end

set_css agrega una clase css a las columnas activas.

def set_icon
if @params[:order] && @params[:order].detect { |e| e == @column } == @column && @params[:order [ @params[:order].find_index(@column) + 1 ]

icon_class = @column == @params[:order].detect { |e| e == @column } && @params[:order][ @params[:order].find_index(@column) + 1 ] == 'asc' ? 'fa fa-sort-up' : 'fa fa-sort-desc'

else
icon_class = nil
end

icon_class
end

set_icon se encarga de dibujar una flecha a la derecha de la cabecera clickeada, esta servirá para indicarle al usuario el orden actual por el cual se está ordenando la tabla. Notar que evaluamos la presencia de la columna en la variable @params y no en @sorting_order.

¡Eso es todo! con esta clase Sortable y un método helper de dos líneas, me sentí satisfecho con mi implementación ya que podía ser explicada de forma relativamente sencilla a aquellos que quieran utilizarla.

¿Momento que soy lento, como utilizo esta funcionalidad en mi sistema? — Homero Simpson

Last tweaks:

Para tener la funcionalidad al 100% funcionando en nuestro proyecto, debemos modificar algunas cositas más.

Como primer paso debemos invocar al método del helper en nuestra vista:

<th> <%= sortable "#{field_name}" %> </th>

Luego debemos permitir el parámetro order:[] en nuestro controller/presenter.

def filter_params
parameters = @params.permit(:page, :construction_id, order:[])
parameters[:page] ||= 1
return parameters unless @params[:filter]
...

Por último, agregar el método que se encargará de transformar el array resultante a un String para su uso en SQL.

def sorting_order
return "#{ORDER[:code]} #{DEFAULT_ORDER_DIRECTION}" unless order.present?

Siguiendo estos pasos, ya tenemos en nuestro sistema una <table> que se ordena aceptando N parámetros de ordenamiento ¡Felicidades!

Snappler

Novedades, Historias, Opiniones y Experiencias de los miembros de Snappler

Juan F. Ferrari

Written by

22 Años, Estudiante de Sistemas y nerd desde los 3 años cuando probé el Power Rangers de Sega Genesis

Snappler

Snappler

Novedades, Historias, Opiniones y Experiencias de los miembros de Snappler

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade