Vuetify 2.2 DataTable multiple filters

Vitalii Feduniak picture Vitalii Feduniak · Jun 30, 2020 · Viewed 7.4k times · Source

I'm new in vue.js, please help. I need filters in my table. Each column must have multiple select filter by values. When we click on column header - dropdown multi select opens, and we can select filtered values. For example like this Vuetify Data Table Inline Filter but this example doesn't work with vuetify 2. enter image description here

My html:

<template>
 <v-card class="elevation-3">
         <v-card-title>
           {{ other_title }}
           <v-btn style="background-color: white; box-shadow: none;" @click="csvExport(other_title, otherIncidentsData)">
               CSV<i class="fas fa-file-csv"></i>
           </v-btn>
           <v-btn text @click="exportToPdf()">
              Pdf<v-icon>mdi-file-pdf-box-outline</v-icon>
            </v-btn>
            <v-btn text @click="showExportModal">
              <v-icon>mdi-email-receive-outline</v-icon>
            </v-btn>
           <v-spacer></v-spacer>
           <v-text-field
             v-model="search"
             append-icon="mdi-magnify"
             label="Search"
             single-line
             hide-details
           ></v-text-field>
         </v-card-title>
         <v-data-table
           :search="search"
           :headers="headers"
           :items="otherIncidents"
           :items-per-page="10"
         >
          <template v-slot:item.priority.name="{ item }">
              <v-chip :color="getPriorityColor(item.priority.name)" dark>{{ item.priority.name }}</v-chip>
          </template>
         </v-data-table>
        </v-card>
</template>

