Welcome to VueJS workshop

Independent consultant

Web / Mobile / VR / AR / IoT

GDE, author, engineer

What is Vue?

Vue is a progressive framework  for building user interfaces that is designed to be incrementally adoptable

Lifecycle of Vue instance

Vue Devtools

Template syntax

you can put one single expression in {{ }}. Everything that is on this will be available in mustache syntax

<span>Message: {{ msg }}</span>
{{ var a = 1 }}
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
{{ if (ok) { return message } }}

Directives

Special attributes with v- prefix

Directives reactively apply changes to DOM when a value of its single expression changes

Directives can take an argument using : syntax

v-text

v-html

v-show

v-if

v-else

v-else-if

v-for

v-on

v-bind

v-model

v-slot

v-once

used instead of {{ }} syntax

render raw HTML (dangerous)

hides element (display: none)

conditionally render element based on predicate (Don't use together with v-for)

previous sibling must be v-if or v-else-if

restrictions the same as on v-else, but accepts expression as v-if

loops over collection

accepts event name as an argument and invokes a function/statement

dynamically binds one or more attributes or props to expression

creates two way binding on a form input element

v-cloak

pass properties to slots

can be used to hide mustache bindings until Vue instance is ready

v-pre

removes mustache syntax and renders as is

render element only once

shorthands

v-bind shorthand :

v-on shorthand @

some directives are so common that they have shorthands

styles

<div v-bind:class="{ active: isActive }"></div>
<div v-bind:class="[activeClass, errorClass]"></div>
<div v-bind:style="{ width: withVariable }"></div>
<div v-bind:style="[activeStyle, otherStyle]"></div>

Object syntax

Array syntax

inline styles

Object syntax

Array syntax

Listening to events

We can listen to any event by using @

<a v-on:click.stop="doThis"></a>

And we can use so-called modifiers

.stop

stopPropagation()

preventDefault()

addEventListener(event, function, useCapture);

will trigger only if target is self

will be triggered only once (also on component events)

indicates that the function specified by listener will never call preventDefault()

.prevent

.capture

.self

.once

.passive

Keyboard events

we can pass key modifiers to map to target keys

<input v-on:keyup.page-down="onPageDown">

.enter

.tab

.delete

.esc

.space

.up

.down

.left

.right

you can use keycodes as well (not consistent in IE9)

<input @keyup.13="submit"></input>

or configure your own mappings

Vue.config.keyCodes.f1 = 112

System modifiers

They work both for mouse and keyboard events

.ctrl

.alt

.shift

.meta

<input v-on:keyup.alt.67="clear">

.exact

used to control exact combination of system modifiers

Mouse modifiers

.left

.right

.middle

Methods

this anywhere in Vue instance is proxied and has everything on data and methods available

Computed

computed properties are cached based on their reactive dependencies.

computed: {
  priceWithCurrencySymbol: function () {
    return this.price + this.currencySymbol
  }
}
computed: {
  priceWithCurrencySymbol: {
    get: function () {
        return this.price + currencySymbol
    },
      // setter
    set: function (priceString) {
      this.currencySymbol = currencySymbolsDict[priceString[priceString.length - 1]] || "$"
      this.price = priceString.slice(0, -1)
    }
  }
}

Computed

If you need to change data -> Use data

When you need to change data presentation -> use computed properties

Watchers

Vue provides a way to "watch" data and react to its change  

watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }

Don't use watchers unless you really need to. In most cases you need computed prop

Use watchers if you need to run side effect on data change

Filters

filters work mustache interpolations and v-bind expressions

