soy Kseso y esto EsCSS

Tablas con encabezado fijo de filas, 1ª columna fija y desplazamiento por datos

Furoya explica cómo manejar tablas de gran contenido con algo de javascrip, con encabezado fijo de filas, la 1ª columna fixed y desplazamiento según los datos.

Tablas con encabezado fijo de filas, 1ª columna fija y desplazamiento por datos

·Por Kseso ✎ 0
Tablas con encabezado de primera fila y columna fijas y su desplazamiento por datos

Todos conocemos las tablas con desplazamiento de contenido y cabecera fija. Tipicamente es para mantener visible el thead superior, aunque puede ser inferior o, quizá un poco más común, el izquierdo.

Éste es el caso que vamos a ver ahora, encbezado fijo a la iquierda, porque los otros ya han sido resueltos con CSS (Sticky Header en tablas; Responsive table con scroll en el Tbody y encabezado fijo), y con alguna pizca de JS más un "truco" CSS (Fixed table-header).

La diferencia está en la forma de mover las celdas, porque ya no vamos a usar una scrollbar convencional, y el desplazamiento será por contenido y no por tamaño.

Ventajas y desventajas del desplazamiento por datos

El mecanismo no es nuevo, y otrora ya publiqué alguna versión, pero no desarrollé el caso, así que aprovecho este espacio y lo hago acá.

El problema empieza cuando tenemos una tabla con demasiadas columnas, como por ejemplo los datos climatológicos del año, que son temperatura, humedad, presión, dirección de viento,... todos encabezando en la primera columna, pero las 365 siguientes son inmanejables en el ancho de un navegador. Lo ideal es que la columna con los títulos de fila se quede fija y que con algún control podamos desplazarnos horizontalmente por los datos.

Y para eso la scrollbar no está mal, aunque hace un tiempo hablamos sobre la incomodidad de cambiar de "pantalla" horizontalmente con un texto o imagen que no se puede cortar, y hay que acomodarlo a mano para verlo completo (al menos, hasta que funcionen los Snap Points).

Lo más práctico es que la barra mueva de a saltos con tamaño fijo, que coincida con una cantidad de columnas de datos o con el ancho mismo de la pantalla (Distribución de bloques en 1 sola fila y navegación horizontal sin declarar su ancho).

El método que vamos a ver ahora no usa scrollbar.

Empezamos con la tabla completa en la página (que puede venir escrita desde un PHP, pero al estar en el documento ya no vamos a depender de él y sus recargas para movernos) y con javascript capturamos todos los datos para crear nuestra propia base en un array bidimensional.

Luego vaciamos la tabla y creamos otra estructura con la misma cantidad de filas pero menos celdas, más ajustada a un tamaño que podemos manejar, y la rellenamos con los mismos datos que tenían pero hasta donde lleguen las columnas que se muestran; el resto no se va a presentar aún.

Si queremos ver más celdas usamos un control (para el caso, un input type=range) y pedimos una o varias columnas más, y entonces todo el contenido se cambia a una o más columnas a la izquierda, haciendo desaparecer las primeras, por supuesto.

Pero no hay un desplazamiento real, sino que el contenido de la primera columna visible desaparece reemplazado por la segunda, el de la última se pasa a la penúltima, y hay un nuevo contenido en la columna final que ahora se pude ver.

See the Pen Horizontal scrolling table. Fixed left header. by solipsistaCP (@solipsistacp) on CodePen.

Esto tiene algunas ventajas adicionales:

A la debatida anteriormente sobre no dejar imágenes o palabras a medio mostrar hasta que hacemos el ajuste fino con la barra de desplazamiento, podemos agregar el capturar al nuevo contenido para luego abrirlo en otra ventana (solamente con lo que queremos ver) y hasta imprimirlo o guardarlo desde allí.

También tenemos un ahorro de recursos, ya que el repaint del arrastrado en tiempo real de imágenes es costoso para el navegador y en máquinas lentas ya sabemos que el desplazamiento se cuelga más de una vez. Aquí el mayor costo es la reescritura del contenido de las celdas, pero eso se hace una vez y de un golpe, así que sigue siendo más práctico.

Otra ventaja adicional es que se pueden agregar facilmente herramientas de filtrado usando las "coordenadas" del array bidimensional. Ya aprendimos cómo se filtran tablas en otro artículo (Filtrar filas por palabra clave contenida en sus celdas) pero aquí es más económico, porque trabajamos con datos duros que podemos recorrer con bucles anidados.

