How To Use Mixins in Your Vue.js Apps
By: John Au-Yeung
Tags: JavaScript, Vue Router, Vue, Mixin, Programming

Start Building

To start building the app, we have to install the Vue CLI. We do this by running:

npm install -g @vue/cli

Node.js 8.9 or later is required to run Vue CLI. I did not have success getting the Vue CLI to run with the Windows version of Node.js. Ubuntu had no problem running Vue CLI for me.

Then, to create the project folder and files, we run:

vue create vue-material-nyt-app

In the wizard, instead of using the default options, we choose ‘Manually select features’. Then, we select Babel, Router, and Vuex from the list of options by pressing space on each. If they are green, it means that they’re selected.

Now, we need to install a few libraries. We need to install an HTTP client, a library for formatting dates, one for generating GET query string from objects, and another one for form validation.

Also, we need to install the Vue Material library itself. We do this by running:

npm i axios moment querystring vee-validate vue-material

axios is our HTTP client, moment is for manipulating dates, querystring is for generating query string from objects, vee-validate is an add-on package for Vue.js to do validation, and vue-material is our Material Design library.

Now that we have all the libraries installed, we can start building our app.

First, we create some components. In the views folder, we create Home.vue and Search.vue . Those are the code files for our pages.

Then, create a mixins folder and create a file called nytMixin.js . Mixins are code snippets that can be incorporated directly into our Vue.js components and used as if they are directly in the component.

Then, we add some filters. Filters are Vue.js code that map from one thing to another.

We create a filters folder and add capitalize.js and formatDate.js . Then, in the components folder, we create a file called SearchResults.vue . The components folder contains Vue.js components that aren’t pages.

To make passing data between components easier and more organized, we use Vuex for state management.

As we selected Vuex when we ran vue create , we should have a store.js in our project folder. If not, create it. In store.js , we put:

import Vue from 'vue'

import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({

state: {

searchResults: []

},

mutations: {

setSearchResults(state, payload) {

state.searchResults = payload;

}

},

actions: { }

})

The state object is where the state is stored. The mutations object is where we can manipulate our state.

When we call this.$store.commit(“setSearchResults”, searchResults) in our code, given that searchResults is defined, then state.searchResults will be set to searchResults . We can then get the result by using this.$store.state.searchResults .

We need to add some boilerplate code to our app. First, we add our filter. In capitalize.js , we put:

export const capitalize = (str) => {

if (typeof str == 'string') {

if (str == 'realestate') {

return 'Real Estate';

}

if (str == 'sundayreview') {

return 'Sunday Review';

} if (str == 'tmagazine') {

return 'T Magazine';

}

return `${str[0].toUpperCase()}${str.slice(1)}`;

}

}

This allows us to map capitalize our New York Times section names listed at this top stories link. Then, in formatDate.js , to format our dates into human-readable format, we put:

import * as moment from 'moment'; export const formatDate = (date) => {

if (date) {

return moment(date).format('YYYY-MM-DD hh:mm A');

}

}

In main.js , we put:

import Vue from 'vue'

import App from './App.vue'

import router from './router'

import store from './store'

import VueMaterial from 'vue-material';

import VeeValidate from 'vee-validate';

import 'vue-material/dist/vue-material.min.css'

import 'vue-material/dist/theme/default.css'

import { formatDate } from './filters/formatDate';

import { capitalize } from './filters/capitalize'; Vue.config.productionTip = false; Vue.use(VueMaterial);

Vue.use(VeeValidate);

Vue.filter('formatDate', formatDate);

Vue.filter('capitalize', capitalize); new Vue({

router,

store,

render: h => h(App)

}).$mount('#app')

Notice in the file above, we have to register the libraries we use with Vue.js by calling Vue.use on them, so that they can be used in our app templates.

We call Vue.filter on our filter functions so we can use them in our templates by adding a pipe and the filter name to the right of our variable.

Then, in router.js , so we can go to the pages when we enter the URLs listed, we put:

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

}

]

})

mode: ‘history’ means that we won’t have a hash sign between the base URL and our routes. If we deploy our app, we need to configure our web server so that all requests will be redirected to index.html so we won’t have errors when we reload the app.

For example, in Apache, we do:

<IfModule mod_rewrite.c>

RewriteEngine On

RewriteBase /

RewriteRule ^index\.html$ - [L]

RewriteCond %{REQUEST_FILENAME} !-f

RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule . /index.html [L]

</IfModule>

And, in Nginx, we put:

location / {

try_files $uri $uri/ /index.html;

}

See your web server’s documentation for instructions on how to do the same thing in your web server.

Now we write the code for our components. In SearchResult.vue , we put:

<template>

<div id="search-results">

