How To Build Vue Search Component

April 28, 2020 (4y ago)

Last week, I had a task to build a keyword search feature on my job. It's required a constant background checking whenever a new keyword was typed. The problem is I don't want to continually request the API; the new keyword was changed. It will hurt our server. Let's start building a component first.

What we're going to build is a search UI that allows users to find any information about Star Wars by name.

Build an Input component

Let’s start with basic input. I’m going to create a search input that accepts name.

_design

<template>
  <div>
    <div class="w-full  px-3 mb-6 md:mb-0">
      <label
        class="block uppercase tracking-wide text-gray-700 text-xs font-bold     mb-2"
        for="name"
      >
        Star wars name
      </label>
      <div class="relative mb-3">
        <input
          class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
          id="name"
        />
        <div
          class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
        >
          <svg
            class="h-4 w-4"
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="#000000"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <circle cx="11" cy="11" r="8" />
            <line x1="21" y1="21" x2="16.65" y2="16.65" />
          </svg>
        </div>
      </div>
      <button
        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      >
        Search
      </button>
    </div>
  </div>
</template>

Let’s add the v-model to the form data and method for checking API.

<template>
  <div>
    <div class="w-full  px-3 mb-6 md:mb-0">
      <label
        class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
        for="name"
      >
        Star wars name
      </label>
      <div class="relative mb-3">
        <input
          v-model="keyword"
          class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
          id="name"
        />
        <div
          class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
        >
          <svg
            class="h-4 w-4"
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="#000000"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <circle cx="11" cy="11" r="8"></circle>
            <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
          </svg>
        </div>
      </div>
      <button
        @click.prevent="checkName"
        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      >
        Search
      </button>
    </div>
  </div>
</template>
 
<script>
export default {
  data: () => ({
    keyword: "",
  }),
  methods: {
    checkName() {
      console.log(`Checking name: ${this.keyword}`);
    },
  },
};
</script>

Prepare the API

I’ve going to use an external API to demonstrate this feature. We’re going to use SWAPI API to test it.

This endpoint we’re going to use. The search query parameter is for our search keyword.

https://swapi.dev/api/people/?search=luke

The endpoint above will give the list of Star Wars with Luke's name.

This is the result. You may use any REST client to test it.

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "name": "Luke Skywalker",
      "height": "172",
      "mass": "77",
      "hair_color": "blond",
      "skin_color": "fair",
      "eye_color": "blue",
      "birth_year": "19BBY",
      "gender": "male",
      "homeworld": "http://swapi.dev/api/planets/1/",
      "films": [
        "http://swapi.dev/api/films/1/",
        "http://swapi.dev/api/films/2/",
        "http://swapi.dev/api/films/3/",
        "http://swapi.dev/api/films/6/"
      ],
      "species": [],
      "vehicles": [
        "http://swapi.dev/api/vehicles/14/",
        "http://swapi.dev/api/vehicles/30/"
      ],
      "starships": [
        "http://swapi.dev/api/starships/12/",
        "http://swapi.dev/api/starships/22/"
      ],
      "created": "2014-12-09T13:50:51.644000Z",
      "edited": "2014-12-20T21:17:56.891000Z",
      "url": "http://swapi.dev/api/people/1/"
    }
  ]
}

Let’s integrate our Vue component with the API

We’re going to use axios to consume the API. Run this command to install it.

npm install axios

Whenever the user clicks the submit button, send the request to the API, and display the result.

Let’s update our checkName methods.