filters: {
  capitalize: function(value) {
    if(!value) return '';
    value = value.toString();
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}
{{ message | filterA | filterB }}

Exercise time

Components

Collection of elements encapsulated in container and can be referred as a single element

Vue.component('click-counter', {
  template: '<button @click="count++">{{count}}</button>',
  data(){
    return {
      count: 0
    }
  }
});

Note: data must be a function

Components

Component template can be defined both as string and as script tag

Vue.component('click-counter', {
  template: '#click-counter-template', // inside html: <script type="script/x-template"></script>
  data(){
    return {
      count: 0
    }
  }
});

Components

Component can accept props

Vue.component('TodoList', {
    template: '#todo-list',
    props: ['todos']
  });
Vue.component('TodoList', {
  template: '#todo-list',
  props: {
    todos: {
      type: Array,
      required: true, //required
      validator: value => { //custom validation
        return true;
      }
  }}
});

bad

good

Components

props can be assigned default values as well as custom validation

Vue.component('TodoList', {
  template: '#todo-list',
  props: {
    todos: {
      type: Array,
      required: true, //required
      default(){
      	return [] // Arrays, objects should be passed default function
      },
      validator: value => { //custom validation
        return true;
      }
  }}
});

Passing data to component

<todo-list :todos="todos"></todo-list>

v-bind to pass data down via props

Component naming

  • Always multi word to prevent conflicts
  • Base components - used globally - they shoud start with Base, App or V
  • Single instance component names - "The"
  • Tightly coupled component names - make relationships clear
  • Order of the words - start with highest level (general) words and end with descriptive and modifying words
  • Using full worlds - use full worlds instead of abbreviation and shorthands

Component naming

<todo-list :todos="todos"></todo-list>

Pascal or camel case converts to kebab-case

Vue.component('TodoList', {
    template: '',
    props: ['todos']
  });

Component registration

const TodoList = {
    template: '#todo-list',
    props: {
      todos: Array
    }
  };

new Vue({
  ...
  components: {
    TodoList
  }
})
Vue.component('TodoList', {
    template: '',
    props: ['todos']
  });

Global registration

Local registration

Events communication

<todo-list :items="items" @update="updateItemArray"></todo-list>

items array on data is passed down as prop

custom event emitted from todo-list component method

this.$emit("update", this.items.map((item, index) =>
  index === itemIndex ? value : item 
));

Component v-model

 

v-model on a component in order to work expects component to emit input event and get value as a prop

You can change that:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

Exercise 1.1

convert exercise 1 to components

Single file components

in Vue we have .vue files which contain both template, code and styles and which we can load with vue-loader and relevant style loader

template

code

styles that are applied through assigning data-v-hash attribute

Let's convert our todo app to webpack

install webpack 

yarn add --dev webpack webpack-cli vue

create src/index.js and src/App.vue files

import Vue from 'vue'
import App from './App.vue'
new Vue({
  el: '#app',
  render: h => h(App)
})
<template>
  <div>
    <h1>Hello World!</h1>
  </div>
</template>

index.js

App.vue

create basic webpack config

'use strict'
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}
yarn add --dev vue-loader vue-template-compiler vue-style-loader css-loader

add build script

"build": "webpack --config build/webpack.config.dev.js"

package.json

include script in index.html

<script src="dist/main.js" type="text/javascript"></script>

Hot reload

 
"dev": "webpack-dev-server --config webpack.config.dev.js"

package.json

add HMR and HTML plugins to webpack config

yarn add --dev webpack-dev-server html-webpack-plugin
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
  filename: 'index.html',
  template: 'index.html',
  inject: true
})

remove dist script reference from index.html

Styles

 
const MiniCssExtractPlugin  = require('mini-css-extract-plugin')

// ...

plugins: [
  new MiniCssExtractPlugin({
    filename: 'main.css'
  })
]

add mini-css-extract plugin (nice to have)

yarn add --dev css-loader vue-style-loader mini-css-extract-plugin

older browsers

 
{
  test: /\.js$/,
  use: 'babel-loader'
}

add loader

yarn add --dev @babel/core babel-loader @babel/preset-env

add .babelrc file

{
  "presets": [
    ["@babel/env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }]
  ]
}

Static files

 
yarn add --dev copy-webpack-plugin

add plugin to webpack.config

const CopyWebpackPlugin = require('copy-webpack-plugin')
//..
const path = require('path')
function resolve (dir) {
  return path.join(__dirname, '..', dir)
}
plugins: [
  new CopyWebpackPlugin([{
    from: resolve('static/img'),
    to: resolve('dist/static/img'),
    toType: 'dir'
  }])
]

Excercise 2

 

Vue CLI

 

A must CLI for prototyping, maintaining Vue projects, installing plugins etc

@vue/cli

@vue/cli-service

@vue/cli-plugin-

CLI tool with scaffolding commands and vue ui command

based on webpack and webpack-dev-server

set of plugins that are loaded automatically by @vue-cli-service

Vue CLI

 
vue serve
vue create

vue ui

Recommended to use only for prototyping - loads .vue files with zero config

Creates new Vue project prompting for different plugins etc. You can use it on an existing directory.

Present neat ui to manage your vue projects

npm install -g @vue/cli-service-global
# or
yarn global add @vue/cli-service-global

vue add plugin-name

invokes plugin generator and resolves vue plugin

How to change Vue CLI configuration

 

vue-cli-service automatically loads optional vue.config.js file where you can override any configuration

publicPath - equivalent to webpacks output.publicPath

outputDir - equivalent to webpacks output.path

assetsDir - directive related to outputDir to nest generated static assets

indexPath - path of index.html relative to outputDir

pages - run in multipage mode

runtimeCompiler - to use template options in components (increase 30% bundle size)

configureWebpack - object that will be merged into webpack.config

chainWebpack - powered by webpack-chain and allows more fine grained control

devServer - all options of webpack dev server

chainWebpack: config => {
        config
            .plugin('html')
            .tap(args => {
                args[0].template = path.join(__dirname,'index.html')
                return args
            })
    }

Vue in VSCode

Vetur extension

Single file components

template need to have one wrapping element

data must be a function

scoped styles that are applied through assigning data-v-hash attributes. not scoped are applied globally

Styles gotchas

 

use <style scoped> in your components

classes and styles passed to components are applied to wrapping element

Excercise 2.1

change our Todo app to Vue CLI

chainWebpack: config => {
        config
            .plugin('html')
            .tap(args => {
                args[0].template = path.join(__dirname,'index.html')
                return args
            })
    }

Nested components

 

import component and register it locally

use base component

Automatic global registration

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // The relative path of the components folder
  './components',
  // Whether or not to look in subfolders
  false,
  // The regular expression used to match base component filenames
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // Get component config
  const componentConfig = requireComponent(fileName)

  // Get PascalCase name of component
  const componentName = upperFirst(
    camelCase(
      // Gets the file name regardless of folder depth
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )


  // Register component globally
  Vue.component(
    componentName,
    // Look for the component options on `.default`, which will
    // exist if the component was exported with `export default`,
    // otherwise fall back to module's root.
    componentConfig.default || componentConfig
  )
})

