Desarrollo de software, y otras magias

Filtros personalizados en DataTables

por

en

Tiempo de lectura:

6 minutos

Odiada por muchos, odiada querida por otros. Entiendo que si has llegado hasta aquí es porque la conoces, pero por si has llegado de casualidad y te pica la curiosidad, aquí te dejo la página oficial de DataTables, donde explican mejor que yo qué son y qué hacen, con muuuchos ejemplos.

Tras un año odiándola y queriéndola al mismo tiempo, me ha tocado pelearme con ella alguna que otra vez, así que he decidido contaros cómo gané una de mis batallitas, los filtros personalizados.

 

Cuestiones varias

¿Qué son los filtros personalizados?

No son más que un método que nos proporciona DataTables para añadir nuestras propias funciones de búsquedaSe ejecuta cada vez que se pinta la tabla y decide si cada fila en cuestión debe ser incluida o no en la tabla.

¿Qué problema solucionan?

DataTable ya de por sí nos permite filtrar muy fácilmente con filtros incrustados en las propias columnas.

Ejemplo de filtrado por columnas por defecto en DataTables

Sí, lo hace bien, es cómodo, es potente; pero reconozcámoslo, muy bonito y personalizable, no es. Además, ¿qué pasa si tú necesitas/quieres añadir el filtro fuera de la propia columna?

Así que básicamente los filtros personalizados vienen a solucionar eso, o más bien a dar la posibilidad de pintar las búsquedas de la manera que quieras. Un ejemplo muy típico es filtrar por un rango de fechas (como fue en mi caso).

También es útil si queremos aprovechar la búsqueda para hacer algo con nuestros datos. Por ejemplo, podemos alterar el valor de las columnas que queramos para las filas que pasen el filtro. En definitiva, nos da un poco de libertad, y eso mola.

¿Cómo funciona?

Easy-peasy, simplemente extendiendo la búsqueda propia de DataTable en el $(document).ready(function(){})de nuestro JS y haciendo lo que queramos con ella:

$.fn.dataTable.ext.search.push(
    function (settings, data, dataIndex) {
        //do stuff
    }
);

