Filtering data in Vuex

Like most challenges in software development, there’s usually more than one way to implement a solution. Filtering data that resides in Vuex is no exception. This is only one potential implementation and depending on your situation, this may or may not be applicable. For me, it worked out just fine. 

The Setup

So, I have this sample application called The Presidents. 

I bet by looking at it you can guess what it does. It displays the US presidents. Fancy!

To keep things simple, I have an Express server running in node locally and when the application loads the App.vue component, a call is made to Vuex to load the data.  Here is the code from the App component:

<script>
import HeaderComponent from "./components/Header";
import { mapGetters } from "vuex";

export default {
  name: "app",
  components: {
    "header-component": HeaderComponent
  },
  created() {
    this.$store.dispatch("getPresidents");
  }
};
</script>

And the getPresidents() action in Vuex:

import axios from 'axios';

const state = {
    presidents: []
};

const actions = {
    getPresidents({commit}) {
        axios.get('/api/presidents').then((response) => {
            commit('UPDATE_PRESIDENTS', response.data);
        });
    }
};

const mutations = {
    UPDATE_PRESIDENTS(state, payload) {
        state.presidents = payload;
    }
};

Once we make the call and load the data, it’s displayed in the PresidentList component. If we think about the UI in terms of components, here’s how it shakes out:

We have the main App component, denoted in red, the header, filter and presidentList components denoted in blue and the PresidentListItem component denoted in Yellow.

There is a getter on the Vuex store that is accessed by the PresidentList Component and a PresidentListItem component is rendered for each president.

So far so good. 

Lets turn our attention to the Filter Component.

Filtering in Vuex

I want to be able to filter the president list based on the name, the party affiliation or both. 

My initial implementation passed two strings to the Vuex store but I really didn’t care for that setup so I decided to create a custom object called filterObj.

filterObj will make it easy to keep track of the filter criteria. My custom object also provided me the ability to easily see if I was dealing with 1 or 2 filter criteria.

Here is the code for the custom filterObj.

const filterObj = {
    _presidentName: undefined,
    _partyAffiliation: undefined,
    _filterCount: 0
}

Object.defineProperty(filterObj, "presidentName", {
    enumerable: true,
    get(){
        return this._presidentName;
    },
    set(newVal) {
        if(newVal === ''){
            this._presidentName = undefined;
            this._filterCount--;
        } else {
            if(this._presidentName === undefined){
                this._filterCount++;
            }
            this._presidentName = newVal.toLowerCase();
        }
    }
});

Object.defineProperty(filterObj, "partyAffiliation", {
    enumerable: true,
    get(){
        return this._partyAffiliation;
    },
    set(newVal) {
        if(newVal === ''){
            this._partyAffiliation = undefined;
            this._filterCount--;
        } else {
            if(this._partyAffiliation === undefined){
                this._filterCount++;
            }
            this._partyAffiliation = newVal.toLowerCase();
        }
    }
});

Object.defineProperty(filterObj, "filterCount", {
    enumerable: true,
    get(){
        return this._filterCount;
    },
    set(newVal) {
        
        this._filterCount = newVal;
    }
});

export default filterObj;

It’s implemented like this for 2 main reasons:

  1. I always want the value being passed in to be in lowercase and its easy to do that within the setter for presidentName.
  2. I want be be able to tell how many filter criteria Im dealing with when I go to actually apply the filter. The reason for this is that if Im dealing with only 1 filter, I’m essentially doing a logical OR whereas if I have 2 criteria, Im performing a logical AND.
  3. Its just nice and easy to pass an object with all the info I need

Whenever the user interacts with the Filter component controls in the UI, it is all kept in sync with the filterObj:

<template>
    <div>
        <div class="box">filter presidents <br>
            <label for="name">name: </label>
            <input type="text" class="input" v-model="filterObj.presidentName" @keyup="filterPresidents">
            
            <label for="name">party: </label>
            <select class="input" @change="filterPresidents" v-model="filterObj.partyAffiliation"> 
                <option  v-for="(party, key) in parties" :value="party" :key="key">{{ party }}</option>
            </select>
        </div>
    </div>
</template>

<script>
    import filterObj from "../utils/filterObj";
    export default {
        name: 'FilterPanel',
        data() {
            return {
                parties: ['','republican', 'democrat', 
                          'federalist', 'whig',       
                          'democratic-republican', 'none'],
                filterObj
            }
        },
        methods: {
            filterPresidents() {
                this.$store.dispatch('filterPresidents', 
                this.filterObj);
            }
        }
    }
</script>

We call the filterPresidents() method on the component whenever the data in either control changes.

If you’ve worked with Vuex, you know that we never modify the state directly. Instead, we dispatch to Actions on the Store which we do from within the filterPresidents() method passing along our filter object:

filterPresidents() {
       this.$store.dispatch('filterPresidents', 
       this.filterObj);
}

Once “inside” the store, these Actions then Commit mutations which mutate the state in some way:

const state = {
    presidents: [],
    filterObj: {}
};

const actions = {
    filterPresidents({commit}, filterObj) {
        commit('UPDATE_FILTER', filterObj);

    }
};

const mutations = {
    UPDATE_FILTER(state, payload) {
        state.filterObj = payload;
    }
};
const getters = {
    presidents: state => state.presidents
}

With all that in place, all that’s left to do it update our getter from the simple one shown above, to one that recognizes that we now have a filterObj on the state and we can use it to do dynamic filtering:

const getters = {    
    presidents: state => {
        switch(state.filterObj.filterCount) {
            //no filters passed
            case 0:
            case undefined:
                return state.presidents;

            //only 1 filter passed
            case 1:
                return state.presidents.filter(president => {
                    for(let key in state.filterObj) {
                        if(president[key]) {
                       if(president[key].toLowerCase().includes(state.filterObj[key]))
                                return true
                        }
                    }
                    return false;
                });

            //2 filters passed
            case 2:
                return state.presidents.filter(president => {
                    for(let key in state.filterObj) {
                        if(president[key]) {
                            if(!president[key].toLowerCase().includes(state.filterObj[key]))
                                return false;
                        }
                    }
                    return true;
                });
        }
    }
};

Now we can easily filter based on the number of “filters” passed to the store.

filtering data in Vuex

There are probably ways to make this more efficient but its a work in progress. By all means, please post a comment if you have recommendations for improvements of modifications.

In the mean time, thanks for visiting and happy Vueing!