Hackages - VueJS 101 - day 2

by Evan You - @youyuxi

Wifi: S14-Hackages / H-102017

Scalability

Transitions

A very common use case, so it’s incorporated in the framework.

Simple case with the v-if

    <div id="el">
        <button @click="ok = !ok">toggle</button>
        <transition name="fade">
            <div v-if="ok">ok</div>
        </transition>
    </div>
var vm = new Vue({
  el: '#el',
  data: {
    ok: true
  }
})

All you need is to have the css style that fit the transition name:

<style>
    .fade-leave, .fade-enter-to {
        opacity: 1;
    }
    .fade-leave-to,.fade-enter {
        opacity: 0;
    }
    .fade-leave-active, .fade-enter-active {
        transition: opacity 0.5s ease;
    }
</style>

Transition is a special vue component

    <transition name="">

Transition attribute:

see:

You can also have more specific access to code the manage the transition in js.

see:

see:

Internaly the transition is done after the value of the ok button as already changed.

For instance here the ok is directly set to false

Also:

<div id="el">
    <button @click="ok = !ok">toggle</button>
    <transition name="fade">
        <div v-if="ok">ok</div>
        <div v-else="ok">not ok</div>
    </transition>
</div>

Will not work because it’s the same content and just replacement. You need a key.

So we will add key attribute and at the same time mode just to have somehting nicer

<div id="el">
    <button @click="ok = !ok">toggle</button>
    <transition name="fade" mode="out-in">
        <div v-if="ok" key="aaa">ok</div>
        <div v-else="ok" key="bbb">not ok</div>
    </transition>
</div>

In short

CLI

The vue cli! It is 90% a scaffolding tool.

Most of the time it takes stuff from the web and put them locally asking question to know which stuff to take.

    vue init webpack-simple hello-world

    ? Project name hellow-world
    ? Project description A Vue.js project
    ? Author Sébastien Barbieri <sebastien.barbieri@gmail.com>
    ? Use sass? No

       vue-cli · Generated "hello-world".

       To get started:

         cd hello-world
         npm install
         npm run dev.

List of templates

vue list

provide a list of existing official template

Future: provide one single template (based on webpack)

What is the difference between webpack simple and webpack: mainly linter and tools.

What’s inside

src/:

Architecture

Feature Oriented Collocation

Building for production

    hello-world$ yarn run build
    yarn run v1.2.1
    $ cross-env NODE_ENV=production webpack --progress --hide-modules
    Hash: 3cd6f9e2b58db9e7ff20                                                           
    Version: webpack 3.7.1
    Time: 3488ms
                                        Asset     Size  Chunks             Chunk Names
    logo.png?82b9c7a5a3f405032b1db71a25f67021  6.85 kB          [emitted]  
                                     build.js  95.8 kB       0  [emitted]  main
                                 build.js.map   803 kB       0  [emitted]  main
    Done in 3.99s.

Files will be manged by webpack

Webpack

Has the ability to pipe the source file to do source file transformation.

output:

import Vue from 'vue'

Will search within the node_module for a vue folder and parse the package.json search for the module and load the resource. In this case the esm module of vue. Without this line the webpack will not be able to provide the adequate js package to load on the fly vue element. But pre-compiled vue element will still work (template within app.vue file will work but template created on runtime will not)

Webpack will shim the js process, that’s fine for development but not good for production. so to get rid of that, we need to use the definePlugin. That will lead to ‘string1’ !== ‘production’ but because webpack will replace string1 with production the section will be ‘production’ !== ‘production’ which will be completely remove by uglify.js.

Overlay

The overlay is used in dev mode to show errors while hot reload is running.

Routing

What routing is in vuejs

basic router - listen to state route change and call a callback

In the vue context routing is essentially mapping url to component

/: point to the main app /profile: point to the profile component /profile/1234: point to the profile component in a specific state

A basic router

window.addEventListener('hashchange', () => {
  // read hash and update app
})
Exercise routing from scratch
  1. hash-router.html
  2. hash-router-solution.html
  3. hash-router-solution-component-definition.html
  4. hash-router-solution-dynamic-component.html
The vue router

hash-router-solution-vue-router.html

See codesandbox.io -> which povide a sandbox