And JS:

 export default {
      mixins: [
        mixin
      ],
      data() {
        return {
          search: '',
          title: 'MediaMyne reports',
          project_title: 'PROJECTS (open at the end of the reporting period)',
          new_title: 'NEW REQUESTS (created during the reporting period)',
          other_title: 'OTHER REQUESTS (remaining open or changed during the reporting period)',
          tabs: [
            { name: 'Projects' },
            { name: 'New Requests' },
            { name: 'Other Requests' },
          ],
          headers: [
            {
              text: 'Company', align: 'start', sortable: true, value: 'customer.name', width: '14%',
            },
            {
              text: 'Name (Costumer Contact)', align: 'start', sortable: true, value: 'reported_By_Customer_Contact.name', width: '16%',
            },
            {
              text: 'Title', align: 'start', sortable: true, value: 'name', width: '17%',
            },
            {
              text: 'Days open', align: 'center', sortable: true, value: 'daysOpen', width: '9%',
            },
            {
              text: 'Days waiting', align: 'center', sortable: true, value: 'daysWaiting', width: '10%',
            },
            {
              text: 'Workflow step', align: 'start', sortable: true, value: 'workflow_Step.name', width: '12%',
            },
            {
              text: 'Support type', align: 'start', sortable: true, value: 'custom_Fields.customFields.custom_266', width: '12%',
            },
            {
              text: 'Priority', align: 'start', sortable: true, value: 'priority.name', width: '10%',
            },
          ],
          otherIncidents: [],
          newIncidents: [],
          projectIncidents: [],
          activeTab: 0,
          pdfReportTitle: ''
        };
      },
      components: {
        ExportModal
      },
      computed: {
      ...mapGetters([
        'isLoggedIn'
      ]),
      otherIncidentsData() {
          return this.otherIncidents.map(item => ({
            Company: item.customer.name,
            Costumer_contact_name: item.reported_By_Customer_Contact.name,
            Title: item.name,
            Days_open: item.daysOpen,
            Days_waiting: item.daysWaiting,
            Workflow_step: item.workflow_Step.name,
            Support_type: item.custom_Fields.customFields.custom_266,
            Priority: item.priority.name
          }));
        },
      methods: {
        ...mapActions([
          'setLoginState'
        ]),
        getPriorityColor(priority) {
          switch (priority.toLowerCase()) {
            case 'critical':
              return '#fc0000';
            case 'high':
              return '#c20202';
            case 'normal':
              return '#dd7417';
            case 'low':
              return '#318d14';
            default:
              return 'rgb(0,0,0,0)';
          }
         }
    };

Answer

manuel-84 picture manuel-84 · Jul 16, 2020

I'm making this on codepen these days, since I haven't found any nice example:

https://codepen.io/manuel-84/pen/NWxLLmN (work in progress)

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data: () => ({
    dialog: false,
    filters: { 'name': [], 'calories': [], 'status': [] },
    activeFilters: {},
    desserts: [],
    editedIndex: -1,
    editedItem: {
      name: '',
      calories: 0,
      fat: 0,
      carbs: 0,
      protein: 0,
    },
    defaultItem: {
      name: '',
      calories: 0,
      fat: 0,
      carbs: 0,
      protein: 0,
    },
  }),

  computed: {
    headers () {
      return [
        {
          text: 'Dessert (100g serving)',
          align: 'start',
          sortable: true,
          value: 'name',
          filter: value => {
            return this.activeFilters.name.includes(value);
          }
        },
        { text: 'Calories', value: 'calories',
          filter: value => {
            return this.activeFilters.calories.includes(value);
          }
        },
        { text: 'Status', value: 'status',
          filter: value => {
            return this.activeFilters.status.includes(value);
          }
        },
        { text: 'Fat (g)', value: 'fat' },
        { text: 'Carbs (g)', value: 'carbs' },
        { text: 'Protein (g)', value: 'protein' },
        { text: 'Actions', value: 'actions', sortable: false },
      ]
    },
    formTitle () {
      return this.editedIndex === -1 ? 'New Item' : 'Edit Item'
    },
  },

  watch: {
    dialog (val) {
      val || this.close()
    },
    /*filters: {
      deep: true,
      handler(val) {
        console.log(val)
      }
    }*/
  },

  created () {
    this.initialize()
  },

  methods: {
    initialize () {
      this.desserts = [
        {
          name: 'Frozen Yogurt',
          calories: 159,
          fat: 6.0,
          carbs: 24,
          protein: 4.0,
          status: 'DIET'
        },
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          status: 'NO DIET'
        },
        {
          name: 'Eclair',
          calories: 262,
          fat: 16.0,
          carbs: 23,
          protein: 6.0,
          status: 'DIET'
        },
        {
          name: 'Cupcake',
          calories: 305,
          fat: 3.7,
          carbs: 67,
          protein: 4.3,
          status: 'NO DIET'
        },
        {
          name: 'Gingerbread',
          calories: 356,
          fat: 16.0,
          carbs: 49,
          protein: 3.9,
          status: 'DIET'
        },
        {
          name: 'Jelly bean',
          calories: 375,
          fat: 0.0,
          carbs: 94,
          protein: 0.0,
          status: 'NO DIET'
        },
        {
          name: 'Lollipop',
          calories: 392,
          fat: 0.2,
          carbs: 98,
          protein: 0,
          status: 'NO DIET'
        },
        {
          name: 'Honeycomb',
          calories: 408,
          fat: 3.2,
          carbs: 87,
          protein: 6.5,
          status: 'NO DIET'
        },
        {
          name: 'Donut',
          calories: 452,
          fat: 25.0,
          carbs: 51,
          protein: 4.9,
          status: 'DIET'
        },
        {
          name: 'KitKat',
          calories: 518,
          fat: 26.0,
          carbs: 65,
          protein: 7,
          status: 'NO DIET'
        },
      ];
      for (col in this.filters) {
        this.filters[col] = this.desserts.map((d) => { return d[col] }).filter(
          (value, index, self) => { return self.indexOf(value) === index }
        );
      }
      this.activeFilters = Object.assign({}, this.filters)
    },
    
    toggleAll (col) {
      this.activeFilters[col] = this.desserts.map((d) => { return d[col] }).filter(
        (value, index, self) => { return self.indexOf(value) === index }
      )
    },
    
    clearAll (col) {
      this.activeFilters[col] = []
    },

    editItem (item) {
      this.editedIndex = this.desserts.indexOf(item)
      this.editedItem = Object.assign({}, item)
      this.dialog = true
    },

    deleteItem (item) {
      const index = this.desserts.indexOf(item)
      confirm('Are you sure you want to delete this item?') && this.desserts.splice(index, 1)
    },

    close () {
      this.dialog = false
      this.$nextTick(() => {
        this.editedItem = Object.assign({}, this.defaultItem)
        this.editedIndex = -1
      })
    },

    save () {
      if (this.editedIndex > -1) {
        Object.assign(this.desserts[this.editedIndex], this.editedItem)
      } else {
        this.desserts.push(this.editedItem)
      }
      this.close()
    },
    
  },
  
})
.v-list--dense .v-list-item, .v-list-item--dense {
  min-height: 20px !important;
  height: 2rem;
}

