Jason Heylon Blog

Advanced features of vue.js

February 10, 2019 | 24 Minute Read

Reactive

getter and setter

const obj = { foo: 123 }

convert(obj)

obj.foo // should log: 'getting key "foo": 123'

obj.foo = 234
obj.foo // should log: 'getting key "foo"::::: 234'


function convert (obj) {
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        console.log(`getting key "${key}": ${internalValue}`
        return internalValue
      },
      set (newValue) {
        internalValue = newValue
      }
    }
  }
}

Dependency Tracking


const dep = new Dep()

autorun(() => {
  dep.depend()
  console.log('updated')
})

// should log: "updated"

dep.notify()
// should log: "updated"

// --------

window.Dep = class Dep {
  constructor () {
    this.subscribers = new Set()
  }

  depend () {
    if (activeUpdate) {
      this.subscribers.add(activeUpdate)

    }
  }

  notify () {
    this.subscribers.forEach(sub => sub())
  }
}

// JS one thread execution
let activeUpdate

function autorun (update) {
  function wrappedUpdate () {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }

  wrappedUpdate()
}

mini Observer

const state = {
  count: 0
}

observe(state)

autorun(() => {
  console.log(state.count)
})
// should log "count is: 0"

state.count++
// should log "count is: 1"

Plugins

Vue.use(plugin)

function (Vue, options) {
  // ... plugin code
}



Vue.mixin(options)
// apply to all Vue component after mixin

$options // merged options




simple plugin

  const vm = new Vue({
    data: { foo: 10 },
    rules: {
      foo: {
        value: value => value > 1,
        message: 'foo must be greater than one'
      }
    }

  })

  vm.foo = 0 // should log "foo must be greater than one"

  // ------

  const RulesPlugin = {
    install (Vue) {
      Vue.mixin({
        created () {
          if (this.$options.rules){
            Object.keys(this.$options.rules).forEach(key => {
              const rule = this.$options.rules[key]

              this.$watch(key, (newValue) => {
                const result = rule.validate(newValue)
                if (!result) {
                  console.log(rule.message)
                }
              })
            })
          }
        }
      })
    }
  }

  Vue.use(RulesPlugin)

Render Function

Initial Render

Template -> (compiled into) Render Function -> (returns) Virtual DOM -> (generates) Actual DOM

Subsequent Updates

Render Function -> (returns) New Virtual DOM -> (diffed against Old Virtual DOM) DOM Updates -> (applied to) Actual DOM

  • Actual DOM
document.createELement('Div')
// "[object HTMLDivElement]"

Browser Native Object (expensive)

  • Virtual DOM
vm.$createElement('div')
// { tag: 'div', data: {.attrs: {}, ... }, children: [] }

Plain JavaScript Object (cheap)

Render Function API


import MyComponent from '...'

export default {
  render (h) {
    return h('div', {}, [...])
    // h (myComponent, { props: {...} })
  }
}

Dynamically Render Tags

<div id="app">
  <example :tags=['h1', 'h2', 'h3']></example>
</div>
Vue.component('example', {
  props: ['tags'],
  render (h) {
    const children = this.tags.map((tag, idx) => h(tag, idx))
    return h('div', { attrs: { id: 'hello' } }, children)
  }
})

new Vue({el: '#app'})

About the Functional Components

  • In Vue implementation, functional components are expanded eagerly
  • Functional components’s render function will be invoked inside parent components’s render function.
  • It’s kind of expended in line which makes it extremely cheap, because there’s no overhead of creating an instance.
  • Perfect use case is performance optimizations when have a log of leaf nodes(eg: a huge list).
// transform to functional component

Vue.component('example', {
  props: ['tags'],
  functional: true,
  render (h, context) {
    // context.props: this.props in normal vue components
    // context.slots(): this.$slots in normal vue components
    // context.children: raw children
    // context.parent: root component, it's the div[id=app] component in this current codes.

    const children = context.props.tags.map((tag, idx) => h(tag, idx))
    return h('div', { attrs: { id: 'hello' } }, children)
  }
})

new Vue({el: '#app'})

Dynamically Render Components

<div id="app">
  <example :ok="ok"></example>
  <button @click="ok = !ok">toggle</button>
</div>
const Foo = {
  render: h => h('div', 'foo')
}
const Bar = {
  render: h => h('div', 'bar')
}

Vue.component('example', {
  props: ['ok'],
  render (h) {
    // return h(Foo)
    return this.ok ? h(Foo) : h(Bar)
  }
})

new Vue({
  el: '#app',
  data: { ok: true }
})