Call the global vue api:

// 1. Install it and handle router props in Vue
Vue.use(VueRouter)

// 2. create a router instance
const router = new VueRouter({
  routes: [
    // define our routes
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar },
    { path: '*', component: NotFound },
  ]
})

new Vue({
  el: '#app',
  // 3. connect the router to the app
  router: router,
  // or shorter:  (es6)
  // router,
  render: h => h(App)
})

Import the router, tell view to handle router, create a router instance and link the router to the app.

Routes order are important it’s the way route are matched, don’t put the catch all (‘*‘) on top.

Vue.use(VueRouter)

Handle 2 new components:

mode: History use browser history but you need history mode fallback it will handle on server side the application. For instance if your app is in /app/, some urls will be: /app/my-sub-part but if the user directly goes to this url the web server will need to handle this properly and send the app which is in /app/ then point it to the component. See apache/nginx rewrite example: https://router.vuejs.org/en/essentials/history-mode.html#apache

There is an option in dev mode to do that:

devServer: {
    historyApiFallback: true,
}
Handling states
const Post = {template: `<div>Post </div>`}

// ...

{ path: '/post/:id', component: Post }, // a route with a specific state

But a better way is to make a nicer component that will handle this properly

const Post = {
  props: ['id'],
  template: `<div>Post </div>`
}

{ path: '/post/:id', component: Post, props: true }, // a route with a specific state + props
Named path

Named path are usefull to refer to path further on.

<router-link :to="{ name: 'posts', params: { id: 789}}">Name routing to Post 789</router-link>
    {
      name: 'posts',
      path: '/post/:id',
      component: Post,
      props: true
    }, // a route with a specific state + props + name

With router link

<router-link :to="{ name: 'posts', params: { id: 789}}">Name routing to Post 789</router-link>

With code


router.push('/bar')
router.push({name: 'posts', params: {id: 123}}) // no need to stringify path, just use object

this.$router.push('/bar')
this.$router.push({name: 'posts', params: {id: 123}})

The great thing about passing object is that you need to stringify or think avout the url, just put all the param handled by the componant

Component that stay on a page while navigating

Think about gmail and the side, top and footer bar the main content change but the others.

Some route:

In the main vue

<template>
  <div id="app">
    <router-link to="/inbox">Inbox</router-link>
    <router-link to="/settings">Settings</router-link>
    <router-view></router-view>
  </div>
</template>

In the app

  routes: [
    // define our routes
    {
      path: '/inbox',
      component: Inbox,
      // nested routes
      children: [
        {path: '', component: InboxMails }, // empty means default
        {path: 'mail/:id', component: InboxMail, props: true },
      ]
    },
  ]
Handling common stuff to do on each page change

Analytics, View count

To handle this, simply use beforeEach

router.beforeEach((to, from, next) => {
  console.log(to)
  if (to.meta.auth) {
    if (isUserLoggedIn) {
      next()
    } else {
      next('/login?from='+next.fullPath)
    }
  } else {
    next()
  }
  next()  // must be called in order to perform the navigation
})

This is a global router hook but we can also handle in component hook

Handling in component hooks

beforeRouterEnter hook: Can handle all the wait and data handling before the navigation occurs beforeRouterUpdate hook: will take care of updating the component once the component is already there

Scoping components

const Inbox = {
    template: ``,
    components: {
        InboxMails
    }
}

The Inbox component will have access to InboMails component

!!! Dom sensitive stuff, the html element are case insensitive <FooBar> will be transfromed to <foobar> before vue can even see it.

Exercise

Use: https://jsonplaceholder.typicode.com/

  1. One page to display a list of page
  2. One page for post detail

Hint

For ajax call, use axios if possible or jqury ajax

axios is iso morphic (idem server/client)

Not ideal but exists: Vue.prototype.$http = axios

Use promise

const p1 = axios.get ...
const p2 = axios.get ...

Promise.all([p1,p2]).then(console.log('all promise done'))

Components

Don’t use components until you need them … as soon as it is too big, split it.

don’t over split in advance

Code Splitting + Async

Async component

is defined as a function that return a promise

    const Foo = () => {
        return new Promise((resolve, reject) => {
        resolve({
            template: `...`
        })
        })
    }