We want to have BaseComponents as global and the rest as local

Slots

 

in child component use <slot></slot> as a placeholder

In the parent component use v-slot directive on an element to put inside the slot

named slots

 
<slot name="slotName"></slot>
v-slot:slotName

in ParentComponent

ChildComponent

v-slot can be used only on template or component

default content

 
<slot name="slotName">
  Default
</slot>
v-slot:slotName

ParentComponent

ChildComponent

scoped slots

 
<slot name="slotName">
</slot>
v-slot:slotName="slotProps"

ParentComponent

ChildComponent

give parent access to child data

<slot :author="author" >
</slot>
v-slot:default="defaultSlotProps"

msg from child data

msg from child data

destructuring

 
<slot name="slotName">
</slot>

ParentComponent

ChildComponent

<slot :author="author" >
</slot>
v-slot:default="{ author }"

shorthand #

 
<slot name="slotName">
</slot>

ParentComponent

ChildComponent

<slot :author="author" >
</slot>
<template #default="{ author }">
<template #slotName>Content</template>

Components communication

 
EventBus.$emit('eventName', payload);

EventBus

emit event

export const EventBus = new Vue();
EventBus.$on('eventName', handler);

subscribe

EventBus.$off('eventName', handler);

remove listener

you can use $once to trigger event only once

Dynamic components

 

You can pass components dynamically in the following manner:

build in Vue component

<component is="componentPropertyObject"></component>

bind to component property object

keeping components alive

 

Dynamic components are destroyed and recreated when new component is passed to is prop. Use <keep-alive></keep-alive> to keep components alive

<keep-alive>
  <component is="componentPropertyObject"></component>

</keep-alive


 

keep-alive also introduces activated and deactivated lifecycle hooks

Async components

 

components can be loaded asynchronously using the following syntax

const BelowFold = () => import(
    /* webpackChunkName: "below-fold" */ './BelowFold.vue'
  );
 
const AboveTheFold = () => import(
    /* webpackChunkName: "modal" */ './AboveTheFold.vue'
);

//....
//
{
  components: {
    AboveTheFold,
    BelowFold
  }
}

Lifecycle hooks patterns

 

beforeCreate() - runs before initialization data is not reactive yet, events hasn't been set up

created() - data is reactive, events can be accessed, templates and VDOM not mounted or rendered (API calls are usually done here)

beforeMount() - almost not used - do something before initial render

mounted() - component is rendered - you have access to reactive component, templates and VDOM (this.$el)

using it for API calls it's antipattern

use it for DOM access or integrating non Vue libs

beforeUpdate() - runs before update happens just before DOM rerenders. You can get there a new state of your reactive data before it rerenders

updated() - runs after DOM is rerendered (use if you need to access DOM after the change)

beforeDestroy() - runs just before component destruction. Used to get rid of any subscriptions. Component still presents here and fully working

destroyed() - everything is destroyed. Used for any last-minute cleanup

API

fetch / axios / vue-resource

create services/apiService.js

The main purpose is to encapsulate api calls in one single file and expose methods that will be accessed through components

import axios from 'axios'
    
const apiClient = axios.create({  
  baseURL: `http://localhost:3000`,
  withCredentials: false, // This is the default
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json'
  }
})

export default {
  getEvents() {
    return apiClient.get('/events')
  }
}

Excercise 3

 

Create a new project using vue-cli without vue-router or VueX

Create /views directory and inside Home and Blog component

Create BaseLayout component that will have header, footer and content slot and register it globally.

Create DummyNavigator component

navigator will get routes object prop

when routeChange event triggered navigator will switch displayed component based on provided prop

navigator will use history api to change browser url

Thanks and see you tomorrow

  @VladimirNovick

Made with Slides.com