<script>
import axios from "axios";
export default {
  data: () => ({
    keyword: "",
  }),
  methods: {
    checkName() {
      console.log(`Checking name: ${this.keyword}`);
      axios
        .get("https://swapi.dev/api/people/", {
          params: {
            search: this.keyword,
          },
        })
        .then((res) => {
          console.log(res.data.results);
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
};
</script>

_api-request

Nice one!

Let’s list out the result in our UI.

Add new data state to store the result; let’s called it peoples. After the request successful, we store the result in peoples state.

export default { data: () => ({ keyword: "", peoples: [], }), methods: {
checkName() { console.log(`Checking name: ${this.keyword}`); axios
.get("https://swapi.dev/api/people/", { params: { search: this.keyword, }, })
.then((res) => { console.log(res.data.results); this.peoples = res.data.results;
}) .catch((err) => { console.log(err); }); }, }, };

And this is the snippet for UI to show a list of peoples.

<ul class="px-3 list-disc">
      <li v-for="people in peoples" :key="people.url">
        {{ people.name }} - Height: {{ people.height }} Mass: {{ people.mass }}
      </li>
    </ul>

_list-result

Listen to the keyword.

Since we’re going to whenever the user typed the keyword, let’s add a listener to the keyword search. I’m going to add a keyword watcher in our component.

Whenever the keyword field changed, it will trigger the watcher. Thank god the watcher helps a lot.

watch: { keyword(newKeyword,oldKeyword) { console.log(`New keyword is
${newKeyword}`); console.log(`Old keyword is ${oldKeyword}`) } }

Whenever the keyword is changing, it will call the watcher method. Let’s call the checkName method in the watcher.

watch: { keyword(newKeyword,oldKeyword) { console.log(`New keyword is
${newKeyword}`); console.log(`Old keyword is ${oldKeyword}`); this.checkName();
} }

It’s working!

_list-request

But here comes the problem, we don’t want to keep sending the request to the backend whenever use typed. It may hurt our server.

_list-result-1

Delay the request

We need to delay the request and send the request after one second the user finish typing.

Thank god. There is a lodash for the utility library. There is a method in lodash called debounce . It's delayed the request, and it canceled the previous request if there is a new one.

It may save several requests called and provide a good experience to the user interface.

Let's install loadash first. Run npm install lodash and import the debounce methods in our vue file.

<script>
import axios from "axios"; import {debounce} from "lodash"; …
</script>

Initialize the debounce method during the created lifecycle. We passed the methods name and time delay. For this example, we will trigger the request after one second.

created() { this.debounceName = debounce(this.checkName, 1000); }

For our watcher method, we will replace it with a new function that we just created.

    watch: {
      keyword() {
        if (!this.keyword) return;
        this.debounceName();
      }
    }

Finally, its working 😄.

Now, the API will only be request after user finish typing.

_debounce

Source Code

<template>
  <div>
    <div class="w-full  px-3 mb-6">
      <label
        class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
        for="name"
      >
        Star wars name
      </label>
      <div class="relative mb-3">
        <input
          v-model="keyword"
          class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
          id="name"
        />
        <div
          class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
        >
          <svg
            class="h-4 w-4"
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="#000000"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <circle cx="11" cy="11" r="8"></circle>
            <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
          </svg>
        </div>
      </div>
      <button
        @click.prevent="checkName"
        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      >
        Search
      </button>
    </div>
 
    <ul class="px-3 list-disc">
      <li v-for="people in peoples" :key="people.url">
        {{ people.name }} - Height: {{ people.height }} Mass: {{ people.mass }}
      </li>
    </ul>
  </div>
</template>
 
<script>
import axios from "axios";
import { debounce } from "lodash";
 
export default {
  data: () => ({
    keyword: "",
    peoples: [],
  }),
  methods: {
    checkName() {
      // eslint-disable-next-line no-console
      console.log(`Checking name: ${this.keyword}`);
      axios
        .get("https://swapi.dev/api/people/", {
          params: {
            search: this.keyword,
          },
        })
        .then((res) => {
          // eslint-disable-next-line no-console
          console.log(res.data.results);
          this.peoples = res.data.results;
        })
        .catch((err) => {
          // eslint-disable-next-line no-console
          console.log(err);
        });
    },
  },
  created() {
    this.debounceName = debounce(this.checkName, 1000);
  },
  watch: {
    keyword() {
      if (!this.keyword) return;
      this.debounceName();
    },
  },
};
</script>
 
<style scoped></style>

You can view the source code in my github - Source Code

Thanks for reading my article. Let me know if you have any idea, what should I write on my next article.