WHY?

Because the import('./Post.vue') will return a Promise in future browser. It’s called: dynamic import

Webpack is transparently doing this for you

So it’s identical to

    const Foo = () => import('./Foo.vue')

Difference between synchronous and asynchronous

    // synchrone
    import Foo from './Foo.vue'

    // asynchrone
    const Foo = () => import('./Foo.vue')

You need an extra plugin to do it because babel doesn’t know how to do it: syntax-dynamic-import

.babelrc

{
    plugins:[
        'syntax-dynamic-import'
    ]
}

common chunk plugins is used when there is a dependency issue between chunk, its to avoid duplication, but duplication is not a real problem because everything is cached.

Multiple component async

Use webpack webpackChunkName common to make multiple component inot the same chunck

    // asynchrone
    const Foo = () => import(/* webpackChunkName: post */ './Post.vue')
    const Foo = () => import(/* webpackChunkName: post */ './Comment.vue')

State

To cache the data and keep data across several page.

The idea is to store the information at application level in a centralized state management.

The state will be shared accross components.

All component should then refer to the same state.

All library such as flex, vuex … are designed to handle state but also have a lot of constraint.

see state.html

State managment

State management is done via a store

    const obj = {
        foo: 123
    }

    let fooValue

    Object.defineProperty(obj, 'foo', {
        get () {
            // obj.foo
            // track dependency
            return fooValue
        },
        set (newValue) {
            // obj.foo = 1234
            // trigger update
            fooValue = newValue
        }
    })

To handle dependency the getter must be invoked. To be invoked it needs to be accessed via the .

let bar = obj.foo // tracking dep.
// using
bar
// will not track dependency anymore

So you always need to use the . notation of the state object to ensure that the getter is triggered

so in component you need to avoid

    Vue.component('counter', {
        template: `<div>8</div>`,
        data () {
            return { count: store.state.count }
        }
    })

but instead

    Vue.component('counter', {
        template: `<div>8</div>`,
        data () {
            return { store.state }
        }
    })

In Vuex store are created as follow

const store = new Vuex.store({
    state: {
        count: 0
    },
    mutations: {
        inc (state) {
            state.count++
        }
    }
})

For synchronous operation we use mutations

For asynchronous operation we use actions

Actions

Use dispatch

Mutations

Use commit

Similar to COMMIT in DB

Getters

Getters provide caching and provide a way to retrieve data.

Equivalent to SELECT in DB

mapState

To avoid declaring every computed access within the component, there is an helper.

Vuex.mapState(['foo', 'bar']) => {
    foo() {return this.$store.state.foo},
    bar() {return this.$store.state.bar},
}

Basic store application

import Vue from 'vue'
import Vuex, { Store } from 'vuex'
import axios from 'axios'

const API_BASE = ''

let fetchingPost = false

export default new Store({
  state: {
    posts: [],
    error: null
  },
  actions: {
    fetchAllPosts ({ commit }) {
      if (!fetchingPost) {
        axios.get(API_BASE + '/posts').then(({ data }) => {
          commit('saveAllPosts', {
            posts: data // use object it helps with debugging
          })
        })
        .catch(err => {
          commit('error', { err: err })
        })
      }
    }
  },
  mutations: {
    saveAllPosts (state, payload) {
      state.posts = payload.posts
    },
    error (state, error) {
      state.error = error.err
    }
  }
})

to use it now you can drastically reduce the component


// Posts list
exports default {
    computed: {
        posts () {
            return this.$store.state.posts
        }
    }
}

// Post detail
exports default {
    props: ['id']
    computed: {
        posts () {
            return this.$store.state.posts.find( post => {
                return post.id === this.id
            })
        };
        comments () {
            return this.$store.state.comments.filter( comment => {
                return comment.postId === this.id
            })
        }
    },
    created () {
        this.$store.dispatch('fetchCommentsForPost', {id: this.id})
    }
}

Modules

What if 2 persons are working on the store.

You can split the store n several Module and in the store add them.

export default {
    namespaced: true,
    state: {
        // ...
    }
}

To use it

export default new Store({
    modules: {
        posts: postsModule
    }
})