ruk·si

🌲 Vue

Updated at 2016-01-01 00:07

Vue is a JavaScript library similar to React and Angular. While React has the best performance and Angular is the most feature rich, Vue is the simplest; and by far the easiest to add on top of an existing project if you already know jQuery. And compared to jQuery, Vue provides maintainable structure to your JavaScript, but it's very common to use jQuery and Vue together.

The main concept is that you define, create and scope a Vue instance to a DOM element. new is creation, the object passed to Vue is definition and el -> id="" pair is scoping.

new Vue({
    el: '#header',
    data: {
        username: 'foobar'
    }
});
<div id="header">
    <span>Username: {{ username }}</span>
</div>

Use development version of Vue when developing (duh). Warnings are helpful and are only available in the development version.

Vue doesn't support IE8 and below. Everything else should work.

Vue is also reactive, you might want to learn what it means. Not required though.

You should create a strict directory structure for your Vue code.

js/vue/
    components/         # basic Vue components
        DatePicker.js
    views/              # views are page-wide components
        Checkout.js
    filters/            # all Vue filter functions used in piping
        reverse.js
    app.js

v-model creates two-way binding between an element and data. Only usable with input, select and textarea.

new Vue({
    el: '#settings',
    data: {
        username: 'foobar'
    }
});
<div id="settings">
    <div>{{ username }}</div>
    <input v-model="username">
</div>

You can specify parsing type with extra HTML attributes.

<div id="profile">
    <input v-model="age" number>
</div>

You can specify debounce so it waits specified number of ms before triggering the data change event. Good for AJAX powered autocompletion.

<input v-model="searchKey" debounce="500">

Data is automatically accessible through this. All changes trigger a change event that Vue listens to.

new Vue({
    data: {
        dogname: 'Muffin'
    },
    doStuff: function() {
        // this automatically updates all views that show the variable
        this.dogname = 'Spot';
        // remember that you need to `bind` all callbacks
        setTimeout(function() {
            this.dogname = 'Elvis';
        }.bind(this));
    }
});

If data value is not predefined, you can create it with $set.

this.$set('messages', ['Hello World!', ''])

You can debug Vue values by piping data to json filter. Just place this inside the DOM element on which Vue is scoped to.

<pre>{{ $data | json }}</pre>

Vue has a few predefined instance level events that you can listen to.

new Vue({
    created: function() {
        // instance has just been reacted and data is being observed
    },
    beforeCompile: function() {
        // template is about to be compiled
    },
    compiled: function() {
        // template is compiled and in DOM
    },
    ready: function() {
        // template is compiled, in DOM and everything has been bound
    },
    beforeDestroy: function() {
        // instance is about to be destroyed
    },
    destroyed: function() {
        // instance has just been destroyed
    }
});

v-for is for basic collection item processing.

new Vue({
    el: '#farm',
    data: {
        animals: ['horse', 'cow', 'sheep']
    }
});
<div id="farm">
    <ul>
        <li v-for="animal in animals">{{ animal }}</li>
    </ul>
</div>

v-text allows defining the content using a single variable or method. v-html is similar but doesn't escape HTML tags.

new Vue({
    el: '#popup',
    data: { username: 'John' }
});
<div id="popup">
    <div v-text="username"></div>
    <div v-text="'literally username'"></div>
</div>

Vue overwrites array functions like push.

new Vue({
    data: {
        names: ['John', 'Emil']
    },
    methods: {
        doStuff: {
            // anything that depends on names is notified
            this.names.push('Adam');
        }
    }
});

@ is used to bind events defined in Vue instance methods. @ is a shorthand for v-on directive, just use @.

new Vue({
    el: '#shout-box',
    methods: {
        shout: function() {
            console.log('OH NO!');
        }
    }
});
<div id="shout-box">
    <button @click="shout">Shout!</button>
    <input @keyup="shout" @keydown="shout">
</div>

Events can have modifiers using .. Modifiers are like Vue filters, they can do very different things.

<!-- only fires if specified key is pressed -->
<div id="whisper-box">
    <input @keyup.13="whisper">
    <input @keyup.enter="whisper">
    <input @keydown.a="whisper">
</div>

<!-- automatic preventDefault() -->
<form id="some-form" @submit.prevent="formSubmit">
    <button type="submit">Submit</button>
</form>

v-show conditionally assigns display: none to an element if falsy.

new Vue({
    el: '#toggler',
    data: {
        isToggled: true
    }
});
<div id="toggler">
    <button @click="isToggled = !isToggled">Toggle!</button>
    <span v-show="isToggled">State: {{ isToggled }}</span>
