How to create a search filter with faceting in VueJs

I’m a big fan of VueJs, It’s a great framework.  In this quick example I am going to write a Quick search that will filter out results as you type into the search box and it will also have a facet filtering similar to what you see on eCommerce sites.

How to create a search filter and facet in VueJs

Below is a link of what we aim to have built by the end of this tutorial. Preview or “Download.

Step 1

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>VueJs search filter and facet</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
    <div id="app">
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.22/vue.js"></script>
</body>
</html>

We going to start with a blank HTML and going to pull in Tailwind Css and Vuejs from a CDN.

Step 2

<script>
    let app = new Vue({
        el: '#app',
        data() {
        return{
            endpoint:      "https://jsonplaceholder.typicode.com/posts",
            posts: [],
            searchQuery: null,
            facet: [],
            }
        },
    });
</script>

We now initializing the Vue instance and setting the values of the data object. What the endpoint data object does is sets the URL of the Api in this example. I’m using Jsonplaceholder. The post array is what I be setting the output of the array. The searchQuery is the search input and finally the facet is the sidebar filter of out posts.

Step 3

let app = new Vue({
    el: '#app',
    data() {
        return{
            endpoint:      "https://jsonplaceholder.typicode.com/posts",
            posts: [],
            searchQuery: null,
            facet: [],
        }
    },
    methods: {
        fetchposts() {
            fetch(this.endpoint)
                .then(blob => blob.json())
                .then(data => this.posts.push(...data));
        }
    },
    mounted(){
        this.fetchposts();
    }            
});

Using the endpoint we create a method called fetchpost which fetches the posts from JsonPlaceholder and push it to the post array. Then when the page is mounted we run the method fetchpost which was just created.

Step 4

<div id="app">
   {{posts}}
</div>

We just going to check that the fetch command got the data. If you got to the browser you should see some sample posts on the page.

Step 5

<body class="bg-grey-lighter">
    <div class="container mx-auto" id="app">
        <div class="flex w-3/4 mx-auto my-6">
            <input type="search" v-model="searchQuery" name="" id="" class="rounded p-2 w-full bg-grey-light border-grey-light focus:bg-white border border-solid focus:border-indigo-light text-blue-darkest" placeholder="Enter a search here!" />
        </div>
    </div>

Now we adding the search box to the page for the input we are assigning searchQuery field we created early at the same time we going start doing some basic styling with Tailwind Css.

Step 6

methods: {
    fetchposts() {
        fetch(this.endpoint)
            .then(blob => blob.json())
            .then(data => this.posts.push(...data));
    }
},
computed: {
    filterPosts(){
        return this.posts.filter(result => {
            const myRegex = new RegExp(this.searchQuery, 'gi');
            return (result.title.match(myRegex) || result.body.match(myRegex))
        })
    }
},

In this step we have created a computed property called filterPosts. What this does based on what the user types into the search box. It filters out the results in the title or the body of the post.

Step 7

<div class="flex w-3/4 mx-auto my-6">
    <input type="search" v-model="searchQuery" name="" id="" class="rounded p-2 w-full bg-grey-light border-grey-light focus:bg-white border border-solid focus:border-indigo-light text-blue-darkest" placeholder="Enter a search here!" />
</div>
<div class="flex w-3/4 mx-auto text-indigo-darkest">
    <div class="w-1/4 mr-4">
        <div class="bg-white p-4 border border-solid border-grey-light ">
            <p class="text-lg border-b pb-2 mb-2 ">Filter by:</p>
        </div>
    </div>
    <div class="w-3/4">
        <p class="mb-2 text-right" v-if="searchQuery && filterPosts.length > 1 ">{{filterPosts.length}} results</p>
        <ul class="list-reset bg-white p-4 border border-solid border-grey-light ">
            <li v-if="!searchQuery">
                <h2 class=" text-indigo-darker pb-2 text-xl">Search</h2>
                <p class="text-black">Please use the search above</p>
            </li>
            <li v-for="post in filterPosts" v-else :key="post.id" class="mb-6">
                <h2 class=" text-indigo-darker pb-2 text-xl">{{post.title}}</h2>
                <p class="pb-2 text-black">{{post.body}}</p>
                <p class="text-sm text-grey-darker">Posted by: User {{post.userId}}</p>
            </li>
            <li v-if="filterPosts.length == 0" >
                <h2 class=" text-indigo-darker pb-2 text-xl">Sorry</h2>
                <p class="text-black">There seem to be no posts under your criteria</p>
            </li>
        </ul>
    </div>