La principal desventaja viene de la costumbre, de esperar un movimiento que podamos seguir con la vista y así confirmar un desplazamiento. Pero una vez superado eso, no hay dudas que el contenido se desplaza.

Avisos preliminares.

Esto tendrá cierta extensión, pero no por lo complejo, sino por la cantidad de pasos que lleva el armado de la base de datos. La nueva tabla y después la función de relleno van a ver que son más sencillas. Ni hablar del método de filtrado, que no es más que un selector de rango para decirle al escript desde cuál "número de columna" debe mostrar.

Un detalle antes de ir a la explicación : la tabla se carga completa y después se modifica. Esto permite que la puedan ver aún con JS desactivado, pero si la bajada demora mucho van a tener que esperar hasta que se complete el documento para que empiece la reconstrucción.

No es grave, una vez que carga, el escript es rápido; y no es recomendable el método de ocultar la tabla con CSS y reaparecerla cuando esté modificada. Al fin va a tardar lo mismo porque esto depende de la velocidad de la red y la cantidad de datos que pretendan bajar.

Las tablas de doble entrada en arrays bidimensionales

Ya vimos algún ejemplo en otro artículo, pero de nuevo las vamos a recordar un poco antes de comenzar nuestro proyecto.

En javascript un array es un objeto que puede guardar una serie de valores ordenados por un índice. Se pueden referenciar a través de una variable, y hay varias formas de escribirlo.

var miMatriz = new Array(cantidad de elementos); //crea un nuevo array var miMatriz = []; //si hay dos corchetes, tiene que ser array, así que JS crea uno vacío --- /* se asignan valores ítem a ítem... */ miMatriz[0] = "alfa"; miMatriz[1] = "bravo"; miMatriz[2] = "charly"; /* ...o se listan los valores; el orden del ítem está implícito y el array se crea sin necesidad de declararlo previamente */ miMatriz = ["alfa", "bravo", "charly"]; /* se recuperan los valores por su ítem */ alert(miMatriz[0]); //→ alfa alert(miMatriz[1]); //→ bravo alert(miMatriz[2]); //→ charly

Cada elemento de array puede contener en vez de un valor, otro array. Es en este caso que los llamamos vulgarmente "bidimensionales", porque para obtener cada valor vamos a usar dos coordenadas.

miMatriz = [[],[],[]]; //array con tres subarrays miMatriz[0][0] = "alfa"; //crea elemento cero en primer subarray miMatriz[0][1] = "con A"; //crea elemento uno en primer subarray miMatriz[1][0] = "bravo"; //crea elemento cero en segundo subarray miMatriz[1][1] = "con B"; //crea elemento uno en segundo subarray miMatriz[2][0] = "charly"; //crea elemento cero en tercer subarray miMatriz[2][1] = "con C"; //crea elemento uno en tercer subarray /* o la forma más moderna */ miMatriz = [["alfa", "con A"], ["bravo", "con B"], ["charly", "con C"]]; /* que se recuperan con */ alert(miMatriz[0][0]); //→ alfa alert(miMatriz[0][1]); //→ con A alert(miMatriz[1][0]); //→ bravo alert(miMatriz[1][1]); //→ con B alert(miMatriz[2][0]); //→ charly alert(miMatriz[2][1]); //→ con B

Como vemos, la lógica es la misma de las tablas:

[0][1]
[0]alfacon A
[1]bravocon B
[2]charlycon C

y en el ejemplo la primera coordenada sería "x" para las filas y la segunda "y" para las columnas. Donde se crucen estará el dato que pedimos.

Así que para crear nuestra base de datos con el contenido de la tabla original, solamente tenemos que crear un array, a cada elemento del que sería cada fila de la tabla se lo designa como un nuevo (sub)array donde cada uno de sus (sub)elementos serían las celdas de esa fila.

Recorriendo toda la tabla original en el mismo orden (fila 0 ► ∟celda 0; celda 1; ... fila 1 ► ∟celda 0; celda 1; ...) con un par de simples bucles for() anidados ya podemos capturar el contenido de cada celda y pasarlo a su elemento de array, y entonces lo tenemos codificado (por sus coordenadas) para llamarlo a pedido. Aún destruyendo esa tabla original, podemos reconstruirla total o parcialmente a partir del array.

El código para hacer todo esto es:

onload = inicia; //EJECUTA LA FUNCIÓN AL CARGARSE EL DOCUMENTO /* VARIABLES GLOBALES A USAR EN LAS FUNCIONES. LA ÚLTIMA ES UN ARRAY /* var laTabla, totalFilas, elContenido=[]; /* CANTIDAD DE COLUMNAS QUE QUEREMOS VER EN LA TABLA /* var misColumnas = 5; /* FUNCIÓN QUE CREA LA BASE DE DATOS Y LA NUEVA TABLA /* function inicia() { /* REFERENCIA A LA TABLA /* laTabla = document.querySelector("table"); /* REFERENCIA A LA COLECCIÓN DE FILAS /* lasFilas = laTabla.querySelectorAll("tr"); /* REFERENCIA A LA CANTIDAD DE FILAS /* totalFilas = lasFilas.length; /* REFERENCIA A LA COLECCIÓN DE COLUMNAS /* totalColumnas = lasFilas[0].querySelectorAll("td"); /* BUCLE QUE RECORRE TODAS LAS FILAS /* for(r=0; r<totalFilas; r++) { /* CONVIERTE A CADA ELEMENTO DEL ARRAY EN OTRO SUB-ARRAY /* elContenido[r] = []; /* BUCLE QUE RECORRE TODAS LAS COLUMNAS /* for(d=0; d<totalColumnas.length; d++) { /* ASIGNA AL ÍTEM DE ARRAY PARA LA FILA LOS ÍTEMS CON CONTENIDO DE CADA CELDA PROPIA /* elContenido[r][d] = lasFilas[r].querySelectorAll("td")[d].innerHTML; } } /* VARIABLE DE TEXTO VACÍA /* var nuevaTabla = ""; /* COMIENZA UN BUCLE PARA RELLENAR OTRA VEZ LA TABLA CON MISMA CANTIDAD DE FILAS /* for(r=0; r<totalFilas; r++) { /* LLENA VARIABLE CON INICIO DE FILA /* nuevaTabla += "<tr>"; /* SIGUE CON TANTAS CELDAS (COLUMNAS) COMO CONFIGURAMOS /* for(d=0; d<misColumnas; d++) { nuevaTabla += "<td></td>"; } /* Y TERMINA CON CIERRE DE FILA /* nuevaTabla += "</tr>"; } /* REEMPLAZA EL CONTENIDO DE LA TABLA CON LAS CELDAS VACÍAS RECIÉN CREADAS /* laTabla.innerHTML = nuevaTabla; /* LLENA LA PRIMERA CELDA DE CADA FILA CON EL MISMO CONTENIDO DE TABLA ORIGINAL /* for(t=0; t<totalFilas; t++) { laTabla.querySelectorAll("tr")[t].querySelector("td").innerHTML = elContenido[t][0]; } /* EJECUTA FUNCIÓN llenaTabla CON ARGUMENTO 0 /* llenaTabla(0); /* IGUALA ANCHO DEL RANGE CON EL DE LA TABLA /* document.querySelector("input").style.width = laTabla.offsetWidth+"px"; /* ASIGNA EL MÁXIMO DE PASOS AL RANGE (TOTAL DE COLUMNAS VISIBLES) /* document.querySelector("input").max = (totalColumnas.length - misColumnas); /* REINICIA EL RANGE /* document.querySelector("input").value = 0; } /* FUNCIÓN PARA LLENAR LA TABLA SEGÚN PEDIDO /* function llenaTabla(muestra) { /* BUCLE QUE RRECORRE CADA FILA /* for(f=0; f<totalFilas; f++) { /* BUCLE ANIDADO QUE RECORRE CADA CELDA DE LA FILA /* for(c=1; c<misColumnas; c++) { /* EN CADA CELDA PONE EL CONTENIDO SEGÚN ESTÉ EN EL ARRAY /* laTabla.querySelectorAll("tr")[f].querySelectorAll("td")[c].innerHTML = elContenido[f][+muestra+c]; } } }

