Vuetify: Achieve multiple filtering in the Data table component <v-data-table>
29/10/2018 by Capy

Front.id

The Data table element does not support more than a simple text search filtering. This means that it only allows you to add a text field that filters rows whenever a cell partially contains the word you are looking for:

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

But what happens when we want to put more filters as in this example?

vuetify data table with multiple filters

 

What's happening here is the component does not natively support multiple filters. BUT it does allow us to customize the behavior of the only field available and we are going to use that functionality as a trojan horse to expand the filtering possibilities.

1: Theory

We know that the element <v-data-table> has the possibility to use a property ":filter" in which to indicate the "prop" that has to listen. This "prop" has to be a string, and when it changes, the table will internally apply a filtering using that word to show only the rows that meet the criteria.

We also know that the <v-data-table> element allows you to customize the search algorithm mentioned above using the ":custom-filter" attribute. To this attribute you will assign a method that you will have to create in the "methods" section of your component.

Finally we know that to change the value of the prop associated to :filter we have to add a component <v-text-field> and associate it to the prop through v-model="miprop".

2: The problem

As you have already inferred, the problem appears when you want to add more than one filter to the table because the table only allows as entry point to internal filtering the attribute :filter and can not be an array or object, so you can not go sending information from various fields such as a select, a date picker, etc..

3: The solution

I put a component with the implemented solution and explain it below.

<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>

In this example you can see at the beginning two form elements that will be our filters. One is a text field that will filter rows based on the coincidence of the text in the "name" column and the second is a select that will filter rows based on the "author".

Pay attention to these elements. They don't have the v-model="miprop" reference you'd expect it to be if you wanted to use any of these fields as table filters. Instead we have put two events @change and @input with their respective methods.

In both methods comes into play a VUE plugin called $MultiFilters. This is a plugin that contains a very small class that only has 3 methods: updateFilters(), registerFilters() and runFilters()

NOTE: The installation of the plugin is discussed below. See section "Installing the MultiFilters plugin".

In the case of the methods that respond to events we are using updateFilters() to keep the prop "filters" updated with the last value obtained from each of the filters.

/**
* 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});
},

What happens when we update the prop "filters"? happens that having associated it to the table through the attribute :search="filters" is going to execute the internal search of the table. And as I said before, the prop should be a string if we want the filtering to work... so the next step is to intervene in the filtering using our own method defined in the <v-data-table> component through the attr :custom-filter="customFilter".

The first thing we did in the method we created to apply filtering is to instantiate $MultiFilters to have the two methods that are going to help us close the filtering cycle and then we use them. 

This class, in addition to the methods, will take care of extracting from the prop "filters" the value that corresponds to each filter that we are going to use. Better a little code to exemplify it:

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);

});

In each one of the cf.registerFilter() we are indicating on which value of filters we are going to act and the function that applies the filtering. The filtering itself is nothing of the other world. just a little bit of js and returns only the items that have passed the test.

For example in the first method we have indicated that for the value filters.search a function is executed that verifies row by row in the cell "name" if this one has inside its text some part that coincides with the filters.name.

And finally we launch cf.runFilters(); whose purpose is to launch each one of the filters in a sequential way. This detail is important because if the first filter that is applied returns 5 of the 100 elements of a table, the next filter will only evaluate those 5 elements. 

Final component (live demo)

Here I put the example already working. The first table is a more complex example that has a pair of datepickers and a log that shows the values that is taking the filtering. The second table is the one we have explained in this post.

The source code of these examples can be found  HERE and the example can be seen directly from HERE.

 

Install the MultiFilters plugin

The example repository already has it implemented but if you want to take this to your project, in addition to the component explained above you will need to install the Vue MultiFilters plugin.

1: Create 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: On src/main.js import the plugin and add it to the app bootstrap using 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');

 

That's all there is to it. See you next time!

Add new comment

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

Restricted HTML

  • 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.