Higher-Order Components


<div id="app">
  <smart-avatar username='vuejs' id='hello'></smart-avatar>
</div>


// mock API
function fetchURL (username, cb) {
  setTimeout(() => {
    cb(`https://avatars.some-site.com/avatars/${username}.jpg`)
  }, 500)
}

const Avatar = {
  props: ['src'],
  template: '<img :src="src">'
}

function withAvatarURL (InnerComponent) {
  // we also can do some optimizations here, eg: create an object to cache avatar urls.
  return {
    props: ['username'],
    data () {
      return {
        url: 'https://placeholder.avatar.com/avatar.jpg'
      }
    },
    created () {
      fetchURL(this.username, url => {
        this.url = url
      })
    },
    render (h) {
      return h(InnerComponent,  {
        props: {
          src: this.url,
          attrs: this.$attrs // pass other attributes to inner component
        }
      }
    }
  }
}

const SmartAvatar = withAvatarURL(Avatar)

new Vue({
  el: '#app',
  components: { SmartAvatar }
})

State management

Passing Props

<div id=“app”>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <button @click=“count++”>increment</button>
</div>
new Vue({
  el: “#app,
	data: { counter: 0 }
  components: {
	  Counter: {
      props: [count],
      template: <div>8</div>    }
  }
})

Shared Object

Why data need to be a function? Most of the time, we want each component instance to have its own separate unique piece of data, instead of all of these components sharing the same piece of data.

<div id=“app”>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <button @click=“inc”>increment</button>
</div>
const state = {
  count: 0
}

const Counter = {
  // render: h => h(‘div’, state.count)
  data () {
    return state
  },
  render (h) {
    return h(div, this.count)
  }
}

new Vue({
	el: “#app.
  components: { Counter },
  methods: {
    inc () {
      state.count++
    }
  }
})

Shared Instance

<div id=“app”>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <button @click=“inc”>increment</button>
</div>
const state = new Vue({
  data: {
    count: 0
  },
  methods: {
    inc () {
      this.count++
    }
  }
})

const Counter = {
  render: h => h(div, state.count)
}

new Vue({
	el: “#app.
  components: { Counter },
  methods: {
    inc () {
      state.inc()
    }
  }
})

In this situation, we can make the state and alteration together in one object. In real world, the logic here maybe more complex than that only a increment of a count variable.

Mutation

<div id=“app”>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <counter :count=“count”></counter>
  <button @click=“inc”>increment</button>
</div>
function createStore ({ state, mutations }) {
  return new Vue({
    data: { state },
    methods: {
      commit (mutationType) {
        mutations[mutationType](this.state)
      }
    }
  })
}

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

const Counter = {
  render (h) {
    return h(div, store.state.count)
  }
}

new Vue({
  el: ‘#app,
  components: { Counter },
  methods: {
    inc () {
      store.commit(inc)
    }
  }
})

Functional

<div id=“app”></div>
function app ({ el, model, view, actions }) {
  const wrappedActions = {}

  Object.keys(actions).forEach(key => {
    const originalAction = actions[key]
    wrappedActions[key] = () => {
      const nextModel = originalAction(vm.model)
      vm.model = nextModel
    }
  })

  const vm = new Vue({
    el,
    data: {
      model
    },
    render (h) {
      return view(h, this.model, wrappedActions)
    }
  })
}

app({
  el: ‘#app,
  model: {
    count: 0
  },
  actions: {
    inc: ({ count }) => ({ count: count + 1 }),
    dec: ({ count }) => ({ count: count - 1 })
  },
  view: (h, model, actions) => h(div, { attrs: { id: app}}, [
    model.count,  ,
    h(button, { on: { click: actions.inc }}, +),
    h(button, { on: { click: actions.dec }}, -),
  ])
})

Basic Hash Router

<div>
  <component :is=“url”></component>
  <a href=“#foo”>foo</a>
  <a href=“#bar”>bar</a>
</div>
// window.location.hash
//window.addEventListener(‘hashchange’, () => {
//})


window.addEventListener(hashchange, () => {
  app.url = window.location.hash.slice(1)
})

const app = new Vue({
  el: app,
  data: {
    url: window.location.hash.slick(1)
  },
  components: {
    foo: { template: <div>foo</div>’ },
    bar: { template: <div>bar</div>’ }
  }
})

Route Table

<div>
  <a href=“#foo”>foo</a>
  <a href=“#bar”>bar</a>
</div>
const Foo = { template: <div>foo</div>’ }
const Bar = { template: <div>bar</div>’ }
const NotFound = { template: <div>not found!</div>’ }

const routeTable = {
  foo: Foo,
  bar: Bar
}

window.addEventListener(hashchange, () => {
  app.url = window.location.hash.slice(1)
})

const app = new Vue({
  el: ‘#app,
  data: {
    url: window.location.hash.slice(1)
  },
  render (h) {
    return h(div, [
      h(routeTable[this.url] || NotFound),
      h(a, { attrs: { href: ‘#foo }}, foo),
       | ,
      h(a, { attrs: { href: ‘#bar }}, bar)
    ])
  }
})

Path to Regular Expressions And Dynamic Route

route: user/:username url: user/someone params: { username: ‘someone’ }

ref: Path-To-Regexp


<div id=“app”></div>
// some thing we need here
const keys = []
const regex = pathToRegexp(foo/:bar, keys)

const result = regex.exec(/foo/123)
// result [‘/foo/123’, ‘123’]
// [{ name: ‘bar’ }]
// to { bar: ‘123’ }


// ——————
const Foo = {
  props: [id],
  template: <div>foo with id: </div>}
const Bar = { template: <div>bar</div>’ }
const NotFound = { template: <div>not found!</div>’ }

const routeTable = {
  /foo/:id: Foo,
  /bar: Bar
}

const compiledRoutes = []

Object.keys(routeTable).forEach(path => {
  const dynamicSegments = []
  const regex = pathToRegexp(path, dynamicSegments)
  const component = routeTable[path]

  compiledRoutes.push({
    component,
    regex,
    dynamicSegments
  })
})

window.addEventListener(hashchange, () => {
  app.url = window.location.hash.slice(1)
})

const app = new Vue({
  el: ‘#app,
  data: {
    url: window.location.hash.slice(1)
  },
  render (h) {
    const path = / + this.url
    let componentToRender = NotFoud
    let props = {}

    compiledRoutes.some(route => {
      const match = route.regex.exec(path)
      if (match) {
        componentToRender = route.component

        dynamicSegments.forEach((segment, index) => {
          props[segment.name] = match[index + 1]
        })
      }
    })

    return h(div, [
      h(componentToRender, { props }),
      h(a, { attrs: { href: ‘#foo/123 }}, foo 123),
       | ,
      h(a, { attrs: { href: ‘#foo/234 }}, foo 234),
       | ,
      h(a, { attrs: { href: ‘#bar }}, bar)
    ])
  }
})

Form Validation

Create a Validation Library

<div id=“app”>
  <form @submit=“validagte”>
    <input v-model=“text”>
  <br>
  <input v-model=“email”>

  <ul v-if=“!$v.valid” style=“color: red>
    <li v-for=“error in $v.errors>
      
    </li>
  </ul>

  <input type=“submit” :disabled=“!$v.valid”>
  </form >
</div>
const validationPlugin = {
  install (Vue) {
    Vue.mixin({
      computed () {
        $v () {
          const rules = this.$options.validations
	        let valid = true
	        const errors = []

	        Object.keys(rules).forEach(key => {
	          const rule = rules[key]
	          const value = this[key]
	          const result = rule.validate(value)
	          if (!result) {
	            valid = false
	            errors.push(rule.message(key, value))
	          }
	        }
	      }
		}
    })
  }
}

Vue.use(validationPlugin)

const emailRE = //

new Vue({
  el: ‘#app,
  data: {
    text: foo,
    email: ‘’
  },
  validations: {
    text: {
      validate: value => value.length >= 5,
      message: (key, value) => `${key} should have a min length of 5`
    },
    email: {
      validate: value => emailRE.test(value),
      message: key => `${key} must be a valid email`
    }
  },
  methods: {
    validate (e) {
      if (!this.$v.valid) {
        e.preventDefault()
        alert(not valid!)
      }
    }
  }
})

I18n

Internationalization Approaches

<div id=“app”>
  <h1></h1>

  <button>English @click=“changeLang(‘en’)”</button>
  <button>中文 @click=“changeLang(‘zh’)”</button>
  <button>Dutch @click=“changeLang(‘nl’)”</button>
</div>
const i18nPlugin = {
  install (Vue, locaels) {
    Vue.proptype.$t = function (id) {
      return locales[this.$root.lang]
    }
  }
}

Vue.use(i18nPlugin, {
  en: { welcome-message: hello },
  zh: { welcome-message: ’你好’ }
  nl: { welcome-message: Hallo }
})

new Vue({
  el: ‘#app,
  data: {
    lang: en
  },
  methods: {
    changeLang (lang) {
      this.lang = lang
    }
  }

})