Y ahora que tenemos claro cómo es el mecanismo, vamos a detallar cómo se resuelve.

  • Empezamos con el evento que ejecutará la captura y reescritura de la tabla cuando se termine de cargar el documento; seguido de las variables globales para todas las funciones.
  • La variable misColumnas nos permite elegir cuántas columnas (incluyendo la de encabezados) se van a mostrar.
  • La función inicio() hace el trabajo más pesado. Primero llena sus propias variables refiriendo a la tabla y sus partes. Como la tabla no puede tener colspan todas las filas tienen la misma cantidad de celdas, así que para saber la cantidad total de columnas contamos las celdas de cualquier fila, y para el ejemplo es la primera.
  • Ya sabiendo el total de filas (que no vamos a modificar) creamos un bucle para recorrerlas una a una, y la variable r dentro de for() nos sirve como índice para ir creando cada elemento del array en elContenido[], y aprovechamos a declararlo como un (sub)array para llenarlo después con varios elementos. Recordemos que cada fila de la tabla que vamos recorriendo coincide con un elemento del array que creamos.
  • Sin salir del bucle, comenzamos otro con el total de columnas, y mientras estamos en una fila recorremos así todas sus celdas (o columnas) en orden. Capturamos su contenido y lo pasamos al array en su coordenada de fila y también en su coordenada de columna como elemento del (sub)array, que ubicamos aprovechando la misma variable d.
  • Una vez que los bucles anidados recorrieron toda la tabla y guardaron el conenido de sus celdas en el array bidimensional, creamos una variable de texto vacía para meter el contenido de la nueva tabla.
  • El método para rellenar la tabla con nuestra nueva estructura de menos celdas es el mismo, dos bucles anidados con la misma cantidad de filas pero menos columnas, sólo la cantidad que declaramos en misColumnas. Para empezar, agregamos a la variable de texto la apertura <tr> en cada vuelta de "filas", y con el segundo bucle escribimos una apertura y un cierre de celda por cada columna, y una vez completada va un </tr> para cerrarla y hacer el mismo trabajo para la siguiente fila.
  • Con estructura reducida de tabla hecha, se mete el texto de nuevaTabla en la tabla original reemplazando todas sus etiquetas y datos contenidos.
  • Y empezamos la recuperación de datos de la base para llenar la nueva tabla vacía. Pero de momento sólo para la primera columna, que tiene los encabezados y que no se va a modificar.
  • El llenado del resto de los datos lo hace la segunda función llenaTabla(0) que envía el argumento para saber dónde empezar a leer el contenido de las columnas. Lo vemos detalladamente más abajo.
  • Un formato importante para mencionar es que todas las celdas tienen el mismo ancho. No es algo necesario, ya que la tabla se puede acomodar al tamaño del contenido, pero así se ve más prolijo, y una vez creada es más fácil medirla en nuestro ejemplo para pasarle su ancho al input al pie que tiene un botón deslizable. La cantidad máxima de pasos que le asignamos a esa slidebar será el total de celdas original menos las que estamos viendo; y la reseteamos a valor cero para que siempre coincida con el inicio al momento de recargar.
  • Ahora, sí. La segunda función es la que hace el trabajo pesado. Que es igual a los anteriores: un primer bucle recorre los tr y nos da la primer coordenada para el array que serán sus filas. Un bucle anidado va a ubicar cada td de la fila y cada segunda coordenada para el (sub)array de las columnas, pero con una salvedad, ya que el conteo empieza por "1" y así se saltea la columna de los encabezados, que ya está escrita y no se cambia.
  • Lo más importante en esta función es el argumento. Al ejecutarla desde la anterior se le envió un "0" para que arrancara desde el comienzo, que sumado a la variable del segundo bucle que empieza en "1" da como resultado 1; y así es como empieza a llenar desde la coordenada elContenido[f][1] hasta la que pusimos de límite en misColumnas. Pero después el input va a enviar distintos valores de argumento, y ése será el inicio que va a mostrar la tabla para cada paso.
  • Un detalle en el código para acceder al elemento del array bidimensional es que lo escribimos como elContenido[f][+muestra + c]. Ese signo "+" delante de la variable con el valor del input es para convertirlo a número, porque recordemos que los elementos de formulario envían cadenas de texto como valor; y así no podemos hacer las cuentas.
Nada impide que el encabezado fijo sea la primera fila, o que estén arriba y a la izquierda y que tengamos doble desplazamiento, o que no haya cabeceras. El método da para muchas variantes, sólo tiene que aparecer la necesidad, y luego usar la imaginación.
Furoya: Autor del artículo

Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.

avatar del Editor del blog

the obCSServer ᛯ Ramajero Argonauta, Enredique Amanuense de CSS.
#impoCSSible inside
Dicen que, en español, EsCss es el mejor blog de CSS. Posíblemente exageren.
@Kseso EsCss Kseso