Build an advanced search and filter with Vuex (in Nuxt)
Tags: vue, tutorial, tailwindcss
Published: 2019-12-06 08:54:03

## What are we building?
A filter! We want to be able to search through our leads, filter them by status, and change the order. But, we also want all of these filters to work together and chain.

![2019-12-06-13.48.49.gif](https://s5.gifyu.com/images/2019-12-06-13.48.49.gif)

## Get Started
So to keep this as short as possible I'll refrain from going through the process of setting up a new [Nuxt](https://nuxtjs.org/) project. This should also work fine with plain old [Vuex](https://vuex.vuejs.org/).

Here's some assumptions:
- You already have a project setup
- You have some type of data you want to filter
- You know the basics of Vuex and store management

### Project structure
For my example I'm using a project I've been working on (did I mention it's open source? 👉 [https://github.com/messerli90/jobhuntbuddy](https://github.com/messerli90/jobhuntbuddy)).

We have a bunch of job openings (we're calling *leads*) we'd like to track, but the list is getting long and we want to be able to:
1. Search by **company name** and **job title**
2. Show only leads in a certain **status**
3. Order them by: created date, company name, job title, or status
4. Not make an API call every time the filter changes, all changes to the list should remain local

Let's get started

### Set up the Vuex store
We have a store setup that has our list of leads and current lead in state. We want to add a new list of `filteredLeads` and an initial `filter` object to our state.

```javascript
// ~/store/leads.js
export const state = () => ({
  leads: [],
  filteredLeads: [],
  lead: {},
  filter: {
    search: '',
    status: 'all',
    order: 'createdAt'
  }
})
```
We want to keep the initial list of leads we get back from the API to remain untouched, so when we clear our filters we can just grab all of our leads again.

#### Actions
Let's define the actions our Vue component will be able to call when we make changes to our filter.

*I've prefixed all these methods with 'filter' so know it all belongs together.*

For `filterStatus`, `filterSearch`, and `filterOrder` we first commit a mutation to store them in the filter object we just created. This way, when we can maintain a single source of truth when calling the `filterLeads` method.

Since we want to make all of our filters be maintained no matter which value we change the final `filterLeads` action will first narrow down our list to what we want and then order our new list.

```javascript
// ~/store/leads.js
export const actions = {
// ...
  async filterOrder ({ commit }, order) {
    await commit('setOrder', order)
    await commit('orderLeads')
  },
  async filterStatus ({ commit, dispatch }, status) {
    await commit('setFilterStatus', status)
    dispatch('filterLeads')
  },
  async filterSearch ({ commit, dispatch }, search) {
    await commit('setFilterSearch', search)
    dispatch('filterLeads')
  },
  async filterLeads ({ commit }) {
    await commit('filterLeads')
    await commit('orderLeads')
  },
  // ...
}
```

#### Mutations
Now let's look at the mutations we just commited.

`setFilteredLeads` gets called after applying a new filter so our Vue component shows only the leads we want to see, without losing our initial list.

`setFilterStatus`, `setFilterSearch`, and `setOrder` are only responsible for changing the respective value on the `filter` object.

`filterLeads` first makes a local copy of all leads. We reset our *filteredLeads* list to include all leads. Finally, we call our filter method and store this new list on the state.

Similarly, `orderLeads` grabs this new list of *filteredLeads*, passes it on to our ordering method, and saves our new list.

```javascript
// ~/store/leads.js
import * as Filters from '~/helpers/filters'

export const mutations = {
  // ...
  setFilteredLeads (state, leads) { state.filteredLeads = leads },

  setFilterStatus (state, status) { state.filter.status = status },
  setFilterSearch (state, search) { state.filter.search = search },
  setOrder (state, order) { state.filter.order = order },

  filterLeads (state) {
    const leads = [...state.leads]
    state.filteredLeads = leads
    state.filteredLeads = Filters.filterLeads(state.filter, leads)
  },
  orderLeads (state) {
    const leads = [...state.filteredLeads]
    state.filteredLeads = Filters.orderLeads(state.filter.order, leads)
  }
  // ...
}
```

And that's all we have to change in our Vuex store. Let's move on to our filtering helper methods

### Filter Helpers
This is where the magic happens. We saw in the last step our mutations called `Filter.filterLeads(state.filter, leads)` and `Filter.orderLeads(state.filter.order, leads)` so let's create these and do some sorting!

*Disclaimer: This works, but I am in no way a javascript rockstar and if you have any tips on how to optimize this I am excited to hear from you!*

**Recap**
Remember what our `filter` object looks like:
```javascript
filter: {
  search: '',
  status: 'all',
  order: 'createdAt'
}
```

#### filterLeads(filter, leads)
```javascript
// ~/helpers/filters.js
export function filterLeads (filter, leads) {
  let filteredList = [...leads]

  // Filter status
  if (filter.status !== 'all') {
    const filtered = filteredList.filter(lead => lead.status === filter.status)
    filteredList = filtered
  }

  // Search
  if (filter.search !== '') {
    const searchList = []
    const searchTerm = filter.search.toLowerCase()
    for (let i = 0; i < filteredList.length; i++) {
      if (
        (filteredList[i].companyName !== null && filteredList[i].companyName.toLowerCase().includes(searchTerm)) ||
        (filteredList[i].jobTitle !== null && filteredList[i].jobTitle.toLowerCase().includes(searchTerm))
      ) {
        searchList.push(filteredList[i])
      }
    }
    filteredList = searchList
  }

  return filteredList
}
```
*Read more about `includes()` on MDN:* [String.prototype.includes()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)

Since the search loops through all of our leads to make a text match, we'll do that last to save it from running unnecessary iterations. Let's first first filter through our list to find any leads that match our status filter. 

Now that we have this shorter list we can pass that on to the search logic. If the search field is empty we should skip this whole step. (Remember that we reset our *filteredLeads* list back to our initial *leads* list before calling this). Otherwise, make sure to use `.toLowerCase()` on both the search term and the attribute you want to filter because javascript treats 'A' & 'a' differently and the won't match otherwise. Any matches get pushed to our new `searchList` and then replace our `filteredList`.

#### orderLeads(order, leads)
```javascript
// ~/helpers/filters.js
import moment from 'moment'
export function orderLeads (order, leads) {
  const orderedList = [...leads]

  if (order === 'createdAt') {
    orderedList.sort(function (a, b) {
      const unixA = moment(a.createdAt).unix()
      const unixB = moment(b.createdAt).unix()
      return unixA < unixB ? -1 : 1
    })
  } else {
    orderedList.sort(function (a, b) {
      const nameA = a[order] ? a[order].toLowerCase() : 'zzz'
      const nameB = b[order] ? b[order].toLowerCase() : 'zzz'
      return nameA < nameB ? -1 : 1
    })
  }

  return orderedList
}
```
*Read more about `sort()` on MDN:* [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)

This is our order method. Since currently we're only ordering by *company name*, *job title*, *status*, and *created at* we only need two types of ordering functions: Date and String.

So, if the order is 'createdAt', and we know that `lead.createdAt` is a *timestamp* we transform it to a unix timestamp so it's easier to compare. I use [Moment.js](https://momentjs.com/) here which may be overkill.

Otherwise, our other ordering methods are all strings so we can treat them the same (**assuming our order and object key are equal!**). I've also decided that if a lead doesn't have a certain value (i.e. jobTitle) we'll default this to 'zzz' so it gets pushed to the end of the list.

Then we return our orderList (which has already been filtered)

## Presentation Layer
Now that all the ground work has been done in our Vuex store, let's move on to the Vue component that puts this all together.

### Lead Filter
Our filter component
```javascript
// ~/components/leads/leadFilter.vue
<template>
  <div>
    <div class="w-full mb-2">
      <input
        :value="search"
        type="search"
        class="h-12 p-4 mb-1 w-full bg-white border-2 border-gray-300 rounded-full"
        placeholder="Search company name or job title"
        aria-label="Search by company name or job title"
        @input="handleSearch"
      >
    </div>
    <div class="mb-4 w-full">
      <div class="flex flex-wrap items-center justify-center md:justify-between w-full text-gray-800">
        <button
          class="bg-gray-400 rounded-full px-3 py-2 font-medium text-center text-sm m-1 hover:bg-gray-500"
          :class="{ 'bg-indigo-700 text-white hover:bg-indigo-800' : status === 'all' }"
          @click="handleStatusFilter('all')"
        >
          All Leads
        </button>
        <button
          class="bg-gray-400 rounded-full px-3 py-2 font-medium text-center text-sm m-1 hover:bg-gray-500"
          :class="{ 'bg-yellow-500 text-white hover:bg-yellow-600' : status === 'prospect' }"
          @click="handleStatusFilter('prospect')"
        >
          Prospects
        </button>
        <button
          class="bg-gray-400 rounded-full px-3 py-2 font-medium text-center text-sm m-1 hover:bg-gray-500"
          :class="{ 'bg-green-500 text-white hover:bg-green-600' : status === 'application-sent' }"
          @click="handleStatusFilter('application-sent')"
        >
          Application Sent
        </button>
        <button
          class="bg-gray-400 rounded-full px-3 py-2 font-medium text-center text-sm m-1 hover:bg-gray-500"
          :class="{ 'bg-blue-500 text-white hover:bg-blue-600' : status === 'interview-set' }"
          @click="handleStatusFilter('interview-set')"
        >
          Interview Set
        </button>
        <button
          class="bg-gray-400 rounded-full px-3 py-2 font-medium text-center text-sm m-1 hover:bg-gray-500"
          :class="{ 'bg-red-500 text-white hover:bg-red-600' : status === 'rejected' }"
          @click="handleStatusFilter('rejected')"
        >
          Rejected
        </button>
      </div>
    </div>
    <div class="flex justify-start">
      <div class="relative mb-3 pr-8">
        <p
          v-click-outside="closeOrderDropDown"
          class="text-gray-700 cursor-pointer flex items-center"
          @click="orderOpen = !orderOpen"
        >
          <fa :icon="['fas', 'sort-amount-down']" class="h-4 mx-1" />
          <span class="mr-1">Order By</span>
          <span v-show="orderChanged" class="font-semibold">{{ orderText }}</span>
        </p>
        <ul v-show="orderOpen" class="bg-white absolute z-20 px-3 py-2 mt-1 rounded shadow-lg text-gray-700 min-w-full">
          <li
            class="cursor-pointer pb-1 hover:text-indigo-600"
            :class="{ 'text-indigo-600 font-semibold' : order === 'createdAt' }"
            @click="handleFilterOrder('createdAt')"
          >
            Created Date
          </li>
          <li
            class="cursor-pointer pb-1 hover:text-indigo-600"
            :class="{ 'text-indigo-600 font-semibold' : order === 'companyName' }"
            @click="handleFilterOrder('companyName')"
          >
            Company Name
          </li>
          <li
            class="cursor-pointer hover:text-indigo-600"
            :class="{ 'text-indigo-600 font-semibold' : order === 'jobTitle' }"
            @click="handleFilterOrder('jobTitle')"
          >
            Job Title
          </li>
          <li
            class="cursor-pointer hover:text-indigo-600"
            :class="{ 'text-indigo-600 font-semibold' : order === 'status' }"
            @click="handleFilterOrder('status')"
          >
            Status
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import { debounce } from '~/helpers/index'
export default {
  data () {
    return {
      orderOpen: false,
      orderChanged: false
    }
  },
  computed: {
    search () {
      return this.$store.state.leads.filter.search
    },
    status () {
      return this.$store.state.leads.filter.status
    },
    order () {
      return this.$store.state.leads.filter.order
    },
    orderText () {
      switch (this.order) {
        case 'companyName':
          return 'Company Name'
        case 'jobTitle':
          return 'Job Title'
        case 'status':
          return 'Status'
        default:
          return 'Created Date'
      }
    }
  },
  methods: {
    handleStatusFilter (status) {
      this.$store.dispatch('leads/filterStatus', status)
    },
    handleSearch: debounce(function (e) {
      this.$store.dispatch('leads/filterSearch', e.target.value)
    }, 500),
    handleFilterOrder (orderBy) {
      this.orderOpen = false
      this.orderChanged = true
      this.$store.dispatch('leads/filterOrder', orderBy)
    },
    closeOrderDropDown (e) {
      this.orderOpen = false
    }
  }
}
</script>
```
I can hear you already: "That's a lot of Tailwind CSS...", I know but we're bootstrapping 😉. Let's look at what we care about:

In **computed()** we're grabbing the current state of the three filters we care about: *search*, *status*, and *order*. And making our orders readable since we made them === key on the lead.

Our **methods()** are all very straight forward and only dispatch the actions we created earlier. It's all reactive and gets handled by Vuex!


### Lead List
This is our index page listing all of our leads
```javascript
// ~/pages/leads/index.vue
<template>
  <div id="lead-index-wrapper" class="container pt-4 px-2 w-full md:w-2/3 lg:w-1/2 xl:w-1/3">
    <div>
      <div v-if="leads.length">
        <LeadFilter />
        <nuxt-link v-for="lead in filteredLeads" :key="lead.id" :to="'/leads/' + lead.id">
          <IndexCard :lead="lead" />
        </nuxt-link>
        <NoLeadsCard v-if="!filteredLeads.length" />
      </div>
      <OnboardingCard v-if="!leads.length" />
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import LeadFilter from '~/components/leads/leadFilter'
import IndexCard from '~/components/leads/IndexCard'
import OnboardingCard from '~/components/leads/onboardingCard'
import NoLeadsCard from '~/components/leads/noLeadsCard'
export default {
  middleware: 'authenticated',
  components: { IndexCard, NoLeadsCard, OnboardingCard, LeadFilter },
  computed: {
    ...mapGetters({
      'leads': 'leads/getLeads',
      'filteredLeads': 'leads/getFilteredLeads',
      'lead': 'leads/getLead'
    })
  },
  async fetch ({ store }) {
    await store.dispatch('leads/fetchAllLeads')
  },
  mounted () {
    if (!this.leads.length) {
      this.$store.dispatch('leads/fetchAllLeads')
    }
  }
}
</script>
```
Not everything here is relevant to this guide, but let's take a look at what's happening on the front end.

As you can see, besides checking that leads exist, most of our components only care about the `filteredLeads` which initially are same-same as `leads`.

We import our **LeadFilter** component which is really dumb and only cares about the state in our Vuex store.


## Wrapping up
That's it, we've seen how we can use *actions* to commit *mutations* and dispatch other *actions*. We talked a bit about `sorting()` and using `includes()` in javascript. And mostly, I wanted to demonstrate how to use state to prevent passing multiple arguments to each method and keeping a single source of truth.

I've really enjoyed working with Nuxt and diving deeper into state management using Vuex. I have learned so much over the past couple months and wanted to give back.

#### JobHuntBuddy
[JobHuntBuddy.co](https://jobhuntbuddy.co)
I used a project I'm currently working on as the example. Right now, I'm looking for a new job so this project is killing two bird by helping me manage my job hunt, and giving potential employers an open source code example to look at.


✌️ Happy Coding!

Follow me on Twitter [@michaelmesserli](https://twitter.com/michaelmesserli)