Vue.js

/vjuː/

Vue.js

  • Framework(/Library) to create interfaces (views)
  • Oriented components
  • Easy to use, straightforward syntax
  • Use declarative rendering

Who's using VueJS

(quick) Setup


          
      

{{ message }}

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello hostel!'
  }
})
      

Declarative

Tell WHAT you render instead of HOW

simple

Declarative rendering

declarative rendering

DevTools

declarative rendering

Virtual DOM

virtual DOM

Virtual DOM

virtual DOM

Virtual DOM

virtual DOM

Virtual DOM

virtual DOM

Virtual DOM

virtual DOM

Virtual DOM: Example

Counter: {{ counter }}

(open Chrome DevTools)

The Vue Syntax

Conditional rendering: v-if


{{ hostel.name }}

Book now

var app = new Vue({
  el: '#app',
  data: {
    hostel: {
      name: 'Happy Hostel',
      availability: true
    }
  }
})
      

Conditional rendering: v-else


{{ hostel.name }}

Book now Too late!

var app = new Vue({
  el: '#app',
  data: {
    hostel: {
      name: 'Happy Hostel',
      availability: true
    }
  }
})
      

Conditional rendering: v-show


{{ hostel.name }}

Book now

var app = new Vue({
  el: '#app',
  data: {
    hostel: {
      name: 'Happy Hostel',
      availability: true,
      isActive: true
    }
  }
})
      

Loops: v-for


{{ hostel.name }}


var app = new Vue({
  el: '#app',
  data: {
    hostels: [
      { name: 'Happy Hostel' },
      { name: 'Awesome Hostel' },
      { name: 'Another Hostel' }
    ]
  }
})
      

Form Input Bindings: v-model



Message: {{ message }}

Example:

Message: {{ message }}

Computed properties


const vueModel = new Vue({
  el: '#app',
  data: {
    message: 'Long live v-model!'
  },
  computed: {
    messageUpperCase () {
      return this.message.toUpperCase()
    }
  }
})
      

Workshop

Part 1: Playing with Vue Syntax

Wait... I've to put everything on the same vue?

Nope.

A world of components

What is a component?

components

Communication between components

our first component


Vue.component('hostel-detail', {
  template: `
I'm a hostel!
` }) new Vue({ el: '#app' })

Passing properties



Vue.component('hostel-detail', {
  template: `
hostel: {{ name }}
`, props: { name: String } })

shortcut: :name="'Cool hostel'"

Props validation


Vue.component('hostel-price', {
  template: `
Price: {{ price }}
`, props: { price: { type: Number, required: true, validator (value) { return value > 0 }, default: 100 } } })

Emit events


    
new Vue({
  el: '#emitEvents',
  data: {
    hostel: {
      name: 'Cool hostel'
    }
  },
  methods: {
    addToCart (hostel) {
      this.$emit('selecthostel', hostel)
    }
  }
})
    

Example

Events

Listening events


Cart: {{ cartToString }}

    
Vue.component('hostel-list-item', {
  template: '',
  props: ['hostel'],
  methods: {
    click () {
      this.$emit('select', this.hostel)
    }
  }
})

var hostelList = new Vue({
  el: '#hostel-list-demo',
  name: 'hostelList',
  data: {
    properties: ['hostelA', 'hostelB'],
    cart: []
  },
  computed: {
    cartToString () {
      return this.cart.length ? this.cart.join(' ') : '(empty)'
    }
  },
  methods: {
    add (hostel) {
      this.cart.push(hostel)
    }
  }
})
      

Listening events - Example

Cart: {{ cartToString }}

Watchers



Vue.JS is...{{ appreciation }}


var vueWatcher = new Vue({
  el: '#vue-watcher',
  data: {
    appreciation: ''
  },
  watch: {
    appreciation (newappreciation) {
      if (newappreciation === 'bad') {
        this.appreciation = 'awesome'
      }
    }
  }
})
      

Example:

Vue.JS is...{{ appreciation }}

Lifecycle

Example with mounted()


{{ starship.name }}

{{ starship.model }}


const lifecycle = new Vue({
  el: '#lifecycle',
  data: {
    starship: {}
  },
  mounted () {
    fetch('https://swapi.co/api/starships/12/')
    .then(response => response.json())
    .then(data => {
      this.starship = data
    })
  }
})
      

Workshop

Part 2: Components

The .vue File

vue file

What's the magic behind

vue file

Benefit: Hot reload ❤️

hot reload > live reload

Separation of concerns

Separation of concerns Separation of concerns

Getting started!


npm install -g vue-cli
vue init webpack my-project
cd my-project
npm install
npm run dev 
      

Workshop

Part 3: .vue files

Vue ecosystem: HTTP

Let's find something cool

Please DON'T!
...and forget your jQuery habits!

Think JavaScript!

Vue ecosystem: HTTP

axios/axios

File architecture


├── src/
│   ├── main.js
│   ├── App.vue
│   └── api/
│       ├── bookings.js
│       ├── hostel.js
│       └── user.js
│   └── components/
│       └── ...
│   └── .../
│       └── ...
      

Example (1/2)


// src/api/hostel.js
import axios from 'axios'

export function findAll (limit = 25) {
  return axios.get('hostels/?limit=' + limit)
              .then(response => response.data)
}
      

Example (2/2)


import * as hostelApi from '../api/hostels'

export default {
  data () {
    return {
      hostels: []
    }
  },
  created () {
    hostelApi.findAll().then((data) => {
      this.hostels = data
    })
  }
}
      

Workshop

Part 4: HTTP&Axios

Vue ecosystem: Routing

vue-router

Single Page Application

Vue-Router

  • Simple to use
  • Allow nested routes
  • Window.history
  • Support SSR - Server Side Rendering
  • ...

Installation

Example: Main template


Go to my account Go to booking

Example: JS


const Account = { template: '
account
' } const Bookings = { template: '
bookings
' } const routes = [ { path: '/account', component: Account }, { path: '/bookings', component: Bookings } ] const router = new VueRouter({ routes: routes }) const app = new Vue({ router }).$mount('#app')

Workshop

Part 5: Routing

Vue ecosystem: State Management

vuex

Why you need a state managemet library?

Components need to share the same state

What is Vuex?

  • Inspired by Flux, Redux and The Elm Architecture
  • Provide a centralized store
  • Easy to test!

Idea behing Vuex

Core concepts

  • State: An object containing the data
    Ex: a list of hostels
  • Getters: A state accessor
    Ex: getHostels(), getHostelsFromBerlin()...
  • Mutations: perform modifications of data in the state. Must be syncrhonous!
    Ex: addRoomToSelection()
  • Actions: similar to mutations. Instead of mutating the state, actions commit mutations. Can also be asynchronous
    Ex: roomApi.fetch(), checkout()

Modules

File architecture


├── src/
│   ├── main.js
│   ├── App.vue
│   └── api/
│       └── ...
│   └── components/
│       └── ...
│   └── store/
│       └── modules/ 
│           └── ...
│       ├── index.js
│       └── mutation-types.js
      

Example - store side


// store/modules/hostel.js
import * as api from '../api'
import * as types from '../mutation-types'

export default {
  state: {
    hostels: []
  },
  getters = {
    allHostels: state => state.hostels,
    highRatedHostels: state => state.hostels.filter(h => h.score >= 7)
  },
  mutations: {
    [types.RECEIVE_HOSTEL] (state, hostels) {
      state.hostels = hostels
    }
  },
  actions: {
    loadCountryHostels ({ commit }, country) {
      commit(types.LOAD_HOSTELS_BY_COUNTRY, { country })
      api.getHostels(country).then(data => {
        commit(types.RECEIVE_HOSTELS, { data })
      })
    }
  }
}
      

Example - Component side


// MyComponent.vue
import { mapGetters, mapActions } from 'vuex'

export default {
  computed: mapGetters({
    hostels: 'allHostels'
  }),
  methods: mapActions([
    'loadCountryHostels'
  ]),
  created () {
    this.$store.dispatch('loadCountryHostels', 'Berlin')
  }
}
      

Why actions commit mutations?

Why actions commit mutations?


// store/actions.js
export const addToSelection = ({ commit }, hostel) => {
  commit(types.ADD_TO_SELECTION, { hostel })
}
      

// store/modules/hostelList.js
[types.ADD_TO_SELECTION] (state, { hostel }) {
  state.hostelList.slice(state.hostelList.indexOf(hostel), 1)
}
      

// store/modules/hostelSelection.js
[types.ADD_TO_SELECTION] (state, { hostel }) {
  state.selection.push(hostel)
}
      

👍 Browsers devtools

  • Import/Export state
  • Time travel

Workshop

Part 6: State management with Vuex

Vue ecosystem: Unit Testing

Jest

vue/vue-test-utils

Why Jest?

  • Watch mode
  • Snapshot testing
  • Good code coverage 😥

Watch mode

Run with pattern

Installation

File Architecture


├── test/
│   ├── __mocks__
│   ├── __snapshots__
│   └── api/
│       └── hostelApi.spec.js
│   └── components/
│       └── __snapshots__/
│           └── Selection.spec.js.snap
│       └── MyComponent.spec.js
│   └── store/
│       └── modules/ 
│           └── myModule.spec.js
      

Assertions List


expect(foo).toBe()        // use ===
expect(foo).toEqual()     // object have same values (not necessary same reference)
expect(true).toBeTruthy()
expect(false).toBeFalsy()
expect('banana').toContain('ban')
      

list of assertions

Our first test

Now, let's meet the real world!

Snapshot testing - example


import { mount, shallow } from 'vue-test-utils'
import MyComponent from '@/components/MyComponent.vue'
import store from '@/store'

describe('Shop.vue', () => {
  it('test initial rendering', () => {
    const wrapper = mount(MyComponent, { store })
    const template = wrapper.html()
    expect(template).toMatchSnapshot()
  })
})
      

Snapshot testing - flow

Snapshot testing - flow

Snapshot testing - flow

Testing component interactions (1/2)


it('should do something on close', () => {
  const wrapper = mount(MyComponent, { store })
  const button = wrapper.findAll('div.btn')
  button.trigger('click')
  expect(/** ... */).toBe('true')
});
      

Testing component interactions 🚧 TODO 🚧


it('should do something on close', () => {
  // 🚧 TODO 🚧 
});
      

Testing Axios calls - Mock part


// tests/unit/specs/__mocks__/axios.js
import userBookingResponse from '../api/userBooking.response.json'

class Axios {
  get (url) {
    if (url === '/users/1/bookings'){
      const data = userBookingResponse
    }
    const response = { status: 200, statusText: 'OK', data: data }
    return Promise.resolve(response)
  }
}

export default new Axios()
      

Testing Axios calls - mock using dynamic imports


// tests/unit/specs/__mocks__/axios.js
class Axios {
  get (fullUrl) {
    const url = fullUrl.replace(process.env.API_URL, '')

    switch (url) {
      case 'users/1': return Promise.resolve({ data: import('../api/user.1.response.json') })
      case 'hostels/?page=3': return Promise.resolve({ data: import('../api/hostels.3.response.json') })
      // ...
    }
  }
}

export default new Axios()
      

Testing Axios calls - API part


// tests/unit/specs/api/userBookingApi.spec.js
import userBookingResponse from './userBooking.response.json'
import * as userBookingApi from '@/api/userBookingApi'

describe('userBooking HTTP', () => {
  it('getUserBooking() should return the response data', () => {
    expect.assertions(1)
    return expect(userBookingApi.getUserBooking()).resolves.toEqual(userBookingResponse)
  })
})
      

Vuex - Test Getters


it('return all hostels', () => {
  const state = {
    hostels: [{ name: 'hostelA'}, { name: 'hostelB'}]
  }

  const result = hostelStore.getters.allHostels(state)
  expect(result).toEqual({ name: 'hostelA'}, { name: 'hostelB'})
})
      

Vuex - Test actions (1/2)

Example


// tests/unit/custom/testAction.js
export const testAction = (action, payload, state, expectedMutations, done) => {
  let count = 0

  // mock commit
  const commit = (type, payload) => {
    const mutation = expectedMutations[count]

    try {
      expect(mutation.type).toEqual(type)
      if (payload) {
        expect(mutation.payload).toEqual(payload)
      }
    } catch (error) {
      done(error)
    }

    count++
    if (count >= expectedMutations.length) {
      done()
    }
  }

  // call the action with mocked store and arguments
  action({ commit, state }, payload)

  // check if no mutations should have been dispatched
  if (expectedMutations.length === 0) {
    expect(count).toEqual(0)
    done()
  }
}
      

Vuex - Test actions (2/2)


testAction(hostel.actions.loadCountryHostels, 'Spain', {}, [
  //expected commits
  { type: 'LOAD_HOSTELS_BY_COUNTRY', payload: { country: 'Spain' } },
  { type: 'RECEIVE_HOSTELS', payload: { data: expectedJSONHostels } }
], done)
      

Workshop

Part 7: Testing with Jest

Vue ecosystem: SSR

Vue-SSR

Client Side Rendering (CSR)

Server Side Rendering (SSR)

Reading

The Benefits of Server Side Rendering Over Client Side Rendering
- by Alex Grigoryan

Environment Agnostic SSR: https://gist.github.com/yyx990803/9bdff05e5468a60ced06c29c39114c6b

Other

Sync Vuex & vue-router

vuejs/vuex-router-sync

Style Guide

vuejs.org/v2/style-guide/

vuejs/eslint-plugin-vue

Resources

vuejs/awesome-vue

Smart vs. Dumb component

Smart vs. Dumb component

Dumb components:

  • Have no app dependencies
  • Receive only props, providing data and callbacks

Smart components (aka containers):

  • Provide application data, do data fetching
  • Interact with state
smart and dumb component
smart and dumb component
smart and dumb component