How To Use Vuetify in Vue.js
Tags: vue, webdev, javascript, tutorial
Published: 2020-01-22 03:00:47

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

**Even more articles at http://thewebdev.info/**

There is great support for [Material Design](https://material.io/design/) in Vue.js. One of the libraries available for Vue.js is [Vuetify](https://vuetifyjs.com/en/). It is easy to incorporate into your Vue.js app and the result is appealing to the users’ eyes.

In this piece, we will build an app that displays data from the New York Times API. You can register for an API key at [https://developer.nytimes.com/](https://developer.nytimes.com/). After that, we can start building the app.

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

```bash
npm install -g @vue/cli
```

Node.js 8.9 or later is required for Vue CLI to run. 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, we run:

```
vue create vuetify-nyt-app
```

To create the project folder and create the files. In the wizard, instead of using the default options, we choose ‘Manually select features’. We select [Babel](https://babeljs.io/), [Router](https://router.vuejs.org/), and [Vuex](https://vuex.vuejs.org/) from the list of options by pressing space on each. If they are green, it means 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 strings from objects, and another one for form validation.

Also, we need to install the [Vue Material](https://vuematerial.io/) library itself. We do this by running:

```bash
npm i axios moment querystring vee-validate
```

`axios` is our HTTP client, `moment` is for manipulating dates, `querystring` is for generating query strings from objects, and `vee-validate` is an add-on package for Vue.js to do validation.

Then, we have to add the boilerplate for `vuetify`. We do this by running `vue add vuetify`. This adds the library and the references to it in our app, in their appropriate locations in our code.

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 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`.

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:

```javascript
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, `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:

```javascript
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 in the [New York Times developer pages](https://developer.nytimes.com/docs/top-stories-product/1/routes/%7Bsection%7D.json/get). Then, in `formatDate.js`, we put:

```javascript
import * as moment from 'moment';
export const formatDate = (date) => {
    if (date) {
        return moment(date).format('YYYY-MM-DD hh:mm A');
    }
}
```

To format our dates into a human-readable format.

In `main.js`, we put:

```javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { formatDate } from './filters/formatDate';
import { capitalize } from './filters/capitalize';
import VeeValidate from 'vee-validate';
import Vuetify from 'vuetify/lib'
import vuetify from './plugins/vuetify';
import '@mdi/font/css/materialdesignicons.css'
Vue.config.productionTip = false;
Vue.use(VeeValidate);
Vue.use(Vuetify);
Vue.filter('formatDate', formatDate);
Vue.filter('capitalize', capitalize);
new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
}).$mount('#app')
```

Notice that, in the file above, we have to register the libraries we use with Vue.js by calling `Vue.use` on them, so 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`, we put:

```javascript
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
    }
  ]
})
```

So that we can go to the pages when we enter the URLs listed.

`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](https://www.nginx.com/), we put:

```
location / {
  try_files $uri $uri/ /index.html;
}
```

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

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

```javascript
<template>
  <v-container>
    <v-card v-for="s in searchResults" :key="s.id" class="mx-auto">
      <v-card-title>{{s.headline.main}}</v-card-title>
<v-list-item>
        <v-list-item-content>Date: {{s.pub_date | formatDate}}</v-list-item-content>
      </v-list-item>
      <v-list-item>
        <v-list-item-content>
          <a :href="s.web_url">Link</a>
        </v-list-item-content>
      </v-list-item>
      <v-list-item v-if="s.byline.original">
        <v-list-item-content>{{s.byline.original}}</v-list-item-content>
      </v-list-item>
      <v-list-item>
        <v-list-item-content>{{s.lead_paragraph}}</v-list-item-content>
      </v-list-item>
      <v-list-item>
        <v-list-item-content>{{s.snippet}}</v-list-item-content>
      </v-list-item>
    </v-card>
  </v-container>
</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;
}
</style>
```

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

We return `this.$store.state.searchResults` in a function in the `computed` property in our app so 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:

```javascript
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}`);
        }
    }
}
```

We return the promises for HTTP requests to get articles in each function. In the `searchArticles` function, we message the object that we pass in into a query string that 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:

```javascript
Object.keys(params).forEach(key => {
  if (!params[key]) {
     delete params[key];
  }
})
```

In `Home.vue`, we put:

```javascript
<template>
  <div>
    <div class="text-center" id="header">
      <h1>{{selectedSection | capitalize}}</h1>
      <v-spacer></v-spacer>
      <v-menu offset-y>
        <template v-slot:activator="{ on }">
          <v-btn color="primary" dark v-on="on">Sections</v-btn>
        </template>
        <v-list>
          <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)">
            <v-list-item-title>{{ s | capitalize}}</v-list-item-title>
          </v-list-item>
        </v-list>
      </v-menu>
      <v-spacer></v-spacer>
      <v-spacer></v-spacer>
    </div>
    <v-spacer></v-spacer>
    <v-card v-for="a in articles" :key="a.id" class="mx-auto">
      <v-card-title>{{a.title}}</v-card-title>
      <v-card-text>
        <v-list-item>
          <v-list-item-content>Date: {{a.published_date | formatDate}}</v-list-item-content>
        </v-list-item>
        <v-list-item>
          <v-list-item-content>
            <a :href="a.url">Link</a>
          </v-list-item-content>
        </v-list-item>
        <v-list-item v-if="a.byline">
          <v-list-item-content>{{a.byline}}</v-list-item-content>
        </v-list-item>
        <v-list-item>
          <v-list-item-content>{{a.abstract}}</v-list-item-content>
        </v-list-item>
        <v-list-item>
          <v-list-item-content>
            <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"
            />
          </v-list-item-content>
        </v-list-item>
      </v-card-text>
    </v-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;
}
.md-card {
  width: 95vw;
  margin: 0 auto;
}
#header {
  margin-bottom: 10px;
}
</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:

```html
<v-menu offset-y>
   <template v-slot:activator="{ on }">
     <v-btn color="primary" dark v-on="on">Sections</v-btn>
   </template>
   <v-list>
      <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)">
        <v-list-item-title>{{ s | capitalize}}</v-list-item-title>
      </v-list-item>
   </v-list>
</v-menu>
```

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`, `await`, and `async` is 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.

Note that the Vuetify library uses the slots of the features of Vue.js extensively. The elements with nesting, like the `v-slot` prop, are in:

```html
<v-menu offset-y>
  <template v-slot:activator="{ on }">
     <v-btn color="primary" dark v-on="on">Sections</v-btn>
  </template>
  <v-list>
    <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)">
       <v-list-item-title>{{ s | capitalize}}</v-list-item-title>
     </v-list-item>
  </v-list>
</v-menu>
```

See the [Vue.js guide](https://vuejs.org/v2/guide/components-slots.html) for details.

In `Search.vue`, we put:

```javascript
<template>
  <div>
    <form>
      <v-text-field
        v-model="searchData.keyword"
        v-validate="'required'"
        :error-messages="errors.collect('keyword')"
        label="Keyword"
        data-vv-name="keyword"
        required
      ></v-text-field>
      <v-menu
        ref="menu"
        v-model="toggleBeginDate"
        :close-on-content-click="false"
        transition="scale-transition"
        offset-y
        full-width
        min-width="290px"
      >
        <template v-slot:activator="{ on }">
          <v-text-field
            v-model="searchData.beginDate"
            label="Begin Date"
            prepend-icon="event"
            readonly
            v-on="on"
          ></v-text-field>
        </template>
        <v-date-picker
          v-model="searchData.beginDate"
          no-title
          scrollable
          :max="new Date().toISOString()"
        >
          <v-spacer></v-spacer>
          <v-btn text color="primary" @click="toggleBeginDate = false">Cancel</v-btn>
          <v-btn
            text
            color="primary"
            @click="$refs.menu.save(searchData.beginDate); toggleBeginDate = false"
          >OK</v-btn>
        </v-date-picker>
      </v-menu>
      <v-menu
        ref="menu"
        v-model="toggleEndDate"
        :close-on-content-click="false"
        transition="scale-transition"
        offset-y
        full-width
        min-width="290px"
      >
        <template v-slot:activator="{ on }">
          <v-text-field
            v-model="searchData.endDate"
            label="End Date"
            prepend-icon="event"
            readonly
            v-on="on"
          ></v-text-field>
        </template>
        <v-date-picker
          v-model="searchData.endDate"
          no-title
          scrollable
          :max="new Date().toISOString()"
        >
          <v-spacer></v-spacer>
          <v-btn text color="primary" @click="toggleEndDate = false">Cancel</v-btn>
          <v-btn
            text
            color="primary"
            @click="$refs.menu.save(searchData.endDate); toggleEndDate = false"
          >OK</v-btn>
        </v-date-picker>
      </v-menu>
      <v-select
        v-model="searchData.sort"
        :items="sortChoices"
        label="Sort By"
        data-vv-name="sort"
        item-value="value"
        item-text="name"
      >
        <template slot="selection" slot-scope="{ item }">{{ item.name }}</template>
        <template slot="item" slot-scope="{ item }">{{ item.name }}</template>
      </v-select>
      <v-btn class="mr-4" type="submit" @click="search">Search</v-btn>
    </form>
    <SearchResults />
  </div>
</template>
<script>
import { nytMixin } from "../mixins/nytMixin";
import SearchResults from "@/components/SearchResults.vue";
import * as moment from "moment";
import { capitalize } from "@/filters/capitalize";
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();
      },
      sortChoices: [
        {
          value: "newest",
          name: "Newest"
        },
        {
          value: "oldest",
          name: "Oldest"
        },
        {
          value: "relevance",
          name: "Relevance"
        }
      ],
      toggleBeginDate: false,
      toggleEndDate: false
    };
  },
  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:

```html
<v-text-field
  v-model="searchData.keyword"
  v-validate="'required'"
  :error-messages="errors.collect('keyword')"
  label="Keyword"
  data-vv-name="keyword"
  required
></v-text-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:

```javascript
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`:

```javascript
<template>
  <v-app>
    <v-navigation-drawer v-model="drawer" app>
      <v-list nav dense>
        <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4">
          <v-list-item>
            <v-list-item-title>New Yourk Times Vuetify App</v-list-item-title>
          </v-list-item>
<v-list-item>
            <v-list-item-title>
              <router-link to="/">Home</router-link>
            </v-list-item-title>
          </v-list-item>
<v-list-item>
            <v-list-item-title>
              <router-link to="/search">Search</router-link>
            </v-list-item-title>
          </v-list-item>
        </v-list-item-group>
      </v-list>
    </v-navigation-drawer>
<v-app-bar app>
      <v-toolbar-title class="headline text-uppercase">
        <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
        <span>New York Times Vuetify App</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
    </v-app-bar>
<v-content>
      <v-container fluid>
        <router-view />
      </v-container>
    </v-content>
  </v-app>
</template>
<script>
export default {
  name: "app",
  data: () => {
    return {
      showNavigation: false,
      drawer: false,
      group: null
    };
  }
};
</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.