.v-application--is-ltr .v-list-item__action:first-child, .v-application--is-ltr .v-list-item__icon:first-child {
  margin-right: .5rem !important;
}
<script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.js"></script>

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet"/>

<span id="app">
  <v-app id="inspire">
    <v-data-table :headers="headers" :items="desserts" sort-by="calories" item-key="name" show-select>
      <template v-for="(col, i) in filters" v-slot:[`header.${i}`]="{ header }">
        <div style="display: inline-block; padding: 16px 0;">{{ header.text }}</div>
        <div style="float: right; margin-top: 8px">
          <v-menu :close-on-content-click="false" :nudge-width="200" offset-y transition="slide-y-transition" left fixed style="position: absolute; right: 0">
            <template v-slot:activator="{ on, attrs }">
              <v-btn color="indigo" icon v-bind="attrs" v-on="on">
                <v-icon small 
                  :color="activeFilters[header.value].length < filters[header.value].length ? 'red' : 'default'">
                  mdi-filter-variant
                </v-icon>
              </v-btn>
            </template>
            <v-list flat dense class="pa-0">
              <v-list-item-group multiple v-model="activeFilters[header.value]" class="py-2">
                <template v-for="(item, i) in filters[header.value]">
                  <v-list-item :key="`item-${i}`" :value="item" :ripple="false">
                    <template v-slot:default="{ active, toggle }">
                      <v-list-item-action>
                        <v-checkbox :input-value="active" :true-value="item"
                          @click="toggle" color="primary" dense></v-checkbox>
                      </v-list-item-action>
                      <v-list-item-content>
                        <v-list-item-title v-text="item"></v-list-item-title>
                      </v-list-item-content>
                    </template>
                  </v-list-item>
                </template>
              </v-list-item-group>
              <v-divider></v-divider>
              <v-btn text block @click="toggleAll(header.value)">Toggle all</v-btn>
              <v-btn text block @click="clearAll(header.value)">Clear all</v-btn>
            </v-list>
          </v-menu>
        </div>
      </template>
      <template v-slot:header="{ props: { headers } }">
        <thead>
          <tr>
            <th :colspan="headers.length">
              This is a header
            </th>
          </tr>
        </thead>
      </template>
      <template v-slot:top>
        <v-toolbar flat color="white">
          <v-toolbar-title>My CRUD</v-toolbar-title>
          <v-divider class="mx-4" inset vertical></v-divider>
          <v-spacer></v-spacer>
          <v-dialog v-model="dialog" max-width="500px">
            <template v-slot:activator="{ on, attrs }">
              <v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">New Item</v-btn>
            </template>
            <v-card>
              <v-card-title>
                <span class="headline">{{ formTitle }}</span>
              </v-card-title>
              <v-card-text>
                <v-container>
                  <v-row>
                    <v-col cols="12" sm="6" md="4">
                      <v-text-field v-model="editedItem.name" label="Dessert name"></v-text-field>
                    </v-col>
                    <v-col cols="12" sm="6" md="4">
                      <v-text-field v-model="editedItem.calories" label="Calories"></v-text-field>
                    </v-col>
                    <v-col cols="12" sm="6" md="4">
                      <v-text-field v-model="editedItem.fat" label="Fat (g)"></v-text-field>
                    </v-col>
                    <v-col cols="12" sm="6" md="4">
                      <v-text-field v-model="editedItem.carbs" label="Carbs (g)"></v-text-field>
                    </v-col>
                    <v-col cols="12" sm="6" md="4">
                      <v-text-field v-model="editedItem.protein" label="Protein (g)"></v-text-field>
                    </v-col>
                  </v-row>
                </v-container>
              </v-card-text>
              <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="blue darken-1" text @click="close">Cancel</v-btn>
                <v-btn color="blue darken-1" text @click="save">Save</v-btn>
              </v-card-actions>
            </v-card>
          </v-dialog>
        </v-toolbar>
      </template>
      <template v-slot:item.actions="{ item }">
        <v-icon small class="mr-2" @click="editItem(item)">
          mdi-pencil
        </v-icon>
        <v-icon small @click="deleteItem(item)">
          mdi-delete
        </v-icon>
      </template>
      <template v-slot:no-data>
        <v-btn color="primary" @click="initialize">Reset</v-btn>
      </template>
    </v-data-table>
  </v-app>
  </div>

I hope the code is clear, anyway feel free to ask