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:
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:
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.
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:
In our next step, we incorporate the components Header
, SearchForm
, SearchResults
, and Pagination
into the App
component. This is usually a three-step process:
- Place the components in the proper position inside the
<template>
section. - Import the component files at the top of the
<script>
section. - 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 SearchForm
, SearchResults
, 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.
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.
Leave A Comment