Instant AJAX Search with Laravel and Vue

Instant searching is quite a popular feature on sites and apps. In this post, we try to cover the key points of building a real-time search component with features like debounce or highlighting results.

Prologue: Official Package for the Real Work

If you need a neat and well-supported way to search your Eloquent models, Laravel provides an official package for that. Laravel Scout is a driver based, full-text search to the Eloquent models. Currently, it supports only Algolia as a driver, but you can easily bring your own search driver as well.

Getting Started

The progress as the result should be the following: We type something to an input field and we forward the given keywords to the server side with an AJAX request. Then we catch the keywords at the back-end and retrieve the models that match the given query.

Since, what we want here, it no more than a demonstration, both the back-end and the front-end should be quite simple. What we really want to highlight here are the key parts and features what makes it a bit more usable.

Building the Back-End for the Feature

Many times we want to implement more complex things than to filter results by only one parameter. If we don’t want to use Scout, we can write our own query filters and automatize the filtering. We won’t cover query filters in this post, we just really want to mention this way to work multiple query parameters that we want to apply and use. If you want to know more about the topic, Laracasts has a very nice episode about it, it worths to watch it!

For now, let’s move on and create the controller.

<?php // SearchController.php

public function search(Request $request)
{
    $posts = Post::where('name', $request->keywords)->get();

    return response()->json($posts);
}

As you can see, it’s really simple now, but we should notice two things:

The first, we return with a JSON response, because we want to retrieve it from our front-end. That also means we should use API routes here and not the normal web routes, but it’s secondary now.

The second thing, since we use the $request->keywords the query string should look something like this ?keywords=Some+search+query.

As a result, we get an Eloquent collection of the matched models, what we convert to JSON and make processable from the front-end.

Performing the Search with Vue

To simplify the things, we will have only an input and a list of the results. As a first step, we create the Vue instance and bind a model to the input. Then we trigger some action when the value of the input changes. Let’s see how should it look like:

<template>
    <div>
        <input type="text" v-model="keywords">
        <ul v-if="results.length > 0">
            <li v-for="result in results" :key="result.id" v-text="result.name"></li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            keywords: null,
            results: []
        };
    },

    watch: {
        keywords(after, before) {
            this.fetch();
        }
    },

    methods: {
        fetch() {
            axios.get('/api/search', { params: { keywords: this.keywords } })
                .then(response => this.results = reponse.data)
                .catch(error => {});
        }
    }
}
</script>

So what is happening here? First of all, we have the template part, where we bind the Vue model and iterate through the results. In the script part, we set up the data that we want to use, also we define a fetch method, what is a wrapper around an axios request.

When the value of the keywords data property changes, we trigger the fetch method again, with the new keywords and we list the new results.

In the next steps, we try to go through some things that make our component a bit neater.

Debounce for the v-model

What is the problem with the current code? This way we fetch the data instantly after the user typed a letter. Most of the times users are typing words or fragments, so it would be a nice thing to fire the fetch method after they stopped typing.

The first way to go is the lazy modifier. With this v-model modifier, we change the sync event from input to change. That means the model is getting updated to the new value if the input has a blur event or we press an enter. You can read the docs here. If you don’t need more than this, it’s an easy to implement and use solution.

The other way is to implement a debouncer to our v-model. Actually, in Vue v1 we had a debounce, but it has been removed in version 2. Like in the migration guide, we could use lodash’s (_) debounce library, but in my opinion, if we can bring our own debounce solution, this is the case where we should do it.

For a long time, I could not find any nice and clean solutions, but recently I found a brilliant one. This post and this repo tell the story behind it and provide the code what you need. You can easily pull the code into yours without any dependency.

To make it work, you have to add the .lazy modifier to the model! Don’t forget that!

So, let’s say we have integrated the debounce. Now we can delay any changes on our Vue model if we want. Imagine that, if there is no change in the given interval, we commit the last state to the v-model. The watch method is getting triggered and we can fetch the new data. But we do it only once, instead of 4-5 times.

If we want to add 300ms delay to our v-model, we can do it like this:

<input type="text" v-model.lazy="keywords" v-debounce="300">

That’s it! At the end, we will show an example of the integrated debounce solution.

Highlight the results

From UX aspect this part is significant. If we could highlight the matches with the given keyword it would be a good way to help the user to find faster what she/he wants.

We have posted a snippet to lookup and wrap matches to a given keyword in a given context. You can find the snippet here. We can put it into the methods part and use it easily.

highlight(text) {
    return text.replace(new RegExp(this.keywords, 'gi'), '<span class="highlighted">$&</span>');
}
As you can see, we will return with HTML and not a simple string. That means we need to change v-text to v-html.

Summary

We can see, it’s not a big deal to compose a simple text-based AJAX search solution. The hard thing is -as always- to find and integrate little but powerful features like debounce or highlight. As we think, to pay attention to the little things like these make your app better and more user-friendly than you think.

As we promised you can find the fully working solution (without back-end) here: https://jsfiddle.net/hej7L1jy/2/.

It’s a bit modified because we need a little bit different logic if we work with static data. We hope you can use any of these things!