Slots is a useful feature of Vue.js that allows you to separate different parts of a component into an organized unit. With your component compartmentalized into slots, you can reuse components by putting them into the slots you defined. It also makes your code cleaner since it lets you separate the layout from the logic of the app.
Also, if you use slots, you no longer have to compose components with parent child relationship since you can put any components into your slots.
A simple example of Vue slots would be the following. You define your slot in Layout.vue
file:
`<template>
<div class="frame">
<slot` name="`frame`"`></slot>
</div>
</template>`
Then in another file, you can add:
<`Layout>` <template v-slot:frame>
`<img src="an-image.jpg">
</template>
</Layout>`
To use the slot in your Layout
component.
We will clarify the above example by building an example app. To illustrate the use of slots in Vue.js, we will build a responsive app that displays article snippets from the New York Times API and a search page where users can enter a keyword to search the API.
The desktop layout will have a list of item names on the left and the article snippets on the right. The mobile layout will have a drop down for selecting the section to display and the cards displaying the article snippets below it.
The search page will have a search form on top and the article snippets below it regardless of screen size.
To start building the app, we start by running the Vue CLI. We run:
npx @vue/cli create nyt-app
to create the Vue.js project. When the wizard shows up, we choose ‘Manually select features’. Then we choose to include Vue Router and Babel in our project.
Next we add our own libraries for styling and making HTTP requests. We use BootstrapVue for styling, Axios for making requests, VueFilterDateFormat for formatting dates and Vee-Validate for form validation.
To install all the libraries, we run:
npm i axios bootstrap-vue vee-validate vue-filter-date-format
After all the libraries are installed, we can start building our app.
First we use slots yo build our layouts for our pages. Create BaseLayout.vue
in the components
folder and add:
<template>
<div>
<div class="row">
<div class="col-md-3 d-none d-lg-block d-xl-none d-xl-block">
<slot name="left"></slot>
</div>
<div class="col">
<div class="d-block d-sm-none d-none d-sm-block d-md-block d-lg-none">
<slot name="section-dropdown"></slot>
</div>
<slot name="right"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "BaseLayout"
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
In this file, we make use of Vue slots to create the responsive layout for the home page. We have the left
, right
, and section-dropdown
slots in this file. The left
slot only displays when the screen is large since we added the d-none d-lg-block d-xl-none d-xl-block
classes to the left
slot. The section-dropdown
slot only shows on small screens since we added the d-block d-sm-none d-none d-sm-block d-md-block d-lg-none
classes to it. These classes are the responsive utility classes from Bootstrap.
The full list of responsive utility classes are at https://getbootstrap.com/docs/4.0/utilities/display/
Next, create a SearchLayout.vue
file in the components
folder and add:
<template>
<div class="row">
<div class="col-12">
<slot name="top"></slot>
</div>
<div class="col-12">
<slot name="bottom"></slot>
</div>
</div>
</template>
<script>
export default {
name: "SearchLayout"
};
</script>
to create another layout for our search page. We have the top
and bottom
slots taking up the whole width of the screen.
Then we create a mixins
folder and in it, create a requestsMixin.js
file and add:
const axios = require("axios");
const APIURL = "https://api.nytimes.com/svc";
export const requestsMixin = {
methods: {
getArticles(section) {
return axios.get(
`${APIURL}/topstories/v2/${section}.json?api-key=${process.env.VUE_APP_API_KEY}`
);
},
searchArticles(keyword) {
return axios.get(
`${APIURL}/search/v2/articlesearch.json?api-key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
);
}
}
};
to create a mixin for making HTTP requests to the New York Times API. process.env.VUE_APP_API_KEY
is the API key for the New York Times API, and we get it from the .env
file in the project’s root folder, with the key of the environment variable being VUE_APP_API_KEY
.
Next in Home.vue
, replace the existing code with:
<template>
<div class="page">
<h1 class="text-center">Home</h1>
<BaseLayout>
<template v-slot:left>
<b-nav vertical pills>
<b-nav-item
v-for="s in sections"
:key="s"
:active="s == selectedSection"
@click="selectedSection = s; getAllArticles()"
>{{s}}</b-nav-item>
</b-nav>
</template>
<template v-slot:section-dropdown>
<b-form-select
v-model="selectedSection"
:options="sections"
@change="getAllArticles()"
id="section-dropdown"
></b-form-select>
</template>
<template v-slot:right>
<b-card
v-for="(a, index) in articles"
:key="index"
:title="a.title"
:img-src="(Array.isArray(a.multimedia) && a.multimedia.length > 0 && a.multimedia[a.multimedia.length-1].url) || ''"
img-bottom
>
<b-card-text>
<p>{{a.byline}}</p>
<p>Published on: {{new Date(a.published_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
<p>{{a.abstract}}</p>
</b-card-text>
<b-button :href="a.short_url" variant="primary" target="_blank">Go</b-button>
</b-card>
</template>
</BaseLayout>
</div>
</template>
<script>
// @ is an alias to /src
import BaseLayout from "@/components/BaseLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "home",
components: {
BaseLayout
},
mixins: [requestsMixin],
data() {
return {
sections: `arts, automobiles, books, business, fashion,
food, health, home, insider, magazine, movies, national,
nyregion, obituaries, opinion, politics, realestate, science,
sports, sundayreview, technology, theater,
tmagazine, travel, upshot, world`
.split(",")
.map(s => s.trim()),
selectedSection: "arts",
articles: []
};
},
beforeMount() {
this.getAllArticles();
},
methods: {
async getAllArticles() {
const response = await this.getArticles(this.selectedSection);
this.articles = response.data.results;
},
setSection(ev) {
this.getAllArticles();
}
}
};
</script>
<style scoped>
#section-dropdown {
margin-bottom: 10px;
}
</style>
We use the slots defined in BaseLayout.vue
in this file. In the left
slot, we put the list of section names in there to display the list on the left when we have a desktop sized screen.
In the section-dropdown
slot, we put the drop down that only shows in mobile screens as defined in BaseLayout
.
Then in the right
slot, we put the Bootstrap cards for displaying the article snippets, also as defined in BaseLayout
.
We put all the slot contents inside BaseLayout
and we use v-slot
outside the items we want to put into the slots to make the items show in the designated slot.
In the script
section, we get the articles by section by defining the getAllArticles
function from requestsMixin
.
Next create a Search.vue
file and add:
<template>
<div class="page">
<h1 class="text-center">Search</h1>
<SearchLayout>
<template v-slot:top>
<ValidationObserver ref="observer" v-slot="{ invalid }">
<b-form @submit.prevent="onSubmit" novalidate id="form">
<b-form-group label="Keyword" label-for="keyword">
<ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
<b-form-input
:state="errors.length == 0"
v-model="form.keyword"
type="text"
required
placeholder="Keyword"
name="keyword"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-button type="submit" variant="primary">Search</b-button>
</b-form>
</ValidationObserver>
</template>
<template v-slot:bottom>
<b-card v-for="(a, index) in articles" :key="index" :title="a.headline.main">
<b-card-text>
<p>By: {{a.byline.original}}</p>
<p>Published on: {{new Date(a.pub_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
<p>{{a.abstract}}</p>
</b-card-text>
<b-button :href="a.web_url" variant="primary" target="_blank">Go</b-button>
</b-card>
</template>
</SearchLayout>
</div>
</template>
<script>
// @ is an alias to /src
import SearchLayout from "@/components/SearchLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "home",
components: {
SearchLayout
},
mixins: [requestsMixin],
data() {
return {
articles: [],
form: {}
};
},
methods: {
async onSubmit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
return;
}
const response = await this.searchArticles(this.form.keyword);
this.articles = response.data.response.docs;
}
}
};
</script>
<style scoped>
</style>
It’s very similar to Home.vue
. We put the search form in the top
slot by putting it inside the SearchLayour
, and we put our slot content for the top
slot by putting our form inside the <template v-slot:top>
element.
We use the ValidationObserver
to validate the whole form, and ValidationProvider
to validate the keyword
input. They are both provided by Vee-Validate.
Once the Search button is clicked, we call this.$refs.observer.validate();
to validate the form. We get the this.$refs.observer
since we wrapped the ValidationObserver
outside the form.
Then once form validation succeeds, by this.$refs.observer.validate()
resolving to true
, we call searchArticles
from requestsMixin
to search for articles.
In the bottom
slot, we put the cards for displaying the article search results. It works the same way as the other slots.
Next in App.vue
, we put:
<template>
<div>
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#">New York Times App</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/" :active="path == '/'">Home</b-nav-item>
<b-nav-item to="/search" :active="path == '/search'">Search</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<router-view />
</div>
</template>
<script>
export default {
data() {
return {
path: this.$route && this.$route.path
};
},
watch: {
$route(route) {
this.path = route.path;
}
}
};
</script>
<style>
.page {
padding: 20px;
}
</style>
to we add the BootstrapVue b-navbar
here and watch the route as it changes so that we can set the active
prop to the link of the page the user is currently in.
Next we change main.js
‘s code to:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import VueFilterDateFormat from "vue-filter-date-format";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
Vue.use(VueFilterDateFormat);
Vue.use(BootstrapVue);
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
We import all the app-wide packages we use here, like BootstrapVue, Vee-Validate and the calendar and date-time picker widgets.
The styles are also imported here so we can see them throughout the app.
Next in router.js
, replace the existing code with:
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Search from "./views/Search.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/search",
name: "search",
component: Search
}
]
});
to set the routes for our app, so that when users enter the given URL or click on a link with it, they can see our page.
Finally, we replace the code in index.html
with:
<!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" />
<title>New York Times App</title>
</head>
<body>
<noscript>
<strong
>We're sorry but vue-slots-tutorial-app doesn't work properly without
JavaScript enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
to change the app’s title.
Finally we run our app by running npm run serve
in our app’s project folder to run our app.