In this tutorial, we are going to build a simple app for searching YouTube videos and displaying the search results. The tutorial is suited for Vue.js beginners, but it assumes that you are familiar with some web development basics. Please note that this is not a real-world web app but a great exercise to get your feet wet in Vue.js.

Prerequisites for this tutorial

  • Strong understanding of HTML and CSS
  • Good knowledge of modern JavaScript
  • Basic knowledge of REST APIs and JSON
  • Basic familiarity with the command-line and NPM
  • Basic familiarity with Bootstrap

You can use whatever code editor you prefer, but I highly recommend Visual Studio Code. Before beginning this tutorial, make sure you have Node.js installed. We will use the NPM tool to install Vue.js and two other JavaScript packages. And since we will be dealing with the YouTube API, you also need an API key. If you don’t know how to create one, you can follow my tutorial here.

Outline of what will be covered in this tutorial

  • Installation of Vue.js
  • Basic structure of a Vue.js app
  • Basic anatomy of a Vue.js component
  • Filters
  • List rendering
  • Conditional rendering
  • Event handling
  • Class bindings
  • Form input bindings

You can download the files for this tutorial from GitHub.

1. Basic Structure of the User Interface

The user interface has a simple design. Each of the red boxes represents a distinct Vue.js component:

Vue.js

The header is only used for branding purposes. Below the header is the search form. The search results are displayed by another component. The toggle button at the upper right corner of the search results component switches between a grid and a list view. Depending on the selected display mode, each video item is either rendered by a grid item component or a list item component. At the bottom of the user interface is some basic pagination for navigating through the search results. The number of results per page is adjustable in the code.

2. Installing Vue.js and Creating a New Project

Open your favorite command-line terminal and run this command to install Vue.js globally on your machine:

npm install -g @vue/cli

After the installation is complete, you can check the version with this command:

vue --version

In your command-line window, navigate to the directory where you would like to install your project. In this directory, Vue.js will create a new directory with the name of your project. Of course, you can name your project whatever you like as long as it is a valid directory name. Let’s call it youtube-search-app. To create the new project, run this command:

vue create youtube-search-app

You will be prompted to pick a preset. Just select the default preset with Babel/ESlint and hit the Enter key. Vue.js will scaffold out the basic structure of a Vue.js app for you. The installation might take a while. Once the process is completed, change to the newly created youtube-search-app directory:

cd youtube-search-app

Let’s start the dev server and see what we’ve got so far:

npm run serve

Point your web browser to the URL http://localhost:8080/. You should be greeted with this screen:

Vue.js

If this is your first Vue.js project, congratulations and welcome to the world of Vue.js!

Before we proceed, let’s install two modules that we will need later on. The first one is Axios, a very popular JavaScript library for performing HTTP requests. Axios is a convenient wrapper for JavaScript’s fetch() method and helps you write less and cleaner code.

Either stop the dev server with Ctrl+C or open another terminal window and run this command:

npm install axios

The second one is Moment.js, a utility module for formatting times and dates in JavaScript. I’m sure you know the drill by now:

npm install moment

3. Tweaking the Created Files

Let’s take a closer look at some of the files that were created by Vue.js.

Vue.js

3.1 public/index.html

The index.html file contains some basic HTML markup and is the first file that loads when the application starts. Please note the <div id="app"> element within the <body> section. This element will be replaced with HTML code generated by Vue.js.

We only need to change the title of the document, and add the CDN links of Bootstrap and Font Awesome. Go to Bootstrap’s Quick Start section, copy the stylesheet and JavaScript links, and paste them into the HTML code. Next, get the Font Awesome stylesheet link and paste it into the document header. Your index.html file should look similar to this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS"
    crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
    crossorigin="anonymous">
  <title>YoutTube Search App</title>
</head>
<body>
  <noscript>
    <strong>We're sorry but the YouTube Search App doesn't work properly without JavaScript enabled. Please enable it
      to continue.</strong>
  </noscript>
  <div id="app"></div>
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
    crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
    crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
    crossorigin="anonymous"></script>
</body>
</html>

3.2 src/main.js

This is the main JavaScript file where the Vue instance is created and assigned to the DOM element identified by #app.

We are adding a filter here that we will use to format the date/time string in YouTube’s publishedAt property. Filter functions are commonly used in Vue.js for text formatting. You can define a filter locally in a component’s options. However, we will need the formatDate() function in two different components, so we are defining it globally in the main.js file:

import Vue from 'vue'
import App from './App.vue'
import moment from 'moment'

Vue.config.productionTip = false

Vue.filter('formatDate', function (value) {
  if (!value) return ''
  return moment(value.toString()).format('MM/DD/YYYY hh:mm')
})

new Vue({
  render: h => h(App),
}).$mount('#app')

Line 3: First, we need to import the moment module we installed earlier.

Lines 7-10: The filter itself calls the moment() function, which returns a date/time string in the format MM/DD/YYYY hh:mm.

Lines 12-14: The Vue instance is created. You can find a brief explanation of this shorthand syntax here.

3.3 src/App.vue

This is our main app component which acts as a container for all other components. Like any other Vue.js component, it contains three sections: one for the HTML template, one for the JavaScript code, and one for the CSS styling. For the moment, let’s strip the code down to the bare minimum:

<template>
  <div id="app"></div>
</template>

<script>
export default {
  name: 'app',
  components: {

  }
}
</script>

You can remove the assets folder and the HelloWorld.vue file in the components directory. We don’t need them anymore.

4. Working with the YouTube API

Before we start creating our components, we need to understand the mechanics of the YouTube API. The base URL is https://www.googleapis.com/youtube/v3/search?, followed by a list of parameters. Some are required for any API request, such as type and part. The type parameter identifies the available resource types, whereas part defines the resource properties that should be included in the API response. For our purposes, we need to use type=video and part=snippet. For example, if we omit the type parameter, the search results do not only include videos but also channels and playlists.

We will also be using the parameters order, maxResults, q, and key. The first two are self-explanatory, the q parameter takes the query string, and key has to be set to a valid API key.

As an example, if we want to search for the keywords “vuejs” and “tutorial” and limit the results to three per page, the complete URL looks like this:

https://www.googleapis.com/youtube/v3/search?type=video&part=snippet&order=viewCount&q=vuejs+tutorial&maxResults=3&key=YOUR_API_KEY

Of course, you have to replace “YOUR_API_KEY” with your own API key. Here’s the JSON object that we get back from the API:

{
  "kind": "youtube#searchListResponse",
  "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/iyJ6Qkewu1xk5TBsgfwd2c99ZK4\"",
  "nextPageToken": "CAMQAA",
  "regionCode": "DE",
  "pageInfo": {
    "totalResults": 40097,
    "resultsPerPage": 3
  },
  "items": [
    {
      "kind": "youtube#searchResult",
      "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/aM1W2UxtlNX12R22a2F4xZLNqpQ\"",
      "id": {
        "kind": "youtube#video",
        "videoId": "z6hQqgvGI4Y"
      },
      "snippet": {
        "publishedAt": "2016-12-04T20:16:40.000Z",
        "channelId": "UC29ju8bIPH5as8OGnQzwJyA",
        "title": "Vue.js 2.0 In 60 Minutes",
        "description": "UPDATED VERSION - https://www.youtube.com/watch?v=Wy9q22isx3U In this crash course we will cover all of the fundamentals of the Vue.js 2.0 JavaScript ...",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/z6hQqgvGI4Y/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/z6hQqgvGI4Y/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/z6hQqgvGI4Y/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "Traversy Media",
        "liveBroadcastContent": "none"
      }
    },
    {
      "kind": "youtube#searchResult",
      "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/kHrTO7gx-tkLN230vKKaeyx_g9k\"",
      "id": {
        "kind": "youtube#video",
        "videoId": "5LYrN_cAJoA"
      },
      "snippet": {
        "publishedAt": "2017-04-10T15:11:50.000Z",
        "channelId": "UCW5YeuERMmlnqo4oq8vwUpg",
        "title": "Vue JS 2 Tutorial #1 - Introduction",
        "description": "Donate via PayPal - https://www.paypal.me/thenetninja Donate via Patreon - https://www.patreon.com/thenetninja Hey gang and welcome to your first Vue.js ...",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/5LYrN_cAJoA/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/5LYrN_cAJoA/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/5LYrN_cAJoA/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "The Net Ninja",
        "liveBroadcastContent": "none"
      }
    },
    {
      "kind": "youtube#searchResult",
      "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/k8RTJizeRHop_rYqi-GRszi4g48\"",
      "id": {
        "kind": "youtube#video",
        "videoId": "78tNYZUS-ps"
      },
      "snippet": {
        "publishedAt": "2018-02-13T18:21:53.000Z",
        "channelId": "UCVyRiMvfUNMA1UPlDPzG5Ow",
        "title": "The Vue Tutorial for 2018 - Learn Vue 2 in 65 Minutes",
        "description": "SUBSCRIBE if you enjoy this! The video course and written lessons: https://goo.gl/8MWGxy In this course, you're going to learn all about the major topics ...",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/78tNYZUS-ps/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/78tNYZUS-ps/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/78tNYZUS-ps/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "DesignCourse",
        "liveBroadcastContent": "none"
      }
    }
  ]
}

The JSON object has a few top-level properties that reveal meta-information about the response data. We are especially interested in the nextPageToken property. Since this is page 1 of the search results, there’s no prevPageToken. In order to retrieve page 2, just add the parameter nextPageToken and the value provided in the JSON object to the URL and call the API again. The new JSON object will include both tokens.  This way we can set up the pagination for the search results.

Of course, the main property we are interested in is the items array. Each object in this array represents a YouTube video. We will only use these properties for our app:

  • id.videoId
  • snippet.thumbnails.medium.url
  • snippet.title
  • snippet.description
  • snippet.channelTitle
  • snippet.publishedAt

Now that we know how to set up a request and what we get back from the API, we can write the code for our main component.

5. Creating the Main Component (App.vue)

Since our app is fairly simple, there’s not a lot going on in our state management. All our data will be stored in the main component. In more complex apps, you would rather use a state manager like Vuex.

5.1 Data

So what properties do we need in our main component’s data object? Obviously, we need an array for the video items that we get from the YouTube API. Being very creative in our naming, we call it videos.

As you may have noticed in the screenshot of the finished app, there’s a heading at the top of the search results that says “Search Results for”, followed by the search string in quotes. Actually, this is not the original search string that was typed into the input field, it’s a cleaned-up version. Let’s call this property reformattedSearchString.

Both properties will be dynamically assigned to the search results component (SearchResults.vue). Whenever these properties are updated, any UI element that is bound to them will be redrawn automatically.

I decided to put all the URL parts into an object called api, so the code for constructing the different URLs gets more readable. This is also the place where you can set the maximum number of search results per page (api.maxResults).

Here’s the corresponding code snippet from App.vue. I will provide the complete App.vue code at the end of this section.

<script>
export default {
  name: 'app',
  components: {},
  data() {
    return {
      videos: [],
      reformattedSearchString: '',
      api: {
        baseUrl: 'https://www.googleapis.com/youtube/v3/search?',
        part: 'snippet',
        type: 'video',
        order: 'viewCount',
        maxResults: 12,
        q: '',
        key: YOUR_API_KEY,
        prevPageToken: '',
        nextPageToken: ''
      }
    };
  },
  methods: {}
};
</script>

5.2 Methods

I split the tasks of our app into the four methods search(), prevPage(), nextPage(), and getData().

The following is a code snippet of the method search():

search(searchParams) {
  this.reformattedSearchString = searchParams.join(' ');
  this.api.q = searchParams.join('+');
  const { baseUrl, part, type, order, maxResults, q, key } = this.api;
  const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}`;
  this.getData(apiUrl);
},

The method takes an array of search words (searchParams). This array is being created inside the SearchForm component and is sent to the main component along with an event notification. We’ll come back to this when we take a look at the SearchForm.vue code.

Inside the method search() we also assign a value to the reformattedSearchString property by joining the search words with a space character. In order to provide a valid query parameter (q), the search words are joined again but this time with a plus sign. Finally, the URL is created and passed on to the getData() method.

Please note the ES6 destructuring syntax in this line:

const { baseUrl, part, type, order, maxResults, q, key } = this.api;

If you’re not familiar with the desctructuring concept, I recommend reading this article by Wes Bos.

The methods prevPage() and nextPage() are based on the same logic as search():

prevPage() {
  const { baseUrl, part, type, order, maxResults, q, key, prevPageToken } = this.api;
  const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}&pageToken=${prevPageToken}`;
  this.getData(apiUrl);
},

nextPage() {
  const { baseUrl, part, type, order, maxResults, q, key, nextPageToken } = this.api;
  const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}&pageToken=${nextPageToken}`;
  this.getData(apiUrl);
},

The getData() method performs the actual HTTP requests by using the Axios module. It also updates the videos, prevPageToken, and nextPageToken properties:

getData(apiUrl) {
  axios
    .get(apiUrl)
    .then(res => {
      this.videos = res.data.items;
      this.api.prevPageToken = res.data.prevPageToken;
      this.api.nextPageToken = res.data.nextPageToken;
    })
    .catch(error => console.log(error));
}

5.3 Data Binding and Events

Now that we have the code for constructing URLs and making HTTP requests in place, we can set up the data binding. But first, let’s create some empty component files.

Make a new directory inside components and call it layout. Of course, you can place all your components in the same directory, but I like to distinguish between Vue.js components that include UI logic and others that are just used for layout purposes.

Inside the components/layout directory, create a file named Header.vue. Then create the files Pagination.vue, SearchForm.vue, SearchResults.vue, VideoGridItem.vue, and VideoListItem.vue inside the components folder. I suggest you add empty <template> tags to these files to avoid error messages in the browser’s console while testing.

Your file and directory structure should now look like this:

Vue.js

In our next step, we incorporate the components HeaderSearchFormSearchResults, and Paginationinto the App component. This is usually a three-step process:

  1. Place the components in the proper position inside the <template> section.
  2. Import the component files at the top of the <script> section.
  3. Register the components in the components object.

The communication between the components is based on data bindings and events. The following code snippet from the App.vue file shows how the components SearchFormSearchResults, and Pagination interact with the main component.

The communication between the components is based on data bindings and events. The following code snippet from the App.vue file shows how the components SearchForm, SearchResults, and Pagination interact with the main component.

<template>
  <div id="app">
    <Header/>
    <SearchForm v-on:search="search"/>
    <SearchResults
      v-if="videos.length > 0"
      v-bind:videos="videos"
      v-bind:reformattedSearchString="reformattedSearchString"
    />
    <Pagination
      v-if="videos.length > 0"
      v-bind:prevPageToken="api.prevPageToken"
      v-bind:nextPageToken="api.nextPageToken"
      v-on:prev-page="prevPage"
      v-on:next-page="nextPage"
    />
  </div>
</template>

Line 4: The attribute v-on:search="search" adds an event listener that triggers the search() method.

Line 6: The SearchResults component is only displayed if the videos array is not empty. In Vue.js speak, this is called conditional rendering.

Lines 7-8: The properties videos and reformattedSearchString are passed on to the SearchResults component via data binding.

Line 11: The pagination is only displayed if the videos array contains data.

Lines 12-13: The properties api.prevPageToken and api.nextPageToken are passed on to the Pagination component via data binding.

Lines 14-15: Event listeners trigger the prevPage() and nextPage() methods.

5.4 Completed Code

Here’s the completed code of the App.vue file:

<template>
  <div id="app">
    <Header/>
    <SearchForm v-on:search="search"/>
    <SearchResults
      v-if="videos.length > 0"
      v-bind:videos="videos"
      v-bind:reformattedSearchString="reformattedSearchString"
    />
    <Pagination
      v-if="videos.length > 0"
      v-bind:prevPageToken="api.prevPageToken"
      v-bind:nextPageToken="api.nextPageToken"
      v-on:prev-page="prevPage"
      v-on:next-page="nextPage"
    />
  </div>
</template>

<script>
import Header from './components/layout/Header';
import SearchForm from './components/SearchForm';
import SearchResults from './components/SearchResults';
import Pagination from './components/Pagination';
import axios from 'axios';

export default {
  name: 'app',
  components: {
    Header,
    SearchForm,
    SearchResults,
    Pagination
  },
  data() {
    return {
      videos: [],
      reformattedSearchString: '',
      api: {
        baseUrl: 'https://www.googleapis.com/youtube/v3/search?',
        part: 'snippet',
        type: 'video',
        order: 'viewCount',
        maxResults: 12,
        q: '',
        key: 'YOUR_API_KEY',
        prevPageToken: '',
        nextPageToken: ''
      }
    };
  },
  methods: {
    search(searchParams) {
      this.reformattedSearchString = searchParams.join(' ');
      this.api.q = searchParams.join('+');
      const { baseUrl, part, type, order, maxResults, q, key } = this.api;
      const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}`;
      this.getData(apiUrl);
    },

    prevPage() {
      const { baseUrl, part, type, order, maxResults, q, key, prevPageToken } = this.api;
      const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}&pageToken=${prevPageToken}`;
      this.getData(apiUrl);
    },

    nextPage() {
      const { baseUrl, part, type, order, maxResults, q, key, nextPageToken } = this.api;
      const apiUrl = `${baseUrl}part=${part}&type=${type}&order=${order}&q=${q}&maxResults=${maxResults}&key=${key}&pageToken=${nextPageToken}`;
      this.getData(apiUrl);
    },

    getData(apiUrl) {
      axios
        .get(apiUrl)
        .then(res => {
          this.videos = res.data.items;
          this.api.prevPageToken = res.data.prevPageToken;
          this.api.nextPageToken = res.data.nextPageToken;
        })
        .catch(error => console.log(error));
    }
  }
};
</script>

6. Creating the Component Files

Now it’s time to add life to the other components.

6.1 The Header Component

This is a static component with some basic HTML code that is decorated with some Bootstrap classes. It’s just there for eye candy. Type or copy the following code into the Header.vue file inside the src/components/layout directory.

<template>
  <header>
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark mb-5">
      <div class="container">
        <a class="navbar-brand" href="#">
          <i class="fab fa-youtube fa-2x"></i>
          <span class="ml-3">YouTube Search</span>
        </a>
      </div>
    </nav>
  </header>
</template>

<script>
export default {
  name: 'Header'
};
</script>

<style scoped>
i {
  vertical-align: middle;
  color: red;
}

i + span {
  vertical-align: middle;
}
</style>

6.2 The SearchForm Component

The search form consists of an input field and a button. Please scroll down and read the line-by-line explanation below the code.

<template>
  <div class="container">
    <form class="mb-5">
      <div class="input-group">
        <input
          v-model="searchString"
          @keydown.13.prevent="parseSearchString"
          type="text"
          class="form-control"
          placeholder="Search ..."
        >
        <div class="input-group-append">
          <button @click="parseSearchString" class="btn btn-outline-secondary" type="button">
            <i class="fas fa-search"></i>
          </button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  name: 'SearchForm',
  data() {
    return {
      searchString: ''
    };
  },
  methods: {
    parseSearchString() {
      // Trim search string
      const trimmedSearchString = this.searchString.trim();

      if (trimmedSearchString !== '') {
        // Split search string
        const searchParams = trimmedSearchString.split(/\s+/);
        // Emit event
        this.$emit('search', searchParams);
        // Reset input field
        this.searchString = '';
      }
    }
  }
};
</script>

<style scoped>
input,
button {
  box-shadow: none !important;
}

.form-control {
  border-color: #6c757d;
}
</style>

Line 6: The v-model directive is used to bind the input to the searchString property in the component’s data object (line 27).

Line 7: Instead of using the search button, the user might just hit the Enter key (key code 13). Therefore, we are binding a keydown event listener to a method named parseSearchString (lines 31-43). In vanilla JavaScript, it’s common to call event.preventDefault() inside event handlers to prevent the browser’s default behaviour. Vue.js provides the event modifier prevent for exactly this purpose. Please note that @keydown is shorthand syntax for the Vue.js directive v-on:keydown.

Line 13: We are adding an @click event listener to the button and bind it to the same method as before. Again, @click is short for v-on:click.

Line 27: The searchString property is initially set to an empty string.

Lines 31-43: Before the search string is passed on to the YouTube API, we do some basic sanity checks and convert the string into an array of distinct keywords.

Line 33: Any leading and/or trailing whitespace is removed from the search string.

Line 37: The search string is split by whitespace into the array searchParams.

Line 39: An event with the name search is triggered. The array searchParams is passed on as an additional argument. We catch this event in our main app component.

Line 41: The searchString property of the component’s data object is reset to an empty string. Due to the magic of two-way binding, this also clears the input field.

6.3 The SearchResults Component

This component displays the search results either in grid or in list view. The display mode can be changed with the help of two buttons at the upper right corner of the component. The layout relies on a few standard Bootstrap classes.

<template>
  <div class="container mb-3">
    <div class="d-flex mb-3">
      <div class="mr-auto">
        <h3>Search Results for "{{ reformattedSearchString }}"</h3>
      </div>
      <div class="btn-group ml-auto" role="group">
        <button
          @click="changeDisplayMode('grid')"
          type="button"
          class="btn btn-outline-secondary"
          v-bind:class="{ active: displayMode === 'grid' }"
        >
          <i class="fas fa-th"></i>
        </button>
        <button
          @click="changeDisplayMode('list')"
          type="button"
          class="btn btn-outline-secondary"
          v-bind:class="{ active: displayMode === 'list' }"
        >
          <i class="fas fa-list"></i>
        </button>
      </div>
    </div>

    <div class="card-columns" v-if="displayMode === 'grid'">
      <div class="card" v-bind:key="video.id.videoId" v-for="video in videos">
        <VideoGridItem v-bind:video="video"/>
      </div>
    </div>
    <div v-else>
      <div class="card mb-2" v-bind:key="video.id.videoId" v-for="video in videos">
        <VideoListItem v-bind:video="video"/>
      </div>
    </div>
  </div>
</template>

<script>
import VideoListItem from './VideoListItem';
import VideoGridItem from './VideoGridItem';

export default {
  name: 'SearchResults',
  components: {
    VideoListItem,
    VideoGridItem
  },
  data() {
    return {
      title: 'Search Results',
      displayMode: 'grid'
    };
  },
  methods: {
    changeDisplayMode(displayMode) {
      this.displayMode = displayMode;
    }
  },
  props: ['videos', 'reformattedSearchString']
};
</script>

<style scoped>
button:focus {
  box-shadow: none !important;
}
</style>

Line 5: The property reformattedSearchString is embedded into the <h3> heading.

Line 9: An event listener is added to the first button. If the button is clicked, the method changeDisplayMode() gets called.

Line 12: Vue.js supports dynamic class binding. If the displayMode is set to “grid”, the CSS class “active” is added to the button.

Lines 17-20: The second button follows the same pattern as the first one.

Lines 27-36: Please note the v-if directive in line 27 and the v-else directive in line 32. This is another example of conditional rendering. Depending on the selected displayMode, either the VideoGridItem or the VideoListItem component is used.

Line 28: We use the v-for directive for looping through the video items. To give Vue.js a hint so that it can track each item’s identity, we need to provide a unique key attribute for each item. The property video.id.videoId is the ideal candidate for this task.

Line 29: While iterating through the array, each video object is bound to an instance of the VideoGridItem component.

Lines 33-35: This is the same logic as in the lines 28-30.

Lines 47-48: The components VideoGridItem and VideoListItem are registered.

Line 61: Each external property that is passed on to a component needs to be registered in props.

6.4 The VideoGridItem Component

The code of this component is pretty simple. Again, a few Bootstrap classes are applied to the HTML markup to make it look pretty. The different properties of the video object are displayed by utilizing the text interpolation feature of Vue.js. The double curly braces are also known as “mustache” syntax.

<template>
  <div>
    <a :href="'https://www.youtube.com/watch?v=' + video.id.videoId" target="_blank">
      <img class="card-img-top" :src="video.snippet.thumbnails.medium.url" alt="YouTube thumbnail">
    </a>
    <div class="card-body">
      <h5 class="card-title">{{ video.snippet.title }}</h5>
      <h6
        class="card-subtitle mb-2 text-muted"
      >{{ video.snippet.channelTitle }} | {{ video.snippet.publishedAt | formatDate }}</h6>
      <p class="card-text">{{ video.snippet.description }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VideoGridItem',
  props: ['video']
};
</script>

Line 3: The link to the YouTube video is injected into the href attribute. Please note that :href is short for v-bind:href!

Line 4: We use shorthand syntax for binding the URL of YouTube’s thumbnail image to the src attribute.

Line 10: We apply the formatDate() filter that we defined earlier in the main.js file. Vue.js uses the pipe symbol for applying filters to JavaScript expressions.

6.5 The VideoListItem Component

This component works the same as the VideoGridItem component.

<template>
  <div>
    <div class="card-body">
      <h5 class="card-title">
        <a
          :href="'https://www.youtube.com/watch?v=' + video.id.videoId"
          class="card-link"
          target="_blank"
        >{{ video.snippet.title }}</a>
      </h5>
      <h6
        class="card-subtitle mb-2 text-muted"
      >{{ video.snippet.channelTitle }} | {{ video.snippet.publishedAt | formatDate }}</h6>
      <p class="card-text">{{ video.snippet.description }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VideoListItem',
  props: ['video']
};
</script>

6.6 The Pagination Component

This component should also be pretty self-explanatory. There are a few explanations below the code.

<template>
  <div class="container">
    <nav>
      <ul class="pagination justify-content-end">
        <li class="page-item" v-bind:class="{ disabled: prevPageToken === undefined }">
          <a @click="prevPage" class="page-link" href="#">Previous</a>
        </li>
        <li class="page-item" v-bind:class="{ disabled: nextPageToken === undefined }">
          <a @click="nextPage" class="page-link" href="#">Next</a>
        </li>
      </ul>
    </nav>
  </div>
</template>

<script>
export default {
  name: 'Pagination',
  props: ['prevPageToken', 'nextPageToken'],
  methods: {
    prevPage() {
      this.$emit('prev-page');
    },
    nextPage() {
      this.$emit('next-page');
    }
  }
};
</script>

<style scoped>
.page-link {
  box-shadow: none !important;
}
</style>

Line 5: The CSS class “disabled” is added to the previous page button if the prevPageToken has a value of undefined.

Line 8: Same story. The CSS class “disabled” is added to the next page button if the corresponding token is empty.

Lines 21-26: The methods prevPage() and nextPage() emit events that bubble up to the main component.

If everything went smoothly, your grid and list views should look similar to these screenshots.

Vue.js
Vue.js

7. Deploying the App

The final step is to deploy our app. Fortunately, Vue.js makes this a very easy task. Get back to your terminal window and run this command:

npm run build

The build process generates  a new folder called dist that contains the distribution files.

I hope you found this tutorial helpful. If you have any questions, please leave a comment below.