</div>

v-if conditionally renders an element if truthy. Note that contained components are destroyed and constructed during toggles. Can optionally be followed by another element with v-else.

new Vue({
    el: '#conditional-box',
    data: { truth: true },
    methods: {
        toggleConditional: function() {
            this.truth = !this.truth;
        }
    }
});
<div id="conditional-box">
    <div v-if="truth">Yup!</div>
    <div v-else>Nope!</div>
    <button @click="toggleConditional">What?</button>
</div>

Radio buttons and checkboxes are straightforward.

new Vue({
    el: '#tv-channel-selection',
    data: {
        channel: 'all'
    }
});
<div id="tv-channel-selection">
    <div>{{ channel }}</div>
    <label>
        <input type="radio" name="channel" v-model="channel" value="all">
        All
    </label>
    <label>
        <input type="radio" name="channel" v-model="channel" value="fox">
        Fox
    </label>
    <label>
        <input type="radio" name="channel" v-model="channel" value="bbc">
        BBC
    </label>
</div>

computed allows making values that are computed from the data.

new Vue({
    computed: {
        errors: function() {
            // you can do any kind of synchronous computation here
            // result is cached and only re-evaluated when
            // tracked data values are changed
            return true;
        }
    }
});

new Vue({
    computed: {
        errors: {
            cache: false, // disabling the cache
            get: function () {
                return true;
            }
        }
    }
});

watch allows listening when data changes. Usually you can accoplish this better using computer properties but watch has its uses.

new Vue({
    el: '#passport',
    data: {
        firstName: 'Ruksi',
        lastName: 'Laine',
        fullName: 'Ruksi Laine'
    },
    watch: {
        firstName: function(value) {
            this.fullName = value + ' ' + this.lastName;
        },
        lastName: function(value) {
            this.fullName = this.firstName + ' ' + value;
        },
    }
});
<div id="passport">
    <div>{{ fullName }}</div>
    <input v-model="firstName">
    <input v-model="lastName">
</div>

Filters

Sorting is done with orderBy filter applied to v-for. Data filtering is done with filterBy filter applied to v-for.

new Vue({
    el: '#contacts',
    data: {
        searchKey: '',
        sortKey: '',
        sortReversed: -1,
        people: [
            { name: 'John', age: 22 },
            { name: 'Sam', age: 23 },
            { name: 'Aaron', age: 33 },
            { name: 'Eric', age: 37 },
            { name: 'Matt', age: 24 }
        ]
    },
    methods: {
        toggleSort: function(sortKey) {
            if (this.sortKey == sortKey && this.sortReversed < 0) {
                this.sortReversed = 0;
            }
            else {
                this.sortReversed = -1;
            }
            this.sortKey = sortKey;
        }
    }
});
<div id="contacts">
    <input v-model="searchKey">
    <table>
        <thead>
            <th><a href="#" @click="toggleSort('name')">Name</th>
            <th><a href="#" @click="toggleSort('age')">Age</th>
        </thead>
        <tbody>
            <tr v-for="person in people | filterBy searchKey | orderBy sortKey sortReversed">
                <td>{{ person.name }}</td>
                <td>{{ person.age }}</td>
            </tr>
        </tbody>
    </table>
</div>

Piped functions are called filters.

new Vue({
    el: '#mystery-letter',
    data: {
        message: 'Hello World'
    },
    filters: {
        reverse: function(value, wordsOnly) {
            if (wordsOnly == 'trueStr') {
                return value.split(' ').reverse().join(' ');
            }
            return value.split('').reverse().join('');
        }
    }
});
<div id="mystery-letter">
    <h1>{{ message | reverse trueStr }}</h1>
</div>

You can also define global filters if multiple Vues use it.

Vue.filter('reverse', function(value, wordsOnly) {
    if (wordsOnly == 'trueStr') {
        return value.split(' ').reverse().join(' ');
    }
    return value.split('').reverse().join('');
});

Filtering collections is like you would expect it to be...

new Vue({
    el: '#weapon-shop',
    data: {
        weaponType: 'all',
        weapons: [
            { name: 'Katana',    type: 'sword' },
            { name: 'Excalibur', type: 'sword' },
            { name: 'Mjölnir',   type: 'hammer' },
            { name: 'Fat Man',   type: 'bomb' }
        ]
    },
    filters: {
        bySelectedWeaponType: function(weapons) {
            if (this.weaponType == 'all') { return weapons; }
            return weapons.filter(function(weapon) {
                return weapon.type == this.weaponType;
            }.bind(this));
        }
    }
});
<div id="weapon-shop">
    <input v-model="weaponType">
    <div v-for="weapon in weapons | bySelectedWeaponType">
        {{ weapon.name }}
    </div>
