lobo_tuerto's notes

§ Opinions

In the time I have been delving into Vue.js I’ve come to really appreciate the framework, and its surrounding libraries.

This opinionated guide details the steps I take to start with a solid foundation for a new Vue.js project.

What particular opinions are you talking about? —you say

Glad you asked, well, for starters:

§ CLI tools

  • Package manager: Yarn — Fast, reliable, and secure dependency management.
  • Project generation tool: vue-cli — CLI for rapid Vue.js development.

§ UI

  • UI component framework: Vuetify — Material Design component framework.
  • Material icons library: Material Design Icons — 4000+ Material Design Icons from the Community.
  • Validation library: Vuelidate — Simple, lightweight model-based validation for Vue.js.
  • Data visualization library: D3 — A JavaScript library for visualizing data using web standards.

§ Utility libraries

  • HTTP client: Axios — Promise based HTTP client for the browser and Node.js.
  • Utility library: Lodash — A modern JavaScript utility library.
    Here is an alternative: Ramda.
  • State management pattern + library: Vuex — Centralized state management for Vue.js.
  • Date library: date-fns — Modern JavaScript date utility library.
    Here are some alternatives: Moment.js, Luxon, Day.js, you don’t need momentjs.

§ Template and preprocessor languages

  • HTML template engine: Pug — A robust, elegant, feature rich template engine for Node.js.
  • CSS preprocessor language: Sass — Sass is the most mature, stable, and powerful professional grade CSS extension language in the world.

§ Misc

  • Filters: vue2-filters — A collection of standard filters for Vue.js.
  • i18n: vue-i18n — Internationalization plugin for Vue.js.

I have found that these tools and libraries are performant, intuitive and very easy to work with.

§ Browser developer tools

Last, but definitely not least:

I had a similar stack for Angular that included Angular Material plus some custom component primitives for rendering dynamic forms, data tables and other stuff.

I was really fond of the dynamic forms implementation I had made, as it allowed other developers to declare highly configurable and dynamic forms using a simple JSON specification.

The generated forms integrated well with our Rails JSON API backend.

I intend to write a tutorial about dynamic forms with Vue.js and Vuetify, but I digress…

§ Setting up a new Vue.js app

Here we’ll see how to setup a newly created app with vue-cli so it’ll be ready for us to start hacking on it right away.

§ Prerequisites

Install Node.js, Yarn and vue-cli

§ Generate a new project

vue create my-vuejs-project

My usual preset looks like this:

Vue CLI v4.0.5

? Please pick a preset:
Manually select features

? Check the features needed for your project:
Babel, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E

? Use history mode for router?
(Requires proper server setup for index fallback in production):
No

? Pick a CSS pre-processor
(PostCSS, Autoprefixer and CSS Modules are supported by default):
Sass/SCSS (with dart-sass)

? Pick a linter / formatter config:
Standard

? Pick additional lint features:
(Press <space> to select, <a> to toggle all, <i> to invert selection):
Lint on save

? Pick a unit testing solution:
Jest

? Pick a E2E testing solution:
Cypress

? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?
In dedicated config files

? Save this as a preset for future projects?
No

§ Install project dependencies

Let’s use the new vue add command to install Vuetify in our brand new project.

vue add vuetify

You’ll see something along the lines of:

📦  Installing vue-cli-plugin-vuetify...
✔  Successfully installed plugin: vue-cli-plugin-vuetify

? Choose a preset:
Configure (advanced)

? Use a pre-made template?
(will replace App.vue and HelloWorld.vue):
Yes

? Use custom theme?
Yes

? Use custom properties (CSS variables)?
Yes

? Select icon font:
Material Design Icons

? Use fonts as a dependency (for Electron or offline)?
No

? Use a-la-carte components?
Yes

? Select locale:
English

🚀  Invoking generator for vue-cli-plugin-vuetify...
📦  Installing additional dependencies...

The following files have been auto-fixed:

  src/App.vue
  src/components/HelloWorld.vue
  src/main.js
  src/plugins/vuetify.js
  vue.config.js

 DONE  All lint errors auto-fixed.

