VUE.JS FROM SCRATCH

Kamil Szubrycht

@kamilszubrycht

RRUG#12

Agenda

  • vue-cli
  • components
  • vue-router
  • Vuex
  • e2e & unit tests
  • devtools

Vue.js + Rails

$ rails new myapp --webpack=vue
$ npm install -g vue-cli
$ vue init webpack myapp
  • webpack
  • webpack-simple
  • simple

vue-cli

? Project name (myapp)
? Project description (A Vue.js project)
? Author (Kamil Szubrycht <kamil.szubrycht@gmail.com>) 
? Vue build (Use arrow keys)
  + Runtime + Compiler: recommended for most users 
  - Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML)
    are ONLY allowed in .vue files - render functions are required elsewhere
? Install vue-router? (Y/n)
? Use ESLint to lint your code? (Y/n) 
? Pick an ESLint preset (Use arrow keys)
  + Standard (https://github.com/standard/standard) 
  - Airbnb (https://github.com/airbnb/javascript) 
  - none (configure it yourself)
? Set up unit tests (Y/n) 
? Pick a test runner (Use arrow keys)
  + Jest 
  - Karma and Mocha 
  - none (configure it yourself)
? Setup e2e tests with Nightwatch? (Y/n)
? Should we run `npm install` for you after the project has been created? (recommended)
  (Use arrow keys)
  + Yes, use NPM 
  - Yes, use Yarn 
  - No, I will handle that myself

Project structure

├── README.md
├── build
├── config
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── router
├── static
└── test
    ├── e2e
    └── unit

npm commands

# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report

# run unit tests
npm run unit

# run e2e tests
npm run e2e

# run all tests
npm test

Components system

https://vuejs.org/images/components.png

Single File Components

<template></template>
<script></script>
<style></style>

Data object 

<template>
  <div>
    {{ name }}
  </div>
</template>

<script>
export default {
  data () {
    return {
      name: 'Ragnar'
    }
  }
}
</script>

Methods

<script>
export default {
  data () {
    return {
      name: 'Ragnar'
    }
  },
  methods: {
    setName (name) {
      this.name = name
    }
  }
}
</script>

Watched properies

<script>
export default {
  data () {
    return {
      firstName: 'Ragnar',
      lastName: 'Lothbrok',
      fullName: 'Ragnar Lothbrok'
    }
  },
  watch: {
    firstName (val) {
      this.fullName = `${val} ${this.lastName}`
    },
    lastName (val) {
      this.fullName = `${this.firstName} ${val}`
    }
  }
}
</script>

Computed properies

<script>
export default {
  data () {
    return {
      firstName: 'Ragnar',
      lastName: 'Lothbrok'
    }
  },
  computed: {
    fullName () {
      return `${this.firstName} ${this.lastName}`
    }
    // fullName: {
    //   get () {
    //     return `${this.firstName} ${this.lastName}`
    //   },
    //   set (newValue) {
    //     var names = newValue.split(' ')
    //     this.firstName = names[0]
    //     this.lastName = names[1]
    //   }
    // }
  }
}
</script>

Directives

<li v-for="user in users">
  {{ user.email }}
</li>
<!-- full syntax -->
<a v-on:click="doSomething"> ... </a>
<!-- shorthand -->
<a @click="doSomething"> ... </a>
<input v-model="username">
{{ username }}
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Not A/B</div>

<h1 v-show="ok">Hello!</h1>
<span v-class="
  red    : hasError,
  hidden : isHidden
"></span>
<!-- full syntax -->
<a v-bind:href="url"> ... </a>
<!-- shorthand -->
<a :href="url"> ... </a>

Custom directives

<script>
export default {
  (...)
  directives: {
    randomColor: {
      // bind: (el, binding, vnode) => { ... }
      // inserted: (el, binding, vnode) => { ... }
      // update (el, binding, vnode, oldvnode) => { ... }
      componentUpdated (el, binding, vnode, oldvnode) {
        el.style.backgroundColor = `#${Math.random().toString(16).substr(-6)}`
      }
      // unbind: (el, binding, vnode) => { ... }
    }
  }
}
</script>
Vue.directive('randomColor', {
  componentUpdated (el) {...}
})

Lifecycle hooks

<script>
export default {
  (...)
  // beforeCreate () { ... },
  // created () { ... },
  // beforeMount () { ... },
  mounted () { this.loading = false },
  // beforeUpdate () { ... },
  // updated () { ... },
  // beforeDestroy () { ... },
  // destroyed () { ... }
}
</script>

Parent-child relationship

https://vuejs.org/images/props-events.png

Passing props

<!-- parent.vue -->

<template>
  <div>
    <Child :name='name'></Child>
  </div>
</template>

<script>
import Child from '@/components/child';

export default {
  components: { Child },
  data () {
    return {
      name: 'Floki'
    }
  }
}
</script>
<!-- child.vue -->

<template>
  <div>
    {{ name }}
  </div>
</template>

<script>
export default {
  // props: ['name']
  props: {
    name: {
      type: String,
      required: true
    }
  }
}
</script>

Emitting events

<!-- parent.vue -->

<template>
  <div>
    <child @message='showMessage'></child>
  </div>
</template>

<script>
import Child from '@/components/child';

export default {
  components: { Child },
  methods: {
    showMessage (message) {
      alert(message)
    }
  }
}
</script>
<!-- child.vue -->

<template>
  <div>
    <button @click="send">Send</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      message: 'hello!'
    }
  },
  methods: {
    send () {
      this.$emit('message', this.message)
    }
  }
}
</script>