</div>

You can also create two-way filters. This allows filters used in input elements to write the value back in the right format.

Vue.filter('currencyDisplay', {
    read: function(value) {
        return '$' + value.toFixed(2)
    },
    write: function(newValue, oldValue) {
        var number =+ newValue.replace(/[^\d.]/g, '')
        return isNaN(number) ? 0 : parseFloat(number.toFixed(2));
    }
});

Components

Component are custom HTML elements created with Vue.

var Book = Vue.extend({
    template: '#book-template',
    data: function() {
        // note that data is a function as it works like a constructor
        return { title: 'Untitled' };
    }
});

new Vue({
    el: '#library',
    components: { 'book': Book }
});
<!-- this can be with the rest of your JS in footer or header -->
<template id="book-template">
    <input v-model="title">
</template>

<div id="library">
    <div><book></book></div>
    <div><book></book></div>
    <div><book></book></div>
<div>

You can define component template in JavaScript or with a query selector. Make sure component template always has a single top level DOM element as the root e.g. div or span.

var Book = Vue.extend({
    template: '#book-template',
    // or
    template: '<input v-model="title">'
});

You can also register components globally.

Vue.component('book', Book);

// this also automatically implies Vue.extend if you pass in an object
Vue.component('book', {
    template: '#book-template'
});

Asynchronous Components

If you have a lot of components that all might not be defined on each page, you can define component as a function that uses callback to create the component.

Vue.component('async-example', function(resolve, reject) {
  setTimeout(function () {
    resolve({ template: '<div>I am async!</div>' });
  }, 1000);
});
new Vue({
    el: '#async-box',
    data: {
        asyncs: [1]
    },
    methods: {
        addOne: function() {
            this.asyncs.push(1);
        }
    }
});
<div id="async-box">
    <div v-for="async in asyncs" track-by="$index">
        <async-example />
    </div>
    <button @click="addOne">Add</button>
</div>

Component Mixins

You can use mixins to provide common behavior to multiple components.

var myMixin = {
    created: function () {
        this.hello();
    },
    methods: {
        hello: function () {
            console.log('hello from mixin!');
        }
    }
};

var Component = Vue.extend({ mixins: [myMixin] });
var component = new Component();
// -> "hello from mixin!"

Global mixins are also possible but should be used sparingly.

Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption;
    if (myOption) {
      console.log(myOption);
    }
  }
});

new Vue({ myOption: 'hello!' });
// -> "hello!"

Literal Properties

props allows registering properties to components. Properties are automatically converted to camel case for easier access through this. Changes made in the parent won't propagate down to children, passed value is threated as literal string.

var Book = Vue.extend({
    template: '#book-template',
    props: ['title']
});

new Vue({
    el: '#library',
    components: { 'book': Book }
});
<template id="book-template">
    <span>{{ title }}</div>
</template>

<div id="library">
    <div><book title="Lord of The Rings"></book></div>
    <div><book title="Silmarillion"></book></div>
    <div><book title="There and Back"></book></div>
<div>

Properties can also have validation.

Vue.components('example', {
    props: {
        name: {
            type: String,
            required: true
        },
        age: Number,
        city: {
            type: String,
            default: 'Turku'
        },
        postCode: {
            type: String,
            validator: function(v) {
                return (v.length == 5);
            },
            coerce: function(v) {
                // applied before setting the value
                return v + '';
            }
        }
    }
});

Dynamic Properties

:property-name syntax allows creating one-way bound properties. Changes on the parent data will be propagated to the children. :property-name.sync makes the binding two-way so changes on the child data will be propagated to the parent. : is shorthand for v-bind.

new Vue({
    el: '#selector',
    data: {
        options: ['one', 'two', 'three', 'four'],
        selectedOption: 'one'
    },
    components: {
        'selector-option': {
            template: '<button @click="select">{{ option }}</button>',
            props: ['option', 'selected-option'],
            methods: {
                select: function() {
                    this.selectedOption = this.option;
                }
            }
        }
    }
});
<div id="selector">
    <div>
        You have selected: {{ selectedOption }}
    </div>
    <div v-for="option in options">
        <selector-option :option="option" :selected-option.sync="selectedOption" />
    </div>
