Componente para interfaz: la tabla de datos.

Figura
Figura. Tabla de datos

El módulo data-table.js es un componente para crear una tabla de datos para uso general, especialmente como componente de una interfaz de usuario cuando tenemos que presentar o recibir datos tabulados. Como se observa en la Figura, partiendo de los datos en un Array, se crea una tabla con los datos en campos de texto <input> y con selectores de filas y columnas. Estos son botones que abren un panel con acciones para modificar los datos. Una última fila con el botón "✱" está presente para insertar una nueva fila. Inicialmente puede configurarse la tabla para permitir ediciones o bien que sea sólo de lectura.

En el siguiente ejemplo creamos una tabla de datos y la insertamos en un elemento <div id="location1">. La tabla se identificará con ID="tabla1". Agregamos a este ejemplo unos botones y un área de texto que servirá para probar como establecer y obtener los valores de la tabla. Esto se comenta en un apartado posterior.

Ejemplo: Tabla de datos

Para crear la tabla de datos de este ejemplo hemos usado el siguiente código:

let valueIni = [["Archivo", "KB", "Fecha"],
        ["index.html", 28, "2/6/2019"],
        ["general.js", 101, "15/12/2018"],
        ["info.php", 1, "8/1/2019"],
        ["base.css",  56, "15/4/2019"],
        ["calc.js", 88, "23/2/2019"]];
let error = wxL.dataTable.createTable({
    location: document.getElementById("location1"),
    id: "tabla1",
    data: valueIni,
    caption: `Tabla de datos "tabla1"`,
    styleCols: {
        1: "color: green",
        3: "width: 6em; text-align: center"
    }, 
    validateCols: {
        2: {test: 0, min: 0, defaultValue: 0, 
            message: `Debe ser un número entero no negativo`},
        3: {test: "date", message: `Debe ser una fecha válida d/m/aaaa`}
    },
    showError: message => 
        document.getElementById("error1").textContent = message,
    lang: "es"
});
if (error) document.getElementById("error1").textContent = error;

Al iniciar el módulo guardamos el constructor en wxL.dataTable. Este objeto dispone de la función createTable(). Le pasamos unos argumentos para la creación, como mínimo la localización donde ubicarse location1. El resto son opcionales. A continuación relacionamos todos los argumentos que podemos utilizar, donde se expone el valor por defecto que tomarán si no se pasa ese argumento:

location=null
Una referencia a un elemento HTML <div> existente, cuyo interior se reemplazará con la nueva tabla de datos. Si la referencia no es válida o no es un <div> un no se creará la tabla.
id=""
Un String para dotar el atributo id de la tabla. Se comprobará que no existe ese identificador en la página, en cuyo caso y también cuando sea vacío se dotará de un ID como datatable-N, siendo N un número correlativo.
data=[[]]
Un Array compuesto de Arrays que serán las filas, donde la primera fila se tomará como títulos. El número de columnas de la tabla queda configurado con el número de celdas de la primera fila de títulos. El resto de filas se ajustará para que tengan igual número de columnas, bien ignorando el exceso o completando con valores vacíos. El valor que se pase se ajustará a un Array de Arrays, que como mínimo será [["Col1"]] creándose una tabla con una columna y dotándole de ese título.
caption=""
Un String para el título de la tabla, eliminándose el marcado HTML que se pase.
rowsPage=0
Filas por página, para dotar de paginado a la tabla.
styleTable=""
Estilo CSS para la tabla.
styleCols={}
Estilo CSS para las columnas, de tal forma que se aplicará cada item del Array a cada columna numerándolas desde 1. Por ejemplo, con styleCols: {1: "color: green", 3: "width: 6em; text-align: center"} se aplicará color verde a la primera columna y texto centrado con ancho de "6em" a la tercera.
colorSearch="lime"
Una clave CSS de color (black, red, green, lime, etc.) para marcar los resultados de la búsqueda. Si no se pasa ninguna o no es válida se utilizará el color "lime".
validateCols={}
Expresiones para validar los valores que introduzcamos en las columnas. Por ejemplo, con validateCols: {2: {test: 0, min: 0, defaultValue: 0, message: `Debe ser un número entero no negativo`}} validaremos los datos de la segunda columna para que sean números enteros no negativos. Si indicamos un mensaje se mostrará. El valor por defecto en defaultValue, o una cadena vacía en caso de que no se indique, servirá para sustiuirlo por el valor erróneo introducido. Si se introduce un valor vacío se modificará al valor por defecto sin mostrar mensaje de error. En un apartado siguiente veremos varios tipos de validaciones posibles.
edit=true
Figura
Figura. Menú de fila
Cuando seleccionamos filas, columnas o toda la tabla se abrirá una panel a modo de menú contextual con botones como los de la Figura. Si activamos la edición aparecerán todos los botones disponibles. Si desactivamos la edición sólo se muestran los botones para copiar y el cuadro y botón para buscar, acciones ambas que no modifican la tabla. Al mismo tiempo no aparecerá la nueva fila con el botón "✱" y el texto de las celdas quedará bloqueado. Tampoco podrá asignar valores con el getter que comentaremos en el siguiente apartado. En definitiva, no podrá modificarse la tabla.
onchange=null
Si pasamos una función en este argumento, cada vez que se modifique la tabla se ejecutará. Ver un ejemplo de onchange más abjo.
showError=null
Si pasamos una función en este argumento se utilizará para presentar los mensajes de error. En el ejemplo el mensaje se inserta en un contenedor al pie del ejemplo. Si no se usa este argumento se presentará en el mensaje de alerta del navegador.
lang="en"
El idioma para construir la interfaz de la tabla de datos. Sólo admite inglés ("en") y español ("es").