Mixins

// loading-state.js

export default {
  data () {
    return {
      isLoading: true
    }
  },
  mounted () {
    this.isLoading = false
  }
}
<!-- Component.vue -->

<template>
  <spinner v-if="isLoading"></spinner>
</template>

<script>
  import LoadingState from './mixins/loading-state-mixin'

  export default {
    mixins: [LoadingState],
    // (...)
  }
</script>

vue-router

// router.js

import Vue from 'vue'
import VueRouter from 'vue-router'

import Items from '@/components/items.vue'
import Item from '@/components/item.vue'
import ItemDetails from '@/components/item-details.vue'

Vue.use(VueRouter)

export default new VueRouter({
  routes: [
    { path: '/', name: 'home', component: Items },
    { path: '/item/:id', name: 'item', component: Item,
      children: [
        { path: 'details', name: 'details', component: ItemDetails }
      ]
    }
  ]
})

vue-router

<!-- App.vue -->

<template>
  <div id="app">
    <h1>Items</h1>
    <div id="menu">
      <router-link to="/">
        Items
      </router-link>
      <router-link :to="{ name: 'item', params: { id: 1 }}">
        Item 1
      </router-link>
      <router-link :to="{ name: 'details', params: { id: 1 }}">
        Item 1 details
      </router-link>
    </div>
    <router-view></router-view>
  </div>
</template>

vue-router

// literal string path
router.push('home')

// object
router.push({ path: 'home' })

// named route
router.push({ name: 'user', params: { userId: 123 }})

// with query, resulting in /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

vue-router

router.beforeEach((to, from, next) => {
  // ...
  next()
})

router.afterEach((to, from, next) => {
  // ...
  next(false)
})
export default new VueRouter({
  routes: [
    { path: '/', name: 'home', component: Vikings,
      beforeEnter: (to, from, next) => {
        // ...
        next({ path: '/' })
      }
    }
})

but...

Vuex

https://vuex.vuejs.org/en/images/vuex.png

Vuex

// state.js
export default {
  items: []
}
// getters.js
export function items (state) {
  return state.items
}
export function item (state, getters) {
  (id) => { return getters.items.find(item => item.id === id) }
}
// actions.js
export function fetchItems (context) {
  Vue.axios.get('/items').then((response) => {
    context.commit('setItems', response.data)
  })
}
// mutations.js
export function setItems (state, data) {
  state.items = data
}

Vuex

// store.js

import Vue from 'vue'
import Vuex from 'vuex'

import state from './state'
import * as getters from './getters'
import * as actions from './actions'
import * as mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations
})

Vuex

// Component.vue

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapGetters(['items'])
  },
  methods: {
    ...mapActions({
      fetchData: 'fetchItems'
    })
  }
}
</script>

Unit tests

$ npm install avoriaz --save-dev
import { mount } from 'avoriaz';
import List from '@/components/List';
import Vue from 'vue';

describe('List.vue', () => {
  it('adds new item to list on click with avoriaz', () => {
    const ListComponent = mount(List);

    ListComponent.setData({
      newItem: 'brush my teeth',
    });

    const button = ListComponent.find('button')[0].dispatch('click');

    expect(ListComponent.data().listItems).to.contain('brush my teeth');
  })
})

e2e tests

module.exports = {
  'default e2e tests': function (browser) {
    const devServer = browser.globals.devServerURL

    browser
      .url(devServer)
      .waitForElementVisible('#app', 5000)
      .assert.elementPresent('.hello')
      .assert.containsText('h1', 'Welcome to Your Vue.js App')
      .assert.elementCount('img', 1)
      .end()
  }
}

Devtools

Devtools

Devtools

Links

Thanks!

VUE.JS FROM SCRATCH

By Kamil Szubrycht

VUE.JS FROM SCRATCH

  • 1,412