</div>

Custom Events

Vue provides an interface it's own event system which is separate from DOM events:

  • Call $on() or define events to listen.
  • Call $emit() to trigger an event.
  • Call $dispatch() to trigger event that propagates upward to parents.
  • Call $broadcast() to trigger event that propagates downwards to children.
  • Event propagation is stopped after a single receive if true is not returned.
Vue.component('notify-button', {
    template: '<button @click="notify">Happening</button>',
    methods: {
        notify: function() {
            this.$dispatch('something-happened', 'Something!');
        }
    }
});
new Vue({
    el: '#notifier',
    events: {
        'something-happened': function(what) {
            console.log(what);
        }
    }
});
<div id="notifier">
    <notify-button></notify-button>
    <notify-button></notify-button>
    <notify-button></notify-button>
</div>

You can likewise use more familiar @ syntax.

Vue.component('notify-button', {
    template: '<button @click="notify">Happening</button>',
    methods: {
        notify: function() {
            this.$dispatch('something-happened', 'Something!');
        }
    }
});
new Vue({
    el: '#notifier',
    methods: {
        onSomethingHappened: function(what) {
            console.log(what);
        }
    }
});
<div id="notifier">
    <notify-button @something-happened="onSomethingHappened"></notify-button>
    <notify-button @something-happened="onSomethingHappened"></notify-button>
    <notify-button></notify-button>
</div>

References

Properties and events are usually enough but sometimes you need a reference to child and you can use v-ref for that.

Vue.component('user-profile', {
    template: '<span>Profile!</span>'
});
var parent = new Vue({
    el: '#parent',
    methods: {
        logIt: function() {
            console.log(this.$refs.profile);
        }
    }
});
<div id="parent">
  <user-profile v-ref:profile></user-profile>
  <button @click="logIt">Log It</button>
</div>

Slots

Slots are ways to define sections of a template that will be filled by the parent.

By default, <slot> is replaced with the component DOM contents. If nothing is specified, the default value is used provided in the template.

Vue.component('single-slot', {
  template: '<div><slot>Unknown!</slot></div>'
});
new Vue({ el: '#slotter' });
<div id="slotter">
    <single-slot>Hello World!</single-slot>
    <single-slot>Hello Again!</single-slot>
    <single-slot></single-slot>
</div>

Slots can also be named for more control.

Vue.component('multi-slot', {
  template: '<div><slot name="one">1</slot><slot name="two">2</slot></div>'
});
new Vue({ el: '#slotter' });
<div id="slotter">
    <multi-slot>
        <span slot="one">Hello</span>
        <span slot="two">World</span>
    </multi-slot>

    <multi-slot>
        Where does this go? Nowhere as all slots are named!
        <span slot="two">World</span>
    </multi-slot>

    <multi-slot />
</div>

Component Mounting

new Vue({
  el: '#mounter',
  data: {
    currentView: 'home'
  },
  components: {
    home: { template: 'Home!' },
    about: { template: 'About!' },
    links: { template: 'Links!' }
  }
});
<div id="mounter">
    <button @click="currentView = 'home'">Home</button>
    <button @click="currentView = 'about'">About</button>
    <button @click="currentView = 'links'">Links</button>
    <component :is="currentView" />
</div>

If you add keep-alive attribute, the elements are not destroyed on switching.

<div id="mounter">
    <component :is="currentView" keep-alive />
</div>

Components can define activate if they need to do something asynchronous before rendering.

Vue.component('activate-example', {
  activate: function(done) {
    loadDataAsync(function (data) {
      this.someData = data;
      done();
    }.bind(this));
  }
});

Class and Style Bindings

:class allows specifying conditional CSS classes. The conditional can be Vue data object, computed, method, array or literal.

new Vue({
    el: '#classy',
    data: {
        buttonClasses: ['btn', 'btn-primary'],
        isVoted: false
    }
});
<div id="classy">
    <button :class="'btn btn-primary'">1</button>
    <button :class="['btn', 'btn-primary']">2</button>
    <button :class="buttonClasses">3</button>
    <button :class="{ 'voted': isVoted, 'not-voted': !isVoted }">4</button>
</div>

Using computed properties to define classes is especially powerful.

Vue.component('alert', {
    template: '<div :class="cssClasses"><slot>No message given</slot></div>',
    props: ['type'],
    computed: {
        cssClasses: function() {
            var t = this.type;
            return {
                'alert': true,
                'alert-success': t == 'success',
                'alert-error': t == 'error'
            };
        }
    }
});