Este código así como del resto de ejemplos en esta página lo puede consultar en esta página de código local. Más abajo explicaremos como cargar e iniciar el módulo data-table.js.

El menú contextual de botones tiene los siguientes, donde las acciones se ejecutan sobre las filas o columnas seleccionadas. Con el botón en la parte superior izquierda se seleccionan todas las celdas:

  • : Copiar fila/columna
  • : Cortar fila/columna
  • : Pegar fila/columna
  • : Borrar contenidos
  • : Eliminar fila/columna
  • : Deshacer
  • : Rehacer
  • : Orden ascendente
  • : Orden descendente
  • : Buscar

Puede copiar con el botón y lo tendrá disponible en el portapapeles del dispositivo. El interior de las celdas tendrá el menú contextual propio del ordenador. Pero si pegamos en la celda un String que contenga tabulaciones, se pegará como celdas de la tabla a partir la posición que ocupe.

Obtener o establecer valores de una tabla de datos

Figura
Figura. Objeto HTMLTableElement

Para explicar como obtener o establecer nuevos valores para la tabla, acompañamos en el ejemplo del apartado anterior unos botones y un área de texto, cuya funcionalidad explicaremos ahora.

En primer lugar hemos de explicar que usamos el elemento HTML <table> para construir una tabla de datos. Ese elemento HTML es soportado por el tipo HTMLTableElement. En la Figura puede observar una captura de pantalla de las propiedades de ese objeto desde una tabla de datos. Observe ese tipo en la propiedad _proto_: HTMLTableElement, que es el prototipo a partir del cual JavaScript crea un elemento <table>.

Con una tabla HTML sin modificar la última propiedad antes de _proto_: HTMLTableElement es width, que establece el ancho de la tabla. Pero para este módulo necesitamos guardar con la tabla de datos algunas cosas. Como es recomendable no agregar propiedades directamente a los objetos built-in como este HTMLTableElement, usamos para ello el tipo de variable Symbol. Con eso nos aseguramos no sobreescribir propiedades que pudieran existir con el mismo nombre. Agregamos un objeto en Symbol(dataTable) para guardar todo lo que necesitemos. Observe, por ejemplo, que en Symbol(dataTable).location tenemos la referencia a nuestro <div id="location1">.

Sin embargo hemos insertado directamente la propiedad value. Lo hacemos porque estamos seguros que un elemento <table> no dispone de esa propiedad. Y para obtener o establecer esa propiedad value también insertamos los accesores de datos get value y set value. Se denominan también como getter y setter. Todo esto lo hacemos con defineProperty:

table = Object.defineProperty(table, "value", {
    get: () => {
        let [error, array] = getArray(table);
        return error ? error : array;
    },
    set: array => setArray(table, array)
});

Se usan las funciones internas del módulo getArray(table) y setArray(table, array). Estas funciones no son accesibles externamente, obligando a utilizar el getter y setter de la tabla para establecer u obtener sus datos. Hay que tener en cuenta que el valor almacenado o a almacenar en value será un Array con los datos, sin incluir la primera fila de títulos. Para el ejemplo anterior ese Array es el siguiente, si no se ha modificado posteriormente:

[["index.html", 28, "2/6/2019"],
["general.js", 101, "15/12/2018"],
["info.php", 8 , "8/1/2019"],
["base.css",  28, "15/4/2019"],
["calc.js", 88, "23/2/2019"]]

Así con el botón etiquetado con el código Valores = tabla1.value extraemos ese Array de datos directamente con ese código. Y a continuación, externamente, lo convertimos en un tabla TSV (tab separated values, valores separados por tabulación) para poder representarlo en el área de texto del ejemplo. Se trata simplemente de separar las filas con "\n" y las columnas con "\t". De forma inversa con el botón tabla1.value = Valores pasamos previamente la tabla TSV a un Array y luego lo adjudicamos directamente con ese código.

Paginando una tabla de datos

Un problema que no es fácil de resolver con el elemento <table> se refiere a la altura de la tabla y el control de lo que sobresale (overflow). Una tabla dispone del atributo width para fijar el ancho. Aunque es obsoleto en HTML5 y debe reemplazarse con la propiedad en CSS. Sin embargo no dispone ni responde al atributo height. También ignora el estilo height en CSS. Sabemos que las filas en elementos <tr> se insertan en un elemento <tbody>. Y este elemento tampoco responde a height.

Cuando tenemos una tabla con muchas filas y sólo queremos presentar en pantalla una parte de ellas, podemos recurrir a establecer el alto del contenedor exterior, dotándole de un overflow: auto para controlar lo que sobresale. El siguiente ejemplo tiene este HTML:

<div id="location2" style="height: 15em; overflow: auto"></div>
    

En ese elemento creamos esta tabla de datos:

let provincias =  [["Provincia"],["Álava"],["Albacete"],["Alacant"],
...,
["Zamora"],["Zaragoza"],["Ceuta"],["Melilla"]];
error = wxL.dataTable.createTable({
    location: document.getElementById("location2"),
    id: "tabla2",
    data: provincias,
    rowsPage: 0,
    lang: "es"
});

Se trata de una tabla con las 52 provincias de España. Las hemos omitido en el código anterior para abreviar. Acortamos el alto del contenedor exterior a 15em:

Ejemplo: Tabla de datos sin paginar

Observe en el código anterior que rowsPage: 0, valor por defecto. Esto supone que no se establece paginado pues filas por página es cero. Pero si le hubiésemos puesto rowsPage: 7 tendríamos 7 filas por página, no siendo ahora necesario dar altura al contenedor exterior:

Ejemplo: Tabla de datos paginada

En ese caso aparecerá en la parte inferior una barra navegadora por páginas. El paginado se realiza exclusivamente con CSS haciendo uso de los selectores CSS. Si avanzamos a la segunda página de la tabla anterior (con filas 8 a 14 visibles) y vamos a buscar un elemento STYLE en el HEAD del documento encontraremos esto:

<style id="cssDataTablePage-tabla3">
    #tabla3 tbody tr:nth-child(-1n+7){
        display: none;
    }
    #tabla3 tbody tr:nth-child(1n+15){
        display: none;
    }
</style>

Este estilo se actualiza cada vez que cambiamos de página o el total de páginas se modifica. Observe que los selectores -1n+7 y 1n+15 hacen, respectivamente, que las filas ≤7 y ≥15 se oculten, permaneciendo visibles las filas 8 a 14 de esa segunda página.

Evento onchange de una tabla de datos

Podemos dotar a la tabla de datos de un evento onchange. En el siguiente ejemplo creamos la siguiente tabla de datos para probar esto:

error = wxL.dataTable.createTable({
    location: document.getElementById("location4"),
    id: "tabla4",
    data: [["Columna 1", "Columna 2"],
            ["abc", 123],
            ["def", 456],
            ["ghi", 789]],
    onchange: array => {
        document.getElementById("value4").value = array.
            map(v => v.join("\t")).join("\n");
    }
});

Puede probar a modificar cualquier cosa y verá que los valores se insertarán en el área de texto adjunta:

Ejemplo: Evento onchange para una tabla de datos

En este caso adjudicamos una función cuyo argumento a recibir es el array de valores de la tabla, evento que se activa cada vez que modifiquemos la tabla. En este caso vertemos ese array en un área de texto separando las filas con saltos de línea y las columnas con tabuladores. El array obtenido es una copia de los contenidos de las celdas, por lo que la modificación posterior de ese array no tiene efecto sobre la tabla.

Enviando una tabla de datos en un formulario

La tabla de datos contiene los valores de las celdas en elementos <input>. Estos elementos se nombran con el atributo name con valor IDrNcM, donde "ID" es el idtentificador de la tabla, "r" y "c" son dos letras literales para indicar la fila y la columna en las ubicaciones "N" y "M" respectivamente. En el siguiente ejemplo vamos a crear otra tabla de datos que quedará dentro de un elemento <form> con la finalidad de enviarlo al servidor:

<form action="ejemplos/action-form.html" method="post">
    <label>Texto
        <input type="text" name="texto" value="abc" readonly />
    </label>
    <div id="location5"></div>
    <button type="submit">Enviar form</button>
</form>

También incorporamos un elemento de texto para recibirlo en el servidor junto a la tabla de datos, cuyo codigo de creación es el siguiente:

error = wxL.dataTable.createTable({
    location: document.getElementById("location5"),
    id: "tabla5",
    data: [["Col1", "Col2"],
          ["(1,1)", "(1,2)"],
          ["(2,1)", "(2,2)"],
          ["(3,1)", "(3,2)"]],
    edit: false
});

Este es el ejemplo en ejecución (bloqueamos el campo de texto y la tabla de datos para evitar tener que filtrar en exceso en el servidor):

Ejemplo:

La página con PHP en el servidor que recibe los datos los gestiona y los introduce en un área de texto:

<textarea rows="8" cols="30"><?php 
    $post = "";
    if (isset($_POST)) {
        foreach($_POST as $campo=>$valor){
            if ($campo==="texto" || preg_match("/^[a-z]+[\w-]*r\d+c\d+$/i", $campo)===1){
                $post .= $campo . " = " . htmlspecialchars($valor) . "\n";
            }
        }
    }
    echo $post;
?></textarea>

En el servidor recibimos el elemento de texto y luego las celdas rNcM de la tabla de datos con ID = tabla5:

texto = abc
tabla5r1c1 = (1,1)
tabla5r1c2 = (1,2)
tabla5r2c1 = (2,1)
tabla5r2c2 = (2,2)
tabla5r3c1 = (3,1)
tabla5r3c2 = (3,2)

Detectamos el campo de celda con el patrón /^[a-z]+[\w-]*r\d+c\d+$/i, recordando que el identificador de la tabla debe responder a [a-z]+[\w-]* mientras que la parte r\d+c\d+ detecta la fila y columna.

Validando valores de una tabla de datos

En la siguiente tabla de datos imponemos unas condiciones para probar la validación de datos de entrada:

let colors = ["black", "white", "red", "green", "blue"];
error = wxL.dataTable.createTable({
    location: document.getElementById("location6"),
    id: "tabla6",
    data: [["Col1", "Col2", "Col3", "Col4", "Col5", "Col6", "Col7", "Col8"],
          [1, 101, 2.5, "abc", "red", "2/12/2018", "31/3", "12:00:00"],
          [2, -234, 23, "def", "blue", "5/6/2019", "29/2", "14:30:05"],
          [3, 0, 67.89, "ghi", "green", "27/7/2019", "1/7", "23:45:15"]],
    validateCols: {
        1: {test: 0, min: 1, id: true, 
                message: "Debe ser un número entero mayor que 0 y no debe existir en esa columna"},
        2: {test: 0, defaultValue: 0, 
                message: "Debe ser un número entero"},
        3: {test: 0.1, min: 1, max: 100, defaultValue: 1, 
                message: "Debe ser un número entre 1 y 100"},
        4: {test: /^[a-z]{3}$/, defaultValue: "abc", 
                message: "Debe ser una palabra con 3 letras minúsculas"},
        5: {test: colors, defaultValue: "black", 
                message: `Debe ser una clave de color: ${colors.join(", ")}`},
        6: {test: "date", defaultValue: date => (date = new Date(), [date.getDate(), date.getMonth()+1, date.getFullYear()].join("/")), 
                message: `Debe ser una fecha válida "d/m/aaaa"`},
        7: {test: "d/m", defaultValue: date => (date = new Date(), [date.getDate(), date.getMonth()+1].join("/")), 
                message: `Debe ser una fecha válida  "d/m"`},
        8: {test: "time", defaultValue: date => (date = new Date(), [date.getHours(), date.getMinutes(), date.getSeconds()].
                map(v => v.toString().padStart(2, "0")).join(":")), message: `Debe ser una hora válida hh:mm:ss`}
    }
});
if (error) document.getElementById("error6").innerHTML = error;

Pasamos en la propiedad validateCols el número de columna a validar con un objeto que contiene lo siguiente:

  • test: Puede ser:
    • Un número para limitar a la entrada de números, de tal forma que si es un número entero cualquiera como test:0 limitaremos que se introduzcan enteros, y si pasamos algo como test:0.1 se validará cualquier número que se introduzca. Ese número no tendrá utilidad por su valor, sólo es importante que sea o no un número entero. Recuerde que algo como 1.0 con uno o más ceros es también un entero.
    • Un Array para limitarlo a los elementos del mismo con el método includes() de JavaScript
    • Una expresión regular para validar la entrada con el método test() de JavaScript
    • Un String como clave de validación especial, que por ahora admite validaciones de fecha: "date", "time", ... (ver más detalles en un párrafo más abajo).
  • min y/o max si el campo test es un número limitaremos el número que se introduzca. Si no se pasan estos campos se utilizará min = -Infinity y max = Infinity, con lo que se validará cualquier número posible.
  • id, valor true o false para indicar si esa columna sólo admite valores únicos.
  • defaultValue, valor o función que se usará para poner en la celda en caso de que no sea validado. Si hay un id se ignorará el valor por defecto aunque se pase, poniéndose una cadena vacía en su lugar.
  • message, mensaje de error a mostrar. Se espera texto plano para el mensaje, por lo que al crear la tabla se eliminarán las marcas HTML.

Ejemplo: Validando campos de una tabla de datos

En este ejemplo hemos usado estas validaciones:

  1. Identificador único. {test: 0, min: 1, id: true, message: "Debe ser un número entero mayor que 0 y no debe existir en esa columna"}: Se espera un número entero (pues test es cero) mayor o igual a 1 y que sea un identificador único en la columna, para lo cual pasamos id con valor true. En este caso se ignora el defaultValue aunque se pase.
  2. Número entero. {test: 0, defaultValue: 0, message: "Debe ser un número entero"}: Como test es un número entero solo permitiremos números enteros en cualquier rango.
  3. Cualquir número. {test: 0.1, min: 1, max: 100, defaultValue: 1, message: "Debe ser un número entre 1 y 100"}: Como test es un número real permitiremos cualquier número pero en ese rango [1, 100].
  4. Expresión regular. {test: /^[a-z]{3}$/, defaultValue: "abc", message: "Debe ser una palabra con 3 letras minúsculas"}: test es una expresión regular que limita a palabras de 3 letras minúsculas.
  5. Array. {test: colors, defaultValue: "black", message: `Debe ser una clave de color: ${colors.join(", ")}`}: Como test es un Array comprobaremos que el valor está en ese Array.
  6. Fecha: {test: "date", defaultValue: date => (date = new Date(), [date.getDate(), date.getMonth()+1, date.getFullYear()].join("/")), message: `Debe ser una fecha válida "d/m/aaaa"`}: Validará una fecha en formato "día/mes/año", con una función para devolver como valor por defecto la fecha en el momento de la ejecución.
  7. Día y mes: {test: "d/m", defaultValue: date => (date = new Date(), [date.getDate(), date.getMonth()+1].join("/")), message: `Debe ser una fecha válida "d/m"`}: Valida "día/mes".
  8. Hora: {test: "time", defaultValue: date => (date = new Date(), [date.getHours(), date.getMinutes(), date.getSeconds()]. map(v => v.toString().padStart(2, "0")).join(":")), message: `Debe ser una hora válida hh:mm:ss`}: Valida "horas:minutos:segundos"

En el ejemplo anterior las tres últimas columnas contiene ejemplos de validaciones fecha y hora. Las claves posibles a validar son las siguientes, donde el año siempre ha de pasarse con todos los dígitos.

  • "anyDateTime": valida cualquier cosa que parezca una fecha y/u hora con alguno de los formatos expuestos en esta lista de claves
  • "dateTime", "d/m/y h:n:s": son equivalentes, validan "día/mes/año horas:minutos:segundos"
  • "date", "d/m/y": son equivalentes, validan "día/mes/año"
  • "time", "h:n:s": son equivalentes, validan "horas:minutos:segundos"
  • "h:n": valida "horas:minutos"
  • "d/m/y h:n": valida "día/mes/año horas:minutos"
  • "d/m" o "m/d": valida "día/mes" o "mes/día" respectivamente
  • "m/y" o "y/m": valida "mes/año" o "año/mes" respectivamente

Para el formato donde el mes va en primer lugar, también puede usar "m/d/y", "m/d/y h:n:s" o "m/d/y h:n".

Creando una tabla de datos

Exponemos como cargar el módulo data-table.js y crear una tabla de datos. Lo hacemos en una página de ejemplo, donde podrá observar en su código lo siguiente, con una tabla similar a la del primer apartado:

<div id="location"></div>
<div id ="error" style="color:red"></div>
<script>
    var Wextensible = Wextensible || {};
    window.onload = function () {
        //Cargar el módulo
        Wextensible.DATA_TABLE_JS();
        //Iniciar el módulo
        let dataTable = Wextensible.startDataTable();
        //Crear tabla de datos
        let valueIni = [["Archivo", "KB", "Fecha"],
                ["index.html", 28, "2/6/2019"],
                ["general.js", 101, "15/12/2018"],
                ["info.php", 30, "8/1/2019"],
                ["base.css",  56, "15/4/2019"],
                ["calc.js", 88, "23/2/2019"],
                ["sample.html", 12, "12/7/2019"],
                ["control.js", 75, "15/3/2019"],
                ["email.php", 99, "27/1/2019"],
                ["plus.css",  42, "4/5/2019"],
                ["numbers.js", 67, "2/2/2019"]];
        let error = dataTable.createTable({
            location: document.getElementById("location1"),
            id: "tabla1",
            data: valueIni,
            caption: "tabla1",
            styleCols: {
                1: "color: green",
                3: "width: 6em; text-align: center"
            },
            validateCols: {
                2: {
                    test: 0,
                    min: 0,
                    defaultValue: 0,
                    message: `Debe ser un número entero no negativo`
                }
            },
            rowsPage: 4,
            lang: "es"
        });
        if (error) document.getElementById("error1").textContent = error;
    };
</script>
<script src="/res/inc/data-table.js" async></script>
    

La variable Wextensible es un espacio de nombres global para este sitio. Pero puede cambiarlo por cualquier otro nombre. Cargamos el módulo con Wextensible.DATA_TABLE_JS() y después lo iniciamos con startDataTable(), ejecución que nos devolverá un objeto a modo de constructor. Contiene la función createTable() que nos permitirá crear una tabla de datos.

El ejemplo anterior se carga en una página aparte cuyo único recurso es el módulo data-table.js. Sin embargo este módulo utiliza algunos recursos de otros módulos como general.js y utiles.js, módulos que no vincularemos con ese ejemplo. Debido a eso la utilidad para validar fechas no estará disponible.