⚓  Running completion hooks...

✔  Successfully invoked generator for plugin: vue-cli-plugin-vuetify
   The following files have been updated / added:

     src/assets/logo.svg
     src/plugins/vuetify.js
     vue.config.js
     package.json
     public/index.html
     src/App.vue
     src/components/HelloWorld.vue
     src/main.js
     yarn.lock

   You should review these changes with git diff and commit them.

Let’s add some other project dependencies —these are the ones I always end up installing at one point or another. Customize at your leisure:

yarn add axios d3 date-fns lodash vue-i18n vue2-filters vuelidate

I like having the power of Sass at my disposal when writing CSS rules.
Also, I like to write my templates using Pug for simplified views.

Using --dev with yarn add will add those dependencies to the devDependencies section in your package.json file:

yarn add --dev pug pug-plain-loader

§ Initial app configuration and setup

Adjust ESLint rules

I like using stricter ESLint rules for my Vue.js code:

--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,12 +4,13 @@ module.exports = {
     node: true
   },
   'extends': [
-    'plugin:vue/essential',
+    'plugin:vue/recommended',
     '@vue/standard'
   ],
   rules: {
     'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
-    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-multiple-empty-lines': [2, { max: 2 }]
   },
   parserOptions: {
     parser: 'babel-eslint'

That plugin:vue/recommended is in accord to the Official Vue.js style guide Priority C recommended section.

The reason for the no-multiple-empty-lines change, is that I usually leave two consecutive blank lines between some elements inside my .js and .vue files.

For example, for .js files: between import sections and the code that follows.
For .vue files: between <template>, <script> and <style> sections.

Take this opportunity to run the linter and fix anything that pops up:

yarn lint

If, when trying to add the latest changes to Git you’re greeted by:

fatal: CRLF would be replaced by LF in
src/plugins/vuetify.js.

Fix it by opening that file in VSCode and change the End of Line Sequence from CRLF to LF —you’ll find the control for that near the bottom right corner.

Headless e2e tests

To run your e2e tests in headless mode —very useful for CI / CD pipelines— then make this change:

--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
     "test:unit": "vue-cli-service test:unit",
-    "test:e2e": "vue-cli-service test:e2e",
+    "test:e2e": "vue-cli-service test:e2e --headless",
     "lint": "vue-cli-service lint"
   },
   "dependencies": {

Setup i18n

To setup Vue I18n, add a src/plugins/vue-i18n.js file:

import Vue from 'vue'
import VueI18n from 'vue-i18n'


Vue.use(VueI18n)

const messages = {
  de: {
    message: {
      hello: 'hallo welt'
    }
  },
  en: {
    message: {
      hello: 'hello world'
    }
  },
  es: {
    message: {
      hello: 'hola mundo'
    }
  }
}

export default new VueI18n({
  fallbackLocale: 'en',
  locale: 'en',
  messages
})

Then on src/main.js you need to import this file, and then pass a reference keyed as i18n when instantiating a Vue instance:

// ...
import i18n from './plugins/vue-i18n'
// ...

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

To use translations just use the $t function in a view like this:

<template lang="pug">
.headline {{ $t("message.hello") }}
</template>

Setup common filters

To setup vue2-filters, add a src/plugins/vue2-filters.js file:

import Vue from 'vue'
import Vue2Filters from 'vue2-filters'

Vue.use(Vue2Filters)

Then, import it into your src/main.js:

// ...
import './plugins/vue2-filters'
// ...

Setup form validation

To setup Vuelidate, add a src/plugins/vuelidate.js file:

import Vue from 'vue'
import Vuelidate from 'vuelidate'

Vue.use(Vuelidate)

Then, import it into your src/main.js:

// ...
import './plugins/vuelidate'
// ...

Show current app version and locale in the browser console

Finally, it’s good to be able to see somewhere in the browser console the version set in package.json, along with the locale set for i18n, so I usually add this to my src/main.js:

// ...
import i18n from './plugins/vue-i18n'
import { version } from '../package.json'


/* eslint-disable no-console */
console.log(`App version: ${version}`)
console.log(`App locale: ${i18n.locale}`)
/* eslint-enable no-console */
// ...

§ Basic Vuetify templates

To see Vuetify in action with Pug, change your src/App.vue file to:

<template lang="pug">
v-app
  v-app-bar(app)
    v-toolbar-title.headline.text-uppercase
      span.mr-2 Material
      span.font-weight-light Design

    v-spacer

    v-btn(
      text
      href="https://github.com/vuetifyjs/vuetify/releases/latest"
      rel="noopener"
      target="_blank"
    )
      span Latest Release
      v-icon(right) mdi-open-in-new

  v-content
    router-view
</template>

And change src/views/Home.vue to:

<template lang="pug">
v-container
  hello-world(msg="Welcome to Vuetify")
</template>


<script>
import HelloWorld from '@/components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}
</script>

Finally, let’s convert src/components/HelloWorld.vue to Pug:

<template lang="pug">
v-layout(text-center wrap)
  v-flex(xs12)
    v-img(
      :src="require('../assets/logo.svg')"
      class="my-3"
      contain
      height="200"
    )

  v-flex(mb-4)
    h1.mb-6
      .headline {{ $t("message.hello") }}
      .display-3 {{ msg }}

    p.subheading.font-weight-regular
      | For help and collaboration with other Vuetify developers,
      br
      | please join our online
      |
      a(
        href="https://community.vuetifyjs.com"
        rel="noopener"
        target="_blank"
      ) Discord Community

  v-flex(mb-5 xs12)
    h2.headline.mb-3 What's next?
    v-layout(justify-center)
      a(
        v-for="(next, i) in whatsNext"
        :key="i"
        :href="next.href"
        class="subheading mx-3"
        rel="noopener"
        target="_blank"
      ) {{ next.text }}

  v-flex(xs12 mb-5)
    h2.headline.mb-3 Important Links
    v-layout(justify-center)
      a(
        v-for="(link, i) in importantLinks"
        :key="i"
        :href="link.href"
        class="subheading mx-3"
        rel="noopener"
        target="_blank"
      ) {{ link.text }}

  v-flex(xs12 mb-5)
    h2.headline.mb-3 Ecosystem
    v-layout(justify-center)
      a(
        v-for="(eco, i) in ecosystem"
        :key="i"
        :href="eco.href"
        class="subheading mx-3"
        rel="noopener"
        target="_blank"
      ) {{ eco.text }}
</template>

<script>
export default {
  props: {
    msg: {
      required: true,
      type: String
    }
  },
  data: () => ({
    ecosystem: [
      {
        text: 'vuetify-loader',
        href: 'https://github.com/vuetifyjs/vuetify-loader'
      },
      {
        text: 'github',
        href: 'https://github.com/vuetifyjs/vuetify'
      },
      {
        text: 'awesome-vuetify',
        href: 'https://github.com/vuetifyjs/awesome-vuetify'
      }
    ],
    importantLinks: [
      {
        text: 'Documentation',
        href: 'https://vuetifyjs.com'
      },
      {
        text: 'Chat',
        href: 'https://community.vuetifyjs.com'
      },
      {
        text: 'Made with Vuetify',
        href: 'https://madewithvuejs.com/vuetify'
      },
      {
        text: 'Twitter',
        href: 'https://twitter.com/vuetifyjs'
      },
      {
        text: 'Articles',
        href: 'https://medium.com/vuetify'
      }
    ],
    whatsNext: [
      {
        text: 'Explore components',
        href: 'https://vuetifyjs.com/components/api-explorer'
      },
      {
        text: 'Select a layout',
        href: 'https://vuetifyjs.com/layout/pre-defined'
      },
      {
        text: 'Frequently Asked Questions',
        href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
      }
    ]
  })
}
</script>

Just for completion, src/views/About.vue should look like this:

<template lang="pug">
v-container
  h1 This is an about page
</template>

A note on Pug and the new slot syntax

If you are using Jest, in conjunction with Pug and the new slot syntax:

v-menu
  template(#activator="{ on }")
    v-btn(color="primary" v-on="on")
  //- ...

template(#activator="{ on }") is equivalent to:
template(v-slot:activator="{ on }") but in shorthand syntax.

You might get an error that looks like this:

ERROR: Unexpected character '#' (...)
STACK: SyntaxError: Unexpected character '#' (...)

You can read more about it here.

To fix it, just add this to jest.config.js:

module.exports = {
  // ...
  globals: {
    'vue-jest': {
      pug: {
        doctype: 'html'
      }
    }
  }
}

§ Basic routing

Nothing to change here.
Your src/router.js should look like:

import Vue from 'vue'
import Router from 'vue-router'

import Home from './views/Home.vue'


Vue.use(Router)


export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    }
  ]
})