new Vue({ el: '#alerts' });
<div id="alerts">
    <alert>New notifications available!</alert>
    <alert type="success">Successfully updated your profile.</alert>
    <alert type="error">Removing users is not allowed.</alert>
</div>

Transitions

You can define transition and transition-mode to apply CSS classes. Transitions also work with v-if, v-show and v-for.

  • By default, both components act simultaneously.
  • in-out new component will come in first, then the previous will be removed.
  • out-in the previous will be removed first, then the new one comes in.
<component :is="currentView" transition="fade" transition-mode="out-in" />
.fade-transition {
  transition: opacity .3s ease;
}
.fade-enter,
.fade-leave {
    opacity: 0;
}

You can also define JavaScript hooks for transitions.

Vue.transition('fade', {
  beforeEnter: function (el) {},
  enter: function (el) {},
  afterEnter: function (el) {},
  enterCancelled: function (el) {},

  beforeLeave: function (el) {},
  leave: function (el) {},
  afterLeave: function (el) {},
  leaveCancelled: function (el) {}
})

You can also define transitions as CSS animations.

.bounce-enter {
  animation: bounce-in .5s;
}
.bounce-leave {
  animation: bounce-out .5s;
}
@keyframes bounce-in {
  0% { transform: scale(0); }
  50% { transform: scale(1.5); }
  100% { transform: scale(1); }
}
@keyframes bounce-out {
  0% { transform: scale(1); }
  50% { transform: scale(1.5); }
  100% { transform: scale(0); }
}

You can stagger v-for animations so they don't appear at the same time.

<div v-for="list" transition stagger="100"></div>
// or custom staggering
Vue.transition('fade', {
  stagger: function (index) {
    // increase delay by 50ms for each transitioned item,
    // but limit max delay to 300ms
    return Math.min(300, index * 50);
  }
})

Custom Directives

All HTML attributes that start with v- are called directives.

Vue.directive('log', function() {
    console.log(this);
    // has stuff like el, arg, expression, modifiers, params, etc...
    // never modify any of these, it will break things
});
new Vue({ el: '#logger' });
<div id="logger">
    <div v-log></div>
</div>

Note that the value passed to directive is evaluated.

Vue.directive('log', function(value) {
    console.log(value);
});
new Vue({ el: '#logger' });
<div id="logger">
    <!-- given parameters must be a string or it will be undefined -->
    <!-- or you must define it in the `data` -->
    <div v-logger="'Hello World!'"></div>
    <!-- will log undefined -->
</div>

To get full control of the directive definition, pass an object to the constructor.

Vue.directive('complex', {
    bind: function(value) {
        // fired when bound to the element e.g. creation
        // add event listeners etc.
    },
    update: function(newValue, oldValue) {
        // fires every time the binding value changes
    },
    unbind: function(value) {
        // fired when unbound to the element e.g. destruction
        // remove event listeners etc.
    },
    isLiteral: true,        // passed value is always treated as a string
    acceptStatement: true,  // passed value is always treated as a function
    priority: 1000          // when is the directive bound compared to others
});

AJAX

vue-resource plugin allows creating AJAX requests. You can also use jQuery.

new Vue({
    el: '#guest-book',
    data: {
        newMessage: {
            name: '',
            message: ''
        },
        messages: []
    },
    ready: function() {
        this.fetchMessages();
    },
    methods: {
        fetchMessages: function() {
            this.$http.get('/api/messages', function(messages) {
                this.messages = messages;
            });
        },
        submitMessage: function(e) {
            e.preventDefault();
            this.messages.push(this.newMessage);
            var message = this.newMessage;
            this.newMessage = { name: '', message: '' };
            this.$http.post('api/messages', message);
        }
    }
});
<div id="guest-book">
    <form method="POST" @submit="submitMessage">
        <div>
            <label>Name</label>
            <input name="name" id="name" v-model="newMessage.name">
        </div>
        <div>
            <label>Message</label>
            <input name="message" id="message" v-model="newMessage.message">
        </div>
    </form>
    <div v-for="message in messages">
        <h3>{{ message.name }}</h3>
        <div>{{ message.message }}</div>
    </div>
</div>

vue-resource also allows setting global CSRF token for web frameworks like Laravel and Ruby on Rails.

// You can have this in your header template.
<script>
    Vue.http.headers.common['X-CSRF-TOKEN'] = {{ csrf_token() }}
</script>

Other Plugins

Sources