Como decía antes, aquí se entra cada vez que dibujamos la tabla, es decir, cada vez que se llama a $(‘#myTable’).DataTable().draw(), y se entrará una vez por cada fila de la tabla, decidiendo en cada caso si la fila se pinta (devuelve true) o no (devuelve false).

 

Que sí, que sí, que todo eso está muy bien, pero…

Filtrando por una columna

Imaginemos que tenemos una tabla de usuarios, que muestra información acerca de su fecha de nacimiento, el tipo de usuario y su nombre. Nosotros queremos filtrar por tipo de usuario, pero no queremos que cada vez que vayamos filtrar, tengamos que escribir el tipo en cada columna a mano, como si fuésemos un muggle. No, nos va la marcha, estamos locos y ¡queremos un select!

Supongamos que tenemos en el HTML un combo con las siguientes opciones:

<select id="userTypeFilter" onchange="filterTable()">
    <option value="TODOS">TODOS</option>
    <option value="ADMIN">ADMIN</option>
    <option value="USER">USER</option>
</select>

En el momento que queramos filtrar, que en este caso sería al elegir una opción del SELECT que hemos definido, llamamos al método para dibujar la tabla:

function filterTable() {
    $('#myTable').DataTable().draw();
}

Y en la extensión de búsqueda, podemos hacer algo como:

$(document).ready(function () {
    $.fn.dataTable.ext.search.push(
        function (settings, data, dataIndex) { //'data' contiene los datos de la fila
            //En la columna 1 estamos mostrando el tipo de usuario
            let userTypeColumnData = data[1] || 0;

            if (!filterByUserType(userTypeColumnData)) {
                return false;
            }

            return true;
        }
    );
});

function filterByUserType(userTypeColumnData) {
    let userTypeSelected = $('#userTypeFilter').val();

    //Si la opción seleccionada es 'TODOS', devolvemos 'true' para que pinte la fila
    if (userTypeSelected === "TODOS") {
        return true;
    }

    //La fila sólo se va a pintar si el valor de la columna coincide con el del filtro seleccionado
    return userTypeColumnData === userTypeSelected;
}

 

Filtrando por más de una columna

Ahora supongamos que, además de querer filtrar por el tipo de usuario, también queremos filtrar por su fecha de nacimiento. Nuestro HTML 2.0 quedaría así:

<select id="userTypeFilter" onchange="filterTable()">
    <option value="TODOS">TODOS</option>
    <option value="ADMIN">ADMIN</option>
    <option value="USER">USER</option>
</select>
<input id="birthDateRangePicker" type="text"/>

La llamada para dibujar la tabla sería la misma, pero además tenemos un par de variables:

var startDateFilter = "";
var endDateFilter = "";

function filterTable() {
    $('#myTable').DataTable().draw();
}

Antes de pasar a la extensión, un pequeño inciso. No voy a mostrarlo aquí porque no es el scope del post, pero para este ejemplo se está asumiendo que el birthDateRangePicker está inicializado (por ejemplo con la librería de bootstrap date range picker) y las variables startDateFilter y endDateFilter se actualizan al elegir una fecha del picker, al igual que la llamada a filterTable().

Ahora sí, podríamos hacer la extensión algo como:

$(document).ready(function () {
    $.fn.dataTable.ext.search.push(
        function (settings, data, dataIndex) {
            //En la columna 1 estamos mostrando el tipo de usuario y en la 0 su fecha de nacimiento
            let userTypeColumnData = data[1] || 0;
            let userBirthdateColumnData = data[0] || 0;

            //Si no cumple la 1ª condición, devolvemos 'false' directamente sin comprobar la 2ª
            if (!filterByUserType(userTypeColumnData)) {
                return false;
            }

            if (!filterByDateRange(userBirthdateColumnData)) {
                return false;
            }

            //Sólo si cumple los dos filtros se incluye en la fila en la tabla
            return true;
        }
    );
});

function filterByUserType(userTypeColumnData) {
    let userTypeSelected = $('#userTypeFilter').val();

    if (userTypeSelected === "TODOS") {
        return true;
    } else {
        return userTypeColumnData === userTypeSelected;
    }
}

function filterByDateRange(userBirthdateColumnData) {

    //Filtramos ahora por fecha, sólo si hay fecha seleccionada
    if (startDateFilter === "" || endDateFilter === "") {
        return true;
    } else {
        //Transformamos las fechas en objetos de tipo moment
        let momentUserBirthdate = moment(new Date(userBirthdateColumnData), DATE_FORMAT);
        let momentStartDateFilter = moment(new Date(startDateFilter), DATE_FORMAT);
        let momentEndDateFilter = moment(new Date(endDateFilter), DATE_FORMAT);

        //Hacemos uso de la librería moment para comparar fechas
        return momentUserBirthdate.isSameOrAfter(momentStartDateFilter) && momentUserBirthdate.isSameOrBefore(momentEndDateFilter);
    }
}

 

Bonus Track

Si tenemos más de una tabla en la página y olvidamos indicar para qué tabla queremos que se haga filtrado, puede pasar algo tan gracioso como que no se muestre ninguna fila de la tabla, porque claro, para DataTable, si extiendes la búsqueda van a pasar siempre por ahí todas las filas, y si no cumplen el criterio (es decir, si la función devuelve false) no se pintarán.

Es gracioso cuando descubres lo que es, hasta entonces puedes perder la cabeza intentando buscar por qué tu tabla, que mostraba toda la información perfectamente, de repente ha decidido no mostrarla y no dar error por ningún sitio.

Por suerte esto tiene muy fácil solución, basta con añadir una comprobación del id de la tabla y, si no es la nuestra, devolver siempre true. Al final quedaría algo así:

$.fn.dataTable.ext.search.push(
    function (settings, data, dataIndex) {
        //extend search only for my_table
        if (settings.nTable.id !== 'my_table') {
            return true;
        }

        return doStuffForMyTable();
    }
);

Esto también permite extender diferentes filtros por cada tabla, por ejemplo:

$.fn.dataTable.ext.search.push(
  function (settings, data, dataIndex) {
    if (settings.nTable.id !== 'my_table') {
      return doStuffForMyTable();
    }

    if (settings.nTable.id !== 'my_other_table') {
      return doStuffForMyOtherTable();
    }
  }
);

¡Y eso es todo, amigas y amigos! O no…

– Lo sé, por eso, os dejo por aquí un fiddle para trastear, luego no digáis que no os cuido, ¡eh!