§ Typography

The Material Design guidelines for typography, state that Roboto and Noto are the standard typefaces to use.

When you installed Vuetify through vue add vuetify it modified your public/index.html file and added these lines:

<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
>

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
>

§ Launch your app

Start your project with:

yarn serve

And visit: http://localhost:8080.

§ Fix the tests

First, let’s make some fixes.
In tests/e2e/specs/test.js change this:

 describe('My First Test', () => {
   it('Visits the app root url', () => {
     cy.visit('/')
-    cy.contains('h1', 'Welcome to Your Vue.js App')
+    cy.contains('h1', 'Welcome to Vuetify')
   })
 })

Then, in tests/unit/example.spec.js:

-import { shallowMount } from '@vue/test-utils'
+import { createLocalVue, shallowMount } from '@vue/test-utils'
+import VueI18n from 'vue-i18n'
+import Vue2Filters from 'vue2-filters'
+import Vuelidate from 'vuelidate'
+import Vuetify from 'vuetify'
 import HelloWorld from '@/components/HelloWorld.vue'
 
+const localVue = createLocalVue()
+localVue.use(VueI18n)
+localVue.use(Vue2Filters)
+localVue.use(Vuelidate)
+localVue.use(Vuetify)
+
 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
-      propsData: { msg }
+      propsData: { msg },
+      localVue,
+      i18n: new VueI18n(),
+      vuetify: new Vuetify()
     })
     expect(wrapper.text()).toMatch(msg)
   })

