The official documentation of Vue is nice, but it is very thorough and, so, ends up complicating a bit something that is very simple in essence. I'll try to explain how to get the hang of it mostly with code, so I hope this can be of use.
I'll skip the installation part, the examples below can work in a "packed" environment or simply putting the JS file wrapped in a <script>
tag.
So you got a template file in HTML:
<div id="app">Hello {{ person }}!</div>
And a "controller" in JS (they don't call it like that, but anyways):
const app = new Vue({
el: '#app',
data: { person: 'world' }
});
So it's just like that. Let's find out all the basics and some more in a nutshell.
We got a bunch of options to deal with the logic of our apps here.
const app = new Vue({
el: '#app', // el: element to render on, pretty straight-forward
// data: local state to be sent to template (any format)
// (the defaults are supposed to be here at the beginning)
data: {
person: 'nobody',
enabled: false,
otherValue: []
},
// methods: inner methods to send to template, can use "this" to refer to data
methods: {
changeToJohn: function () { this.person = 'John'; }
// DON'T use arrow functions in this case like:
// changeToCarl: () => { this.person = 'Carl'; } (the "this" is not gonna work here)
// instead, use ESNext Object functions:
changeToPaul() { this.person = 'Paul'; },
// obviously, they can have parameters:
changeToAnyone(name) { this.person = name; },
},
// used to pre-process values from local state (data) to the template
computed: {
// same as above, use "function" or Object functions:
personInCaps() { return this.person.toUpperCase(); }
},
// lifecycle methods: beforeCreate, created, beforeMount, mounted, beforeUpdate, updated,
// beforeDestroy, destroyed
updated() { console.log(this.person); }
});
Note we can also do like:
const app = new Vue({ el: '#app' });
app.data = { person: 'nobody' };
app.updated = function() { /* ... */ }
And we even have a class syntax that resembles a React component with observables (like with MobX).
In the template side:
<!-- Plain text variable (data or computed) binding: -->
<span v-text="person"></span>
<!-- Plain variable insertion (data or computed) among other text: -->
<span>Hello {{ person }}!</span>
<!-- Expressions are possible here (although not recommended, as this is the view) -->
<span>Two is {{ 1 + 1 }}</span>
<!-- HTML variable (data or computed): name of the variable -->
<span v-html="person"></span>
<!-- Attribute binding to a variable (data or computed) -->
<span v-bind:title="tooltip"></span><!-- OR --><span :title="tooltip"></span>
<!-- Class attribute binding can be array of classes -->
<span :class="[classA, classB]"></span>
<!-- Class attribute binding can be object, whose value is a boolean (to de/activate class) -->
<span :class="{ red: isRed }"></span>
<!-- One-time binding (won't be updated after first time): -->
<span v-once>Hello {{ person }}!</span>
<!-- Not meant to be compiled (for code excerpts, for example) -->
<span v-pre>{{ this will not be compiled }}</span>
<!-- Call method on event without arguments -->
<span v-on:click="changeToJohn"></span><!-- OR --><span @click="changeToJohn"></span>
<!-- Call method on event with arguments (which can include the event) -->
<span @click="changeToAnyone('Jack', $event)"></span>
<!-- Call method on event with event's preventDefault and stopPropagation -->
<span @click.stop.prevent="changeToJohn"></span>
<!-- Call method on event just once -->
<span @click.once="changeToJohn"></span>
<!-- Call method with key event and specific key (works with aliases or numbers) -->
<input @keyup.enter="onEnter"><!-- OR --><input @keyup.13="onEnter">
<!-- Call methods for different events on the same element -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
<!-- conditional rendering -->
<span v-if="shouldShow1">1</span><span v-else="shouldShow2">2</span>
<!-- conditional hiding (just hides with CSS) -->
<span v-show="shouldShow1">1</span>
<!-- loop through arrays and objects -->
<li v-for="item in items">{{ item.text }}</li>
<!-- Two-way binding (with data) for input, select and textarea elements -->
<!--
Note: for immutable situations (VueX) using v-model is not recommended
But it still can be done with a computed model with a getter/setter pair
https://vuex.vuejs.org/guide/forms.html#two-way-computed-property
-->
<input v-model="name" placeholder="Your name">
Lastly on Vue section, components are little brothers of Vue instances designed to be a support for separated parts of the apps, but with most of the functionality of big brother instances:
Vue.component('button-counter', {
props: ['initial']
data: function () {
return {
count: this.initial || 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});
You need to declare the name of the component (to be referenced in HTML template, in this case <button-counter>
). Just like in React, props refer to the attributes of the component (like in <button-counter initial="0">
). The data needs to be enclosed in a function, so that different instances of the component may have independent data. Also, the template can be internally or externally declared, even with a render
property instead of template
, with JSX like a React component.
Components can be globally exposed (like the example above) or registered only locally, like in the example below. Note that, for this case, components can be defined as simple objects:
const MyComponent = { /* ... */ };
new Vue({ el: '#app', components: { 'my-component': MyComponent } });
Vue also has one concept that comes from Angular, which is the concept of directives. Directives are basically a special type of components for purer DOM handling. This handling can be done in other forms on normal components, so in ordinary situations they are unlikely to be used.
VueX is a state management tool that follows the Flux pattern (used in the famous redux, for example). It attaches to a Vue app a (usually global) state, that is bound to be changed only through mutation functions, without direct assignment to data (direct mutations).
We initialise the VueX instance with a store. This store will contain the default state in state
, mutations in mutations
and actions
, and optionally value preprocessors in getters
(more on all those later).
const store = new Vuex.Store({
state: { count: 0 }, // default state
mutations: { /* ... */ }, // simple mutations to the state
actions: { /* ... */ }, // groups of mutations or async mutations
getters: { /* ... */ } // pre-processing functions for state values
});
To get the state into components use computed
, either with this.$store
or through mapState
like below:
import { mapState } from 'vuex';
const app = new Vue({ el: '#app', store, components: { Counter } });
// sample component referenced above
const Counter = {
template: `<div>{{ count }}</div>`,
computed: mapState(['count'])
};
The arguments for mapState
can be arrays of values or objects with functions, like below:
/* ... (import and app like above) */
const Counter = {
template: `<div>{{ count }}</div>`,
computed: mapState({
counterAlias: 'count',
countPlusOne: state => state.count + 1
})
};
Mutations are synchronous functions that get the previous state and return a new state, like in the simple example below:
const store = new Vuex.Store({
state: { count: 0 },
mutations: {
increment (state) {
state.count++
}
}
});
From this point we can issue on our app store.commit('increment')
and the counter will be increased (see the basic example). If the function above had a second argument like increment (state, payload) {
we could call the commit function with this argument, that would be issued to the function, like for example:
store.commit('increment', { amount: 2 });
// or, alternatively...
store.commit({ type: 'increment', amount: 2 });
To get the mutations into the components' methods, we should use mapMutations
like below:
import { mapMutations } from 'vuex';
// ... (app)
const Counter = {
template: '<button @click="increment"></button><button @click="add(2)"></button>',
methods: {
...mapMutations(['increment']), // array of references, or...
...mapMutations({ add: 'incrementBy' }) // ...object with keys as aliases
}
};
One very important thing (also valid for the internal state of Vue instance and components) is to take care when updating objects, not adding new properties to it by normal assign, like state.object.new = true
. Instead, do one of those:
state.object = { ...state.object, new: true}
// or, alternatively use the Vue.set helper:
Vue.set(state.object, 'new', true);
Actions are, in basic terms, a wrapper of mutations that may be grouped to be issued at the same time or be scheduled to be issued after an event, as for example an asynchronous call. The action handler receives a context object with the commit
method to issue mutations. See the example below:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
The context
object above contains helpers like commit
, state
and even dispatch
, which can call other actions. By the way, we call actions through dispatch
, like in store.dispatch('increment')
. As said, async actions can only be actions, not mutations:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000)
}
}
Just like as it was with state and mutations, we can map actions to be exposed to our component with mapActions
:
import { mapActions } from 'vuex';
// ... (app)
const Counter = {
template: '<button @click="increment"></button><button @click="add(2)"></button>',
methods: {
...mapActions(['increment']), // array of references, or...
...mapActions({ add: 'incrementBy' }) // ...object with keys as aliases
}
};
There are two other concepts in VueX that may be of less importance than those ones presented, but may also be handy. The getters are as to the state the equivalent of computed
to data
in normal Vue components, what means that they are pre-processors for the state data. They should be populated on the computed
property, like state, and there is the mapGetters
method to bring those into the component.
The modules consist in featuring many different stores in the same app or component, each with its own state, getters, mutations and actions. They are good for finer namespacing and separation of stores, for any reason that may be important (in a bigger project, there may be many).
VueX also features a plugin scheme and a strict mode, to disallow any mutation outside of the mutation functions.