<md-card v-for="s in searchResults" :key="s.id">

<md-card-header>

<div class="md-title title">{{s.headline.main}}</div>

</md-card-header> <md-card-content>

<md-list>

<md-list-item>Date: {{s.pub_date | formatDate}}</md-list-item>

<md-list-item>

<a :href="s.web_url">Link</a>

</md-list-item>

<md-list-item v-if="s.byline.original">{{s.byline.original}}</md-list-item>

<md-list-item>{{s.lead_paragraph}}</md-list-item>

<md-list-item>{{s.snippet}}</md-list-item>

</md-list>

</md-card-content>

</md-card>

</div>

</template> <script>

export default {

computed: {

searchResults() {

return this.$store.state.searchResults;

}

}

};

</script> <style scoped>

.title {

margin: 0 15px !important;

} #search-results {

margin: 0 auto;

width: 95vw;

} .md-title.title {

color: rgba(0, 0, 0, 0.87) !important;

}

</style>

This is where we get our search results from the Vuex store and display it.

We return this.$store.state.searchResults in a function in the computed property in our app so that the search results will be automatically refreshed when the store’s searchResults state is updated.

md-card is a card widget for displaying data in a box. v-for is for looping the array entries and displaying everything. md-list is a list widget for displaying items in a list, neatly on the page. {{s.pub_date | formatDate}} is where our formatDate filter is applied.

Next, we write our mixin . We will add code for our HTTP calls in our mixin. In nytMixin.js , we put:



const querystring = require('querystring');

const apiUrl = '

const apikey = 'your api key'; const axios = require('axios');const querystring = require('querystring');const apiUrl = ' https://api.nytimes.com/svc' const apikey = 'your api key'; export const nytMixin = {

methods: {

getArticles(section) {

return axios.get(`${apiUrl}/topstories/v2/${section}.json?api-key=${apikey}`);

}, searchArticles(data) {

let params = Object.assign({}, data);

params['api-key'] = apikey;

Object.keys(params).forEach(key => {

if (!params[key]) {

delete params[key];

}

})

const queryString = querystring.stringify(params);

return axios.get(`${apiUrl}/search/v2/articlesearch.json?${queryString}`);

}

}

}

Notice that we put our functions in the methods property of the object. This is because we want the functions to be incorporated into the methods property of the component.

If an object has the same name in the component and the mixin, the component’s object will take precedence.

We return the promises for our HTTP requests to get articles in each function. In the searchArticles function, we message the object that we pass in into a query string which we pass into our request.

Make sure you put your API key into your app into the apiKey constant and delete anything that is undefined, with:

Object.keys(params).forEach(key => {

if (!params[key]) {

delete params[key];

}

})

Next in Home.vue , we put:

<template>

<div>

<div class="center">

<h1>{{selectedSection | capitalize}}</h1>

<br />

<md-menu>

<md-button class="md-raised" md-menu-trigger>Sections</md-button>

<md-menu-item v-for="s in sections" :key="s"

</md-menu-content>

</md-menu>

</div>

<br /> @click ="selectSection(s)">{{s | capitalize}}

<md-card v-for="a in articles" :key="a.id">

<md-card-header>

<div class="md-title title">{{a.title}}</div>

</md-card-header> <md-card-content>

<md-list>

<md-list-item>Date: {{a.published_date | formatDate}}</md-list-item>

<md-list-item>

<a :href="a.url">Link</a>

</md-list-item>

<md-list-item v-if="a.byline">{{a.byline}}</md-list-item>

<md-list-item>{{a.abstract}}</md-list-item>

</md-list>

<img

v-if="a.multimedia[a.multimedia.length - 1]"

:src="a.multimedia[a.multimedia.length - 1].url"

:alt="a.multimedia[a.multimedia.length - 1].caption"

class="image"

/>

</md-card-content>

</md-card>

</div>

</template> <script>

