Vuetify: Lograr filtrado múltiple en el componente Data table <v-data-table>
19/11/2018 por Capy

Front.id

El elemento Data table no soporta mas que un filtrado simple de búsqueda de texto. Esto traducido quiere decir que solo permite añadirle un text field que filtre filas siempre que alguna celda contenga parcialmente la palabra que estás buscando:

See the Pen Vuetify Example Pen by Front.id (@frontid) on CodePen.

¿Pero que pasa cuando queremos poner mas filtros como en este ejemplo?

vuetify data table with multiple filters

 

Pues pasa que el componente no soporta de forma nativa multiples filtros. PERO si nos permite customizar el comportamiento del único field disponible y vamos a usar esa funcionalidad como caballo de troya para expandir las posibilidades de filtrado.

1: Teoría

Sabemos que el elemento <v-data-table> tiene la posibilidad de usar una propiedad ":filter" en la que indicar la "prop" que tiene que escuchar. Dicha "prop" tiene que ser un string, y cuando cambie, la tabla internamente va a aplicar un filtrado usando esa palabra para mostrar solo las filas que cumplan con el criterio.

También sabemos que el elemento <v-data-table> permite personalizar el algoritmo de la búsqueda mencionada anteriormente mediante el atributo ":custom-filter". A él le asignas un método que tendrás que crear en la sección de "methods" de tu componente.

Finalmente sabemos que para ir cambiando el valor del prop asociado a :filter tenemos que añadir un componente <v-text-field> y asociarlo a la prop mediante v-model="miprop".

2: El problema

Como ya habrás inferido, el problema aparece cuando quieres añadir mas de un filtro a la tabla ya que la tabla solo permite como punto de entrada al filtrado interno el atributo :filter y no puede ser un array u objeto, por lo que no puedes ir enviando información desde campos varios como puede ser un select, un date picker, etc.

3: La solución

Les pongo un componente con la solución implementada y la explico mas abajo.

<template>
    <v-layout row wrap>

        <v-flex xs6>
            <v-text-field
                    append-icon="search"
                    label="Filter"
                    single-line
                    hide-details
                    @input="filterSearch"
            ></v-text-field>
        </v-flex>

        <v-flex xs6>
            <v-select
                    :items="authors"
                    label="Author"
                    @change="filterAuthor"
            ></v-select>
        </v-flex>


        <v-flex xs12>


            <v-data-table
                    :headers="headers"
                    :items="rows"
                    item-key="name"

                    :search="filters"
                    :custom-filter="customFilter"
            >
                <template slot="headers" slot-scope="props">
                    <tr>
                        <th v-for="header in props.headers" :key="header.text">
                            {{ header.text }}
                        </th>
                    </tr>
                </template>

                <template slot="items" slot-scope="props">
                    <tr>
                        <td>{{ props.item.name }}</td>
                        <td>{{ props.item.added_by }}</td>
                    </tr>
                </template>

            </v-data-table>

        </v-flex>


    </v-layout>
</template>

<script>

  export default {
    data: () => ({
      filters: {
        search: '',
        added_by: '',
      },

      authors: ['Admin', 'Editor'],
      headers: [
        {
          text: 'Names',
          align: 'left',
          value: 'name',
          sortable: false
        },
        {
          text: 'Item addad by',
          value: 'added_by',
          align: 'left',
          sortable: false
        }
      ],
      rows: [
        {
          name: 'Marcelo Tosco',
          added_by: 'admin'
        },
        {
          name: 'Carlos Campos',
          added_by: 'admin'
        },
        {
          name: 'Luis Gonzalez',
          added_by: 'Editor'
        },
        {
          name: 'Keopx',
          added_by: 'Editor'
        },
        {
          name: 'Marco Marocchi',
          added_by: 'Admin'
        },

      ]
    }),

    methods: {

      customFilter(items, filters, filter, headers) {
        // Init the filter class.
        const cf = new this.$MultiFilters(items, filters, filter, headers);

        cf.registerFilter('search', function (searchWord, items) {
          if (searchWord.trim() === '') return items;

          return items.filter(item => {
            return item.name.toLowerCase().includes(searchWord.toLowerCase());
          }, searchWord);

        });


        cf.registerFilter('added_by', function (added_by, items) {
          if (added_by.trim() === '') return items;

          return items.filter(item => {
            return item.added_by.toLowerCase() === added_by.toLowerCase();
          }, added_by);

        });

        // Its time to run all created filters.
        // Will be executed in the order thay were defined.
        return cf.runFilters();
      },


      /**
       * Handler when user input something at the "Filter" text field.
       */
      filterSearch(val) {
        this.filters = this.$MultiFilters.updateFilters(this.filters, {search: val});
      },

      /**
       * Handler when user  select some author at the "Author" select.
       */
      filterAuthor(val) {
        this.filters = this.$MultiFilters.updateFilters(this.filters, {added_by: val});
      },

    }

  };
</script>

En este ejemplo puedes ver al inicio dos elementos de formulario que van a ser nuestros filtros. Uno es un text field que va a filtrar filas basándose en la coincidencia del texto de la columna "name" y el segundo es un select que filtrará filas basándose en el "author".

Presta atención a estos elementos. No tienen la referencia v-model="miprop" que se esperaría que estuviera si quieres usar alguno de estos campos como filtros de la tabla. En su lugar hemos puesto dos eventos @change y @input con sus respectivos métodos.

En los dos métodos entra en juego un plugin VUE llamado $MultiFilters. Este es un plugin que contiene una clase muy pequeña que solo tiene 3 métodos: updateFilters(), registerFilters() y runFilters()

NOTA: La instalación del plugin se comenta mas abajo. Ver sección "Instalar el plugin MultiFilters".

En el caso de los métodos que responden a los eventos estamos usando updateFilters() para mantener la prop "filters" actualizada con el ultimo valor obtenido de cada uno de los filtros

/**
* Handler when user input something at the "Filter" text field.
*/
filterSearch(val) {
    this.filters = this.$MultiFilters.updateFilters(this.filters, {search: val});
},

/**
* Handler when user  select some author at the "Author" select.
*/
filterAuthor(val) {
    this.filters = this.$MultiFilters.updateFilters(this.filters, {added_by: val});
},

¿Que pasa cuando actualizamos la prop "filters"? pues pasa que al haberla asociado a la tabla mediante el atributo :search="filters" se va a ejecutar la búsqueda interna de la tabla. Y como ya comenté antes, la prop debería ser un string si queremos que el filtrado funcione... por eso el siguiente paso es intervenir en el filtrado usando nuestro propio método definido en el componente v-data-table mediante el attr :custom-filter="customFilter".

Lo primero que hicimos en el método que creamos para aplicar el filtrado es instanciar $MultiFilters para disponer de los dos métodos que nos van a ayudar a cerrar el ciclo de filtrado y acto seguidos los usamos. 

Esta clase ademas de los métodos, se encargará de extraer de la prop "filters" el valor que le corresponda a cada filtrado que vayamos a usar. Mejor un poco de código para ejemplificarlo:

cf.registerFilter('search', function (searchWord, items) {
  if (searchWord.trim() === '') return items;

  return items.filter(item => {
    return item.name.toLowerCase().includes(searchWord.toLowerCase());
  }, searchWord);
});


cf.registerFilter('added_by', function (added_by, items) {
  if (added_by.trim() === '') return items;

  return items.filter(item => {
    return item.added_by.toLowerCase() === added_by.toLowerCase();
  }, added_by);

});

En cada uno de los cf.registerFilter() estamos indicando sobre qué valor de filters vamos a actuar y la función que aplica el filtrado. El filtrado en si no es nada del otro mundo. solo un poco de js y se devuelven solo los items que hayan superado el test.

Por ejemplo en el primer método hemos indicado que para el valor filters.search se ejecute una función que verifique fila a fila en la celda "name" si esta tiene dentro de su texto alguna parte que coincida con el filters.name.

Y finalmente lanzamos cf.runFilters(); cuya finalidad es lanzar cada uno de los filtrados de forma secuencial. Es importante este detalle ya que si el primer filtro que se aplique devuelve 5 de los 100 elementos de una tabla, el siguiente filtro solo va a evaluar esos 5 elementos. 

Componente final (live demo)

Aquí les pongo el ejemplo ya funcionando. La primer tabla es un ejemplo mas complejo que tiene un par de datepickers y un log que va mostrando los valores que va tomando el filtrado. La segunda tabla es la que hemos explicado en este post.

El código fuente de estos ejemplos los puedes encontrar AQUÍ y el ejemplo puedes verlo directamente desde AQUÍ.

 

Instalar el plugin MultiFilters

El repositorio de ejemplo ya lo tiene implementado pero si quieres llevarte esto a tu proyecto, ademas del componente explicado mas arriba vas a necesitar instalar el plugin de VUE MultiFilters

1: Crea src/plugins/MultiFilters.js

/**
 * Enabled v-data-table to have moire than one filter.
 */
class MultiFilters {

  /**
   * Constructor.
   *
   * @param items
   * @param filters
   * @param filter
   * @param headers
   */
  constructor(items, filters, filter, headers) {
    this.items = items;
    this.filter = filter;
    this.headers = headers;
    this.filters = filters;
    this.filterCallbacks = {};
  }

  /**
   * Updates filter values.
   * @param filters filter´s object
   * @param val JSON chunk to be updated.
   * @returns {*}
   */
  static updateFilters(filters, val) {
    return Object.assign({}, filters, val);
  }

  /**
   * Adds a new filter
   * @param filterName The name of the filter from which the information will be extracted
   * @param filterCallback The callback that will apply the filter.
   */
  registerFilter(filterName, filterCallback) {
    this.filterCallbacks[filterName] = filterCallback;
  }

  /**
   * Run all filters.
   * @returns {*}
   */
  runFilters() {
    const self = this;
    let filteredItems = self.items;

    Object.entries(this.filterCallbacks)
      .forEach(([entity, cb]) => {
        filteredItems = cb.call(self, self.filters[entity], filteredItems);
      });

    return filteredItems;
  }

}

// Vue plugin.
const MultiFiltersPlugin = {
  install(Vue, options) {
    Vue.prototype.$MultiFilters = MultiFilters;
  }
};

export default MultiFiltersPlugin;

2: En src/main.js importa el plugin y añádelo al bootstrap de la app usando Vue.use()

import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import MultiFiltersPlugin from './plugins/MultiFilters' // <-- THIS
import App from './App.vue'

Vue.config.productionTip = false;

Vue.use(MultiFiltersPlugin); // <-- THIS

new Vue({
  render: h => h(App)
}).$mount('#app');

 

Eso es todo. Hasta la próxima!

Comentarios

Enviado por By Alan (sin verificar) el Wed, 06/03/2019 - 23:52

Amazing! I'll just overwrite some stuff to have a unique input for multiple fields and done. Thanks bro

Enviado por By Mark (sin verificar) el Mon, 29/04/2019 - 10:46

Cool ! This is what I was looking for a week. Thanks so much.

Enviado por By Jakub (sin verificar) el Mon, 03/06/2019 - 17:10

Nice article. Thanks a lot!

Enviado por By teeool (sin verificar) el Thu, 06/06/2019 - 10:52

Hello, can date screening give a demo?

Enviado por By ericksond (sin verificar) el Fri, 16/08/2019 - 17:08

Vuetify 2.0 is now checking the prop type of the search prop. How would we go around that?

[Vue warn]: Invalid prop: type check failed for prop "search". Expected String with value "[object Object]", got Object

Enviado por By Angella (sin verificar) el Thu, 22/08/2019 - 10:01

I have tried using this to enable search through one column instead of the vuetify all columns search but i keep getting an error that the field i am search(in my case name) is undefined.
Could you make an article for searching through only one column?

Enviado por By Rain Pagnamitan (sin verificar) el Thu, 12/09/2019 - 05:56

Its not working on me. It says 'options' is defined but never used in '61 | install(Vue, options) {'.
It is possible to add Expanded row there?

Enviado por By MD (sin verificar) el Wed, 23/10/2019 - 00:27

For Vuetify 1.x multicolumn filters - lean and slicky CodePen Example

Enviado por By Mauricio Montoya (sin verificar) el Fri, 01/11/2019 - 21:29

Hola,

No me funciona, no me devuelve el valor buscado en la tabla ( a pesar que está ahí)

Alguna idea?

Saludos

Enviado por By Rumus Bajarcharya (sin verificar) el Thu, 28/05/2020 - 09:49

This works really well thanks a lot

Enviado por By Simon (sin verificar) el Tue, 16/06/2020 - 22:16

Is MultiFilter work on filter by word and then filter on range, for example smaller than min and bigger than max?

Add new comment

The content of this field is kept private and will not be shown publicly.

HTML Restringido

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.