To run the unit tests:

yarn test:unit

To run the end-to-end tests:

yarn test:e2e

§ Build your app

To build your app just type:

yarn build

The output of that command will be in the dist directory.

If you want to quickly try out your production version locally then type:

cd dist/
python -m http.server

Now visit http://localhost:8000.

§ Deploying to a subdirectory

When building your app for production the compiler assumes that your app is going to be deployed to /.

If that is not the case and you are deploying to /some-other-dir then you need to create a new —or open the existing— vue.config.js file in your app’s root directory and add a publicPath key like this:

module.exports = {
  // ...
  publicPath: process.env.NODE_ENV === 'production'
    ? '/some-other-dir/'
    : '/'
}

You can find more info about vue.config.js here.

That’s it!
Have a good one. :)

§ Bonus section

§ Color theme tool

If you are looking for a Material Design color browser, a tool that’ll let you mix and match colors, then check this Vuetify color theme builder out!

§ Import Lodash the right way

If you want to avoid importing the whole library into your code to decrease your app size, then you should import only the functions you use.

For example, if you’ll be using only the pick and cloneDeep functions in a component, instead of this:

import _ from 'lodash'

// ...

const picked = _.pick(obj, ['attr1', 'attr2', 'attr2'])
const cloned = _.cloneDeep(someOtherObj)

You should do this:

import cloneDeep from 'lodash/cloneDeep'
import pick from 'lodash/pick'

// ...

const picked = pick(obj, ['attr1', 'attr2', 'attr2'])
const cloned = cloneDeep(someOtherObj)

It’s a bit tedious, but if you are trying to save some KBs, then that’s the right thing to do.

§ Useful links

FINIS
Got comments or feedback?
Follow me on twitter
v1.1.2