import { nytMixin } from "../mixins/nytMixin"; export default {

name: "home",

mixins: [nytMixin],

computed: {}, data() {

return {

selectedSection: "home",

articles: [],

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`

.replace(/ /g, "")

.split(",")

};

}, beforeMount() {

this.getNewsArticles(this.selectedSection);

}, methods: {

async getNewsArticles(section) {

const response = await this.getArticles(section);

this.articles = response.data.results;

}, selectSection(section) {

this.selectedSection = section;

this.getNewsArticles(section);

}

}

};

</script> <style scoped>

.image {

width: 100%;

} .title {

color: rgba(0, 0, 0, 0.87) !important;

margin: 0 15px !important;

}

</style>

This page component is where we get the articles for the selected section, defaulting to the home section. We also have a menu to select the section we want to see by adding:

<md-menu>

<md-button class="md-raised" md-menu-trigger>Sections</md-button>

<md-menu-item v-for="s in sections" :key="s"

</md-menu-item>

</md-menu-content>

</md-menu> @click ="selectSection(s)">{{s | capitalize}}

Notice that we use the async and await keywords in our promises code, instead of using then . It is much shorter and the functionality between then and await and async are equivalent.

However, it is not supported in Internet Explorer. In the beforeMount block, we run the this.getNewsArticles to get the articles as the page loads.

Then in Search.vue , we put:



<div>

<div class="center">

<h1>Search</h1>

</div>

<form

<md-field :class="{ 'md-invalid': errors.has('keyword') }">

<label for="keyword">Keyword</label>

<md-input type="text" name="keyword" v-model="searchData.keyword" v-validate="'required'"></md-input>

<span class="md-error" v-if="errors.has('keyword')">{{errors.first('keyword')}}</span>

</md-field> Search @submit ="search" novalidate> Keyword {{errors.first('keyword')}} <div>

<md-datepicker v-model="searchData.beginDate" :md-disabled-dates="disabledDates">

<label>Begin Date</label>

</md-datepicker>

</div> <div>

<md-datepicker v-model="searchData.endDate" :md-disabled-dates="disabledDates">

<label>End Date</label>

</md-datepicker>

</div> <md-field>

<label for="movie">Sort By</label>

<md-select v-model="searchData.sort">

<md-option value="newest">Newest</md-option>

<md-option value="oldest">Oldest</md-option>

<md-option value="relevance">Relevance</md-option>

</md-select>

</md-field> <md-button class="md-raised" type="submit">Search</md-button>

</form>

<SearchResults />

</div>

</template> <script>

import { nytMixin } from "../mixins/nytMixin";

import SearchResults from "@/components/SearchResults.vue";

import * as moment from "moment"; export default {

name: "search",

mixins: [nytMixin],

components: {

SearchResults

},

computed: {

isFormDirty() {

return Object.keys(this.fields).some(key => this.fields[key].dirty);

}

},

data: () => {

return {

searchData: {

sort: "newest"

},

disabledDates: date => {

return +date >= +new Date();

}

};

},

methods: {

async search(evt) {

evt.preventDefault();

if (!this.isFormDirty || this.errors.items.length > 0) {

return;

}

const data = {

q: this.searchData.keyword,

begin_date: moment(this.searchData.beginDate).format("YYYYMMDD"),

end_date: moment(this.searchData.endDate).format("YYYYMMDD"),

sort: this.searchData.sort

};

const response = await this.searchArticles(data);

this.$store.commit("setSearchResults", response.data.response.docs);

}

}

};

</script>

This is where we have a form to search for articles. We also have two date-pickers to label users to set the start and end dates. We only restrict the dates to today and earlier so that the search query makes sense.

In this block:

<md-field :class="{ 'md-invalid': errors.has('keyword') }">

<label for="keyword">Keyword</label>

<md-input type="text" name="keyword" v-model="searchData.keyword" v-validate="'required'"></md-input>

<span class="md-error" v-if="errors.has('keyword')">{{errors.first('keyword')}}</span>

</md-field>

We use vee-validate to check if the required search keyword field is filled in. If it’s not, it’ll display an error message and prevent the query from proceeding.

We also nested our SearchResults component into the Search page component by including:

components: {

SearchResults

}

Between the script tag and <SearchResults /> in the template.

Finally, we add our top bar and menu by putting the following in App.vue :



<div id="app">

<md-toolbar>

<md-button class="md-icon-button"

<md-icon>menu</md-icon>

</md-button>

<h3 class="md-title">New York Times App</h3>

</md-toolbar>

<md-drawer :md-active.sync="showNavigation" md-swipeable>

<md-toolbar class="md-transparent" md-elevation="0">

<span class="md-title">New York Times App</span>

</md-toolbar> @click ="showNavigation = true"> menu New York Times App New York Times App <md-list>

<md-list-item>

<router-link to="/">

<span class="md-list-item-text">Home</span>

</router-link>

</md-list-item> <md-list-item>

<router-link to="/search">

<span class="md-list-item-text">Search</span>

</router-link>

</md-list-item>

</md-list>

</md-drawer> <router-view />

</div>

</template> <script>

export default {

name: "app",

data: () => {

return {

showNavigation: false

};

}

};

</script> <style>

.center {

text-align: center;

} form {

width: 95vw;

margin: 0 auto;

} .md-toolbar.md-theme-default {

background: #009688 !important;

height: 60px;

} .md-title,

.md-toolbar.md-theme-default .md-icon {

color: #fff !important;

}

</style>

If you want a top bar with a left navigation drawer, you have to follow the code structure above precisely.

After all the code is written, we have the following: