Claudio Bisconti
 

mail: claudio.bisconti@comm-it.it
linkedin: linkedin.com/in/claudiobisconti
github: github.com/cbspire

About me

  • Sales Account
  • Technical Leader
  • Fullstack software developer

@

What I know

PREVIOUSLY ON

Vue.js

Created by Evan You (Ex Google)

First release in February 2014

Actually used by

  • Laravel
  • Alibaba
  • Xiaomi
  • Netflix 
  • Gitlab
  • Facebook 🤫
  • and more others

Vue.js

/vjuː/  => { 'view' }

  • Progressive framework
  • for building user interfaces
  • Modern and lightweight approach
  • Components oriented
  • with declarative rendering
  • and reactivity approach
  • where models are simple Javascript object

Vue.js

<template>
    <div id="my-app">
        <header-bar :user="username"/>
        <app-loader v-if="loading" />
        </custom-content v-else />
    </div>
</template>
<script>
import { HeaderBar, AppLoader, CustomContent } from '../components'
export default {
    name: 'MyApp',
    components: { HeaderBar, AppLoader, CustomContent }
    data() {
        return {
            loading: true,
            user: {}
        }
    },
    computed: {
        username() {
            return `${this.firstname} ${this.lastname}`
        }
    },
    async mounted() {
        this.user = await fetchUser()
    }
}
</script>
<style>
    #my-app {
        width: 100vw; height: 100vh;
    }
</style>

Framework comparison

Angular React Vue jQuery
Type Framework Library Framework Library
Building SPA Y Y Y 🧐
Using in already made applications 😰 Y Y 💪
DOM Manipulation Y Y Y Y
Virtual DOM Y Y Y N
Production build size ~300K ~100K ~30K ~85K
Reactive components Y Y Y 😟
Router inside Y N N ?
HTML Templates Y N Y Y
JSX N Y Y N
Dependency Tracking Manual Manual Auto 🙈
Learning curve Fast Medium Fast Auto

U mixins

Mixins are a flexible way to distribute reusable functionalities for Vue components

https://vuejs.org/v2/guide/mixins.html

A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.

U mixins

mixin example


const themeMixin = {
    props: {
        light: {
            type: Boolean,
            default: true
        },
        dark: {
            type: Boolean,
            default: false
        }
    }
    computed: {
        computedThemeClasses() {
            return {
                ['theme-' + this.light]: this.light,
                ['theme-' + this.dark]: this.dark,
            }
        }
    }
}

export default themeMixin

U mixins

mixin use example


<template>
    <div :class="computedThemeClasses">
        ...
    </div>
</template>

<script>
import themeMixin from '../mixins';

export default {
    name: 'MyComponent',
    mixins: [themeMixin]
}

</script>

U mixins

global mixin


Vue.mixin({
  created: function () {
    var theme = this.$options.theme
    if (theme) {
        ...
    }
  }
})

new Vue({
  theme: 'dark'
})

Once you apply a mixin globally, it will affect every Vue instance created afterwards. When used properly, this can be used to inject processing logic for custom options:

U mixins

merge options


Vue.config.optionMergeStrategies.theme = (toVal, fromVal) => {
    const allowedThemes = ['default', 'light', 'dark'];

    if (!allowedThemes.includes(toVal)) {
        return 'default';
    }

    return toVal;
}

Vue router

Official router

 

Creating a Single-page Application with Vue + Vue Router is dead simple.

https://router.vuejs.org

Vue router

creating routes


import VueRouter from 'vue-router'

Vue.use(VueRouter)

import LoginPage from './pages/Login'
import DashboardPage from './pages/Dashboard'

const  UsersList = () => import('./pages/Users')
const  ProjectsList = () => import(/* webpackChunkName: "page-projects" */ './pages/Projects')

const routes = [
  { path: '/login',     component: LoginPage    },
  { path: '/dashboard', component: Dashboard    },
  { path: '/users',     component: UsersList    },
  { path: '/projects',  component: ProjectsList }
]

const router = new VueRouter({
    routes // short for `routes: routes`
})

new Vue({
    //...
    router
})

Vue router

routes

const routes = [
    {
        path: '/user/:id',
        component: UserPage,
        children: [
            {
                path: '', /* path: /users/:id: */
                component: UserHomePage,
            },
            {
                path: 'profile', /* path: /users/:id:/profile */
                component: UserProfilePage,
                name: 'user.profile',
                props: true
            },
            {
                path: 'notifications', /* path: /users/:id:/notifications */
                component: UserNotificationsPage,
                children: [
                    {
                        path: ':nid', /* path: /users/:id:/notifications/:nid */
                        component: UserNotificationPage
                    }
                ]
            }
        ]
    },
]

Vue router

<router-view></router-view>


<div id="app">
    <h1>Hello App!</h1>
    <p>
        <router-link to="/users">Users</router-link>
        <router-link to="/dashboard">Dashboard</router-link>
    </p>
    <!-- component matched by the route will render here -->
    <router-view></router-view>
</div>

Vue router

<router-link></router-link>


<router-link to="/users">Users</router-link>
Rendered as: <a href="...">Users</a>

<router-link tag="button" to="/dashboard">Dashboard</router-link>
Rendered as: <button>Dashboard</button>

<router-link :to="{ name: 'user.profile', params: { id: 1 }}">
    User #1
</router-link>

Vue router

Js usage

<template>
    <div>
        {{ $route.name }}
        {{ $route.params.id }} === {{ id }}
    </div>
</template>


<script>
export default {
    name: 'UserProfile',
    props: ['id'],
    mounted() {
        let userId = this.$route.params.id
        // or use this.id      
        // fetchUserProfileData ?
    },
    methods: {
        onNotificationTabClicked(nid) {
            this.$router.push({
                name: 'user.notification.single',
                params: { nid }
            })
        }
    }
}
</script>

Vue router

Named views

<template>
    <div id="myapp">
        <header>
            <router-view name="header"></router-view>
        </header>
        <router-view></router-view>
    </div>
</template>
const routes = [
    {
        path: '/user/:id',
        components: {
            default: UserPage,
            header: UserPageHeader
        }
    }
]


Vue router

Named views

<template>
    <div id="myapp">
        <header>
            <router-view name="header">
            </router-view>
        </header>
        <router-view></router-view>
    </div>
</template>
const routes = [
    {
        path: '/user/:id',
        components: {
            default: UserPage,
            header: UserPageHeader
        }
    }
]


Vue router

global/route navigation guards

const router = new VueRouter({ ... })

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

    to = { // https://router.vuejs.org/api/#the-route-object
        path,
        params
        query,
        hash,
        fullPath,
        matched,
        ...
    } 

    next()
})

router.beforeEnter((to, from, next) => {

    ...
    next()
})

Vue router

component navigation guards


export default {
    template: `...`,
    beforeRouteEnter (to, from, next) {
        // called before the route that renders this component is confirmed.
        // does NOT have access to `this` component instance,
        // because it has not been created yet when this guard is called!
    },
    beforeRouteUpdate (to, from, next) {
        // called when the route that renders this component has changed,
        // but this component is reused in the new route.
        // For example, for a route with dynamic params `/foo/:id`, when we
        // navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
        // will be reused, and this hook will be called when that happens.
        // has access to `this` component instance.
    },
    beforeRouteLeave (to, from, next) {
        // called when the route that renders this component is about to
        // be navigated away from.
        // has access to `this` component instance.
    }
}

Vue router

router transitions


<transition name="fade">
    <router-view></router-view>
</transition>
export default {
    watch: {
        '$route' (to, from) {
                const toDepth = to.path.split('/').length
                const fromDepth = from.path.split('/').length
                this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
          }
    }
}

<transition :name="transitionName">
    <router-view></router-view>
</transition>

dynamic router transitions

Vue router

other features

  • linkActiveClass
  • scrollBehaviours
  • parseQuery / stringifyQuery
  • fallback
  • currentRoute
  • etc etc

 

https://router.vuejs.org

 

Vuex

State management pattern

https://vuex.vuejs.org

Vuex

Getting started

At the center of every Vuex application is the store. A "store" is basically a container that holds your application state. There are two things that make a Vuex store different from a plain global object

  • Vuex stores are reactive
  • You cannot directly mutate the store's state.

Vuex

Getting started


import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        }
    },
    actions: {
        increment (context) {
            context.commit('increment')
        }
    }
})

Vuex

State


const store = new Vuex.Store({
    state: {
        count: 0,
        users: [],
        user: {
            id: '',
            ...
        },
        auth: {
            expiredAt: null
            token: null
        }
    }
})
  • Initial state
  • All state objects

Reactive... but only first level

Vuex

Actions

const store = new Vuex.Store({
    ...
    actions: {
        increment (context) {
            context.commit('increment')
        },
        decrement ({commit, dispatch, store, rootStore}) {
            if (store.count > 0) {
               commit('increment')
            }
        },
        setCount ({commit}, payload) {
           commit('setCount' parseInt(payload.count))
        },
        setCount2 ({commit}, {count}) {
           commit('setCount' parseInt(count))
        },
        setAsyncCount ({commit}, {count}) {
            return new Promise((resolve, reject) => {
                api.setCount(count).then(() => {
                    commit('setCount' parseInt(count))
                    resolve()
                ).catch((error) => {
                    reject(error.message)
                })
            })
        }
    }
})

Vuex

Actions inside Components

import { mapActions } from 'vuex'

export default {
    name: 'MyComponent',
    methods: {
        ...mapActions([
            'increment',
            'setCount' /* map `this.setCount(count)` */
        ])
    },
    mounted() {
        this.$store.dispatch('increment')
        this.$store.dispatch('setCount', 50)

        this.$store.dispatch('setAsyncCount', 50)
            .then(() => {
                ...
            }).catch(err => {
                alert(err)
            })
    }
}

Vuex

Mutations

const store = new Vuex.Store({
    ...
    mutations: {
        increment (state) {
            state.count++
        },
        decrement (state) {
            state.count--
        },
        setCount (state, count) {
            state.count = count
        }
    }
})

The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler.

Vuex

Mutations inside Components


import { mapMutations } from 'vuex'

export default {
    name: 'MyComponent',
    methods: {
        ...mapMutations([
            'increment',
            'setCount' /* map `this.setCount(count)` */
        ])
    },
    mounted() {
        this.$store.commit('increment')
        this.$store.commit('setCount', 50)
    }
}

Vuex

Accessing store from Components


export default {
    name: 'MyComponent',
    computed: {
        getCount () {
            return this.$store.state.count
        }
    }
}

Vuex

Getters


const store = new Vuex.Store({
    ...
    getters: {
        count: state => state.count,
        isEmpty : state => {
            return state.count > 0
        },
        allNotifications: state => state.notifications,
        notificationsCount: state => state.notifications.length,
        unreadNotifications: state => {
            return _.filter(state.notifications, {'active', true})
        },
        readNotifications: state => {
            return _.filter(state.notifications, {'active', false})
        },
        blueNotifications: state => {
            return _.filter(state.notifications, {'blue', true})
        }
    }
})

Vuex

Getters inside Components


import { mapGetters } from 'vuex'

export default {
    name: 'MyComponent',
    computed: {
        ...mapGetters([
            'count',
            'isEmpty',
            'unreadNotifications'
        ]),
        formatNotifications() {
            return _.map(this.unreadNotifications, (notification) => {
                return {
                    id: notification._id,
                    title: notification.name,
                    date: this.$moment(notification.ts).format('LLLL')
                }
            });
        }
    }
}

Vuex

Vue DevTools integration

Events communication

<template>
    <div class="my-custom-list">
        <ul>
            <li v-for="item in items" :key="item.id">
                {{ item.name }}

                <button @click="selectItem(item)">Select</button>
            </li>
        </ul>
    </div>
</template>
<script>

export default {
    name: 'MyCustomList',
    props: ['items']
    methods: {
        selectItem(item) {
            this.$emit('itemSelected', item)
        }
    }
}
</script>

Events communication

<template>
    <my-custom-list :list="items" @item-selected="onItemSelected" />
</template>
<script>

export default {
    name: 'MyWrapper',
    data() {
        return {
            items: []
        }
    },
    methods: {
        onItemSelected(item) {
            ...

            this.$parent.$parent.itemSelected(item)
            /* 😱😨😰😓🤔🤫😈☠😡 */
        }
    }
}
</script>

Events communication

Component communication pattern


import Vue from 'vue'
export const eventHub = new Vue();

First

Events communication

Component communication pattern


import eventHub from './eventhub'

export default {
    ...
    methods: {
        action(items) {
             eventHub.$emit('dispatch-data', items)
        }
    }
}

Then

Events communication

Component communication pattern


import eventHub from './eventhub'

export default {
    ...
    mounted() {
        eventHub.$on('dispatch-data', (items) => {
            this.items = items
        })
    }
}

Then

Events communication

v-model

<template>
    <my-custom-item v-model="model" />
</template>

<script>

export default {
    name: 'MyWrapper',
    data() {
        return {
            model: 'test'
        }
    }
}
</script>

Events communication

v-model inside

export default {
    name: 'MyCustomItem',
    props: {
        value: {
            type: String,
            default: ''
        },
    },
    data() {
        return {
            myValue: ''
        }
    },
    mounted() {
        this.myValue = this.value
    },
    methods: {
        onValueChanged(newValue) {
            this.$emit('input', newValue)
        }
    },
    watch: {
        value: (newVal, oldVal) {
            if (newVal !== oldVal) {
                this.$emit('input', newValue)
            }
        }
    }
}

Vue.*

how to extend Vue?

With Plugins!

  • Add some global properties or methods
  • Add global directives, filters, transitions, etc...
  • U mixins
  • Vue instance properties this.$something
  • Add libraries

Vue plugins

import MyDirective from './directives/myDirective'
import themeMixin from './mixins/themeMixin'
import MyCustomList from './components/MyCustomList'

const MyPlugin = {}

MyPlugin.install = function (Vue, options) {

  Vue.myGlobalMethod = function () {
    // something logic ...
  }

  Vue.directive('my-directive', MyDirective)
  Vue.component('my-custom-list', MyCustomList)

  Vue.mixin(themeMixin)

  Vue.prototype.$myMethod = function (methodOptions) {
    // something logic ...
  }
}

export default MyPlugin

Vue plugins


import MyPlugin from './plugins/myPlugin'

Vue.use(MyPlugin)


Vue.use(MyPlugin, { ... options })

That's all

Vue Testing

 

There are many popular JavaScript test runners, and Vue Test Utils works with all of them. It's test runner agnostic.

Vue Testing

Documentation example

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}

Vue Testing

Documentation example

// test.js

// Import the `mount()` method from the test utils
// and the component you want to test
import { mount } from '@vue/test-utils'
import Counter from './counter'

// Now mount the component and you have the wrapper
const wrapper = mount(Counter)

// You can access the actual Vue instance via `wrapper.vm`
const vm = wrapper.vm

// To inspect the wrapper deeper just log it to the console
// and your adventure with the Vue Test Utils begins
console.log(wrapper)

Vue Testing

Documentation example

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
      // Now mount the component and you have the wrapper
      const wrapper = mount(Counter)

      it('renders the correct markup', () => {
            expect(wrapper.html()).toContain('<span class="count">0</span>')
      })

      // it's also easy to check for the existence of elements
      it('has a button', () => {
            expect(wrapper.contains('button')).toBe(true)
      })

    it('button click should increment the count', () => {
        expect(wrapper.vm.count).toBe(0)
        const button = wrapper.find('button')
        button.trigger('click')
        expect(wrapper.vm.count).toBe(1)
    })
})

Vue Testing

Documentation example - Shallow Rendering

import { shallowMount } from '@vue/test-utils'
import Counter from './counter'

const wrapper = shallowMount(Counter)
wrapper.vm // the mounted Vue instance

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

expect(wrapper.emitted().foo).toBeTruthy()
expect(wrapper.emitted().foo.length).toBe(2)
expect(wrapper.emitted().foo[1]).toEqual([123])

wrapper.setData({ count: 10 })
wrapper.setProps({ foo: 'bar' })

wrapper.trigger('click')
wrapper.find('button').trigger('click')

Vue Testing

Documentation example - Create Local Vue

import { createLocalVue } from '@vue/test-utils'

// create an extended `Vue` constructor
const localVue = createLocalVue()

// install plugins as normal
localVue.use(MyPlugin)

// pass the `localVue` to the mount options
mount(Component, {
  localVue
})

Large scale applications

  • lang
  • mocks
  • modules
    • <folder>
      • components
      • routes
      • styles
      • pages
  • plugins
  • router
    • routes
  • store
    • modules

Large scale applications

store modules


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

Vue.use(Vuex)

// extract js files inside modules folder
const requireModule = require.context('./modules', true, /index\.js$/)
const modules = {}

requireModule.keys().forEach(fileName => {
  const moduleName = fileName.replace(/(\.\/|\/index\.js)/g, '')
  modules[moduleName] = requireModule(fileName).default
})

const store = new Vuex.Store({
  modules
})

export default store

Large scale applications

router modules

Cordova

Cordova

Start!

 


vueConfig.pages = {
    index: {
        entry: 'src/main.js',
        template: 'public/index.html',
        filename: 'index.html',
        title: 'MyApplication',
        chunks: ['chunk-vendors', 'chunk-common', 'index']
    }
}
vueConfig.outputDir = 'www'

Cordova

my beautiful cordovaLoader.js

const cordovaLoader = (cb) => {
  appendCordovaScript();
  document.addEventListener('deviceready', () => {
    cb();
    setTimeout(() => {
      navigator.splashscreen.hide();
    }, 200)
  })
}

const appendCordovaScript = () => {
  const hasAlready = document.URL.indexOf('http') !== 0 || window.hasOwnProperty('cordova');
  if (hasAlready) {
    return;
  }
  const script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = getCordovaUrl();
  document.head.appendChild(script);
}

const getCordovaUrl = () => {
  return 'cordova.js';
}

export default cordovaLoader

Cordova


....
import cordovaLoader from './cordovaLoader'

cordovaLoader(() => {

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

vueConfig.pages = {
    index: {
        entry: 'src/main.mobile.js',
        template: 'public/index.html',
        filename: 'index.html',
        title: 'MyApplication',
        chunks: ['chunk-vendors', 'chunk-common', 'index']
    }
}
vueConfig.outputDir = 'www'

Awesome Vue. Vol.2

By Claudio Bisconti