</div>

In this step we have added in a dummy sidebar which we will be using later on. <p class=”mb-2 text-right” v-if=”searchQuery && filterPosts.length > 1 “>{{filterPosts.length}} results</p>. This shows how many posts that are in search results and only shows if the filter post is greater than 1 and searchQuery has a value.

<li v-if="!searchQuery">
    <h2 class=" text-indigo-darker pb-2 text-xl">Search</h2>
    <p class="text-black">Please use the search above</p>
</li>

This only shows if the search input has no value.

<li v-for="post in filterPosts" v-else :key="post.id" class="mb-6">
    <h2 class=" text-indigo-darker pb-2 text-xl">{{post.title}}</h2>
    <p class="pb-2 text-black">{{post.body}}</p>
    <p class="text-sm text-grey-darker">Posted by: User {{post.userId}}</p>
</li>

This loops through all the post which filterPosts has returned and shows the title, body and the user.

<li v-if="filterPosts.length == 0" >
    <h2 class=" text-indigo-darker pb-2 text-xl">Sorry</h2>
    <p class="text-black">There seem to be no posts under your criteria</p>
</li>

Finally if the search query matches no results we show a no result message.

Step 8

filterPosts(){
    return this.posts.filter(result => {
        const myRegex = new RegExp(this.searchQuery, 'gi');
        let resultFacet = this.facet;
        if (resultFacet.length == 0) {
            return (result.title.match(myRegex) || result.body.match(myRegex))
        }
        return (result.title.match(myRegex) || result.body.match(myRegex)) && (resultFacet.includes(result.userId)); 
    })
}

Back to the computed property, first we assign a variable resultFacet which is the facet array. If this variable has a length of 0 returns the search we have been using in the previous steps. If not filter out the search results with the normal filters with the addition of filtering out the the userID.

Step 9

<div class="w-1/4 mr-4">
    <div class="bg-white p-4 border border-solid border-grey-light ">
        <p class="text-lg border-b pb-2 mb-2 ">Filter by:</p>
        <dl class="list-reset">
            <dt class="font-bold text-purple-dark  pb-2 uppercase">Users</dt>
            <dd class="mb-2">
                <input id="user1" type="checkbox" name="facet" v-model="facet" :value="1"> 
                <label for="user1"> User 1</label>
            </dd>
            <dd class="mb-2">
                <input id="user2" type="checkbox" name="facet" v-model="facet" :value="2" /> 
                <label for="user2"> User 2</label>
            </dd>
            <dd class="mb-2">
                <input id="user3" type="checkbox" name="facet" v-model="facet" :value="3"> 
                <label for="use3"> User 3</label>
            </dd>
            <dd class="mb-2">
                <input id="user4" type="checkbox" name="facet" v-model="facet" :value="4"> 
                <label for="user4"> User 4</label>
            </dd>
            <dd class="mb-2">
                <input id="user5" type="checkbox" name="facet" v-model="facet" :value="5"  />     
                <label for="user5">User 5</label>
            </dd>
            <dd class="mb-2">
                <input id="user6" type="checkbox" name="facet" v-model="facet" :value="6"> 
                <label for="user6"> User 6</label>
            </dd>
            <dd class="mb-2">
                <input id="user7" type="checkbox" name="facet" v-model="facet" :value="7"> 
                <label for="user7"> User 7</label>
            </dd>
            <dd class="mb-2">
                <input id="user8" type="checkbox" name="facet" v-model="facet" :value="8"> 
                <label for="use8"> User 8</label>
            </dd>
            <dd class="mb-2">
                <input id="user9" type="checkbox" name="facet" v-model="facet" :value="9"> 
                <label for="user9"> User 9</label>
            </dd>
            <dd class="mb-2">
                <input id="user10" type="checkbox" name="facet" v-model="facet" :value="10"  />     
                <label for="user10">User 10</label>
            </dd>
        </dl>
    </div>
</div>

The final step we add in the faceting, we be using the checkboxes for this with the facet model we created earlier.

Here is a link to the demo and link to the Vuejs code to download.

Leave a Reply

Your email address will not be published. Required fields are marked *