Vue - JS: A Javascript Framework For Building Single Page Apps (Spas)
Vue - JS: A Javascript Framework For Building Single Page Apps (Spas)
Vue - JS: A Javascript Framework For Building Single Page Apps (Spas)
js
A Javascript framework for building Single Page Apps (SPAs).
Basics
Data Property
Methods
Vue Instance Lifecycle
Computed Property
Watch Property
Directives
Shortcuts
Dynamic Styles
Array Sytax
Vue CLI & Workflows
Development Workflow
Components
Registering Components Locally
Communicating between Components
Parent -> Child
Child -> Parent
Child -> Child
Event Bus
Advanced Components
Slots
Multiple Slots
Dynamic Components
Custom Directives
Passing values to custom directives
Arguments
Modifiers
Registering Directives locally
Filters
Mixins
Animations
Transition
Assigning CSS classes for Transitions
Animation
Onload animation
Custom CSS Classes Names
Transitioning between Multiple Elements
Transition JS Hooks
Multiple Elements Animations
Vue Router
Route Parameters
Query parameters
Nested Routes
Scroll behavior
Navigation Guards
Lazy Loading
Vuex - Better State Management
Install
Store
Getters
Mapping Getters to Properties
Mutations
Actions
Edge with v-model
Modularizing the Store
Basics
A simple Vue.js built in the browser looks like this.
<div id="app">
{{ message }}
</div>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
</script>
We bind the application to a root element, in this case a DIV. Every Vue application starts by
creating a new Vue instance with the Vue function.
A Vue application consists of a root Vue instance created with new Vue , optionally organized
into a tree of nested, reusable components. For example, a todo app’s component tree might look
like this:
Root Instance
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
Data Property
When a Vue instance is created, it adds all the properties found in its data object to Vue’s
reactivity system. When the values of those properties change, the view will “react”, updating to
match the new values.
We use the {{ }} syntax to display dynamic values taken from the data property (see Basics ).
We can also put small JS syntax inside {{ }} .
Methods
<div id="app">
{{ sayHello() }}
</div>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
methods: {
sayHello() {
return 'Hello!';
}
}
});
</script>
<!-- NO -->
<a href="{{ link }}">Link</a>
<a v-bind:href="link">Link</a>
Computed Property
With the computed property we can execute a function only if its relative data gets changed. The
methods are called every time we dynamically refresh the page. For example let's assume we have
some buttons that increase a counter:
<div id="app">
<button v-on:click="counter++">Increase</button>
<button v-on:click="counter--">Decrease</button>
<button v-on:click="secodCounter++">Increase second</button>
<p>Counter: {{ counter }}</p>
<p>Result: {{ result() }} | {{ output }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
counter: 0,
seconCounter: 0
},
computed: {
output() {
return this.counter > 5 ? 'Greater than 5' : 'Smaller than 5'
}
},
methods: {
result() {
return this.counter > 5 ? 'Greater than 5' : 'Smaller than 5'
}
}
});
</script>
The result function gets called every time because Vue doesn't know if it needs to run it or not.
On the other hand the output function gets called only when the counter gets modified.
Computed is aware of the logic inside it. See how we use 'output' as a property and we don't
invoke the function.
Computed properties are by default getter-only, but you can also provide a setter when you need
it:
Watch Property
An object where keys are expressions to watch and values are the corresponding callbacks. The
value can also be a string of a method name, or an Object that contains additional options.
Vue does provide a more generic way to observe and react to data changes on a Vue instance:
watch properties. When you have some data that needs to change based on some other data, it
is tempting to overuse watch owever, it is often a better idea to use a computed property rather
than an imperative watch callback
The watch property lets us react to changes. Computed properties are much more optimized and
they are best practice but they need to run SYNC.
Directives
v-if if statement
v-html outputs raw html data from a value ([!] XSS [!])
Shortcuts
v-on: --> @
v-bind: --> :
Dynamic Styles
The above syntax means the presence of the active class will be determined by the truthiness
of the data property isActive .
You can have multiple classes toggled by having more fields in the object. In addition, the v-
bind:class directive can also co-exist with the plain class attribute. So given the following
template:
<div
class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>
data: {
isActive: true,
hasError: false
}
It will render:
data: {
classObject: {
active: true,
'text-danger': false
}
}
This will render the same result. We can also bind to a computed property that returns an object.
This is a common and powerful pattern:
<div v-bind:class="classObject"></div>
data: {
isActive: true,
error: null
},
computed: {
classObject: function () {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
Array Sytax
We can pass an array to v-bind:class to apply a list of classes:
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
If you would like to also toggle a class in the list conditionally, you can do it with a ternary
expression:
This will always apply errorClass , but will only apply activeClass when isActive is truthy.
This server will pre-compile and then serve the Vue app to the client, this is good for clientside
performance.
All of this is pre-built, we don't need to write our own static server.
Development Workflow
We compile everything server so the app we ship is ready to be viewed. This means that we can
use Single FIled Templates (.vue files).
Components
Re-usable pieces of code that are used in the Vue instance. It's like splitting the app in different
parts based on their role. Components extend the Vue instance.
Vue.component('ComponentName', {
data() {
return {
str: 'Hello from component'
};
}
});
new Vue({
el: '#app',
data: {
str: 'Hello'
}
});
<div id="app">
<ComponentName />
<ComponentName />
<ComponentName />
</div>
We could also have a shared data object by defining it globally but it is better to have it locally
inside every component.
Registering Components Locally
// global
Vue.component('ComponentName', {
data() {
return {
str: 'Hello from global component'
};
}
});
// local
const ComponentName = {
data() {
return {
str: 'Hello from local'
}
}
}
We can of course move components to a separated file and import it like a JS module.
<template>
<p>
{{ str }}
</p>
<ComponentName />
</template>
<script>
import ComponentName from './components/ComponentName.vue';
export default {
components: {
ComponentName
},
data: {
str: 'Hello'
}
}
</script>
We can also organize components by feature instead of having a single components folder.
For transferring data from the parent to the child we use props .
<script>
export default {
props: ['name']
}
</script>
<script>
export default {
data() {
return {
name: 'Leonardo'
}
}
}
</script>
We can also use the props inside methods using the keyword this , just like accessing the data
property
export default {
props : {
name: String,
required: true
}
}
Since passing data from parent to child means passing pointers we use the $emit method.
export default {
resetName() {
this.name = 'Reverted back';
this.$emit('eventName', this.name);
}
}
And then in the parent we listen to this event with the v-on directive.
Method #2
Method #3
Using an Event Bus, which is a separeted Vue instance. We use $emit with this instance
and we listen from events coming from that Bus.
Event Bus
// main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
<script>
import {eventBus} from '../main';
export default {
props: ['userAge'],
methods: {
editAge() {
this.userAge = 30;
eventBus.changeAge(this.userAge);
}
}
}
</script>
<script>
import { eventBus } from '../main';
export default {
props: {
myName: {
type: String
},
resetFn: Function,
userAge: Number
},
methods: {
switchName() {
return this.myName.split("").reverse().join("");
},
resetName() {
this.myName = 'Max';
this.$emit('nameWasReset', this.myName);
}
},
created() {
eventBus.$on('ageWasEdited', (age) => {
this.userAge = age;
});
}
}
</script>
<script>
import UserDetail from './UserDetail.vue';
import UserEdit from './UserEdit.vue';
export default {
data: function () {
return {
name: 'Max',
age: 27
};
},
methods: {
changeName() {
this.name = 'Anna';
},
resetName() {
this.name = 'Max';
}
},
components: {
appUserDetail: UserDetail,
appUserEdit: UserEdit
}
}
</script>
<script>
import User from './components/User.vue';
export default {
components: {
appUser: User
}
}
</script>
Advanced Components
Slots
With slots we can pass content (entire HTML blocks) between components
<script>
import Quote from './components/Quote.vue';
export default {
components: {
Quote
}
}
</script>
<script>
export default {
}
</script>
Multiple Slots
If you want to render the conent multiple times then just place as many <slot> as you want.
<script>
import Quote from './components/Quote.vue';
export default {
components: {
Quote
}
}
</script>
<script>
export default {
}
</script>
If you name 3/4 slots the 4th one is going to be called the "default" one.
Dynamic Components
The <component> element allows us to dynamically add components
<script>
import C1 from './components/C1.vue';
import C2 from './components/C2.vue';
import C3 from './components/C3.vue';
export default {
components: {
C1,
C2,
C3
}
data() {
return {
selectedComponent = 'C1'
}
}
}
</script>
Dynamic Components get destroyed and re-created every time, but we can over write this
behaviour with the <keep-alive> tag around the dynamic <component> element.
We can control this flow with life cycle methods such as activated and deactivated .
Custom Directives
Vue.directive('directive-name', {
bind(el, binding, vnode) {
el.style.backgroundColor = 'green';
}
});
<p v-directive-name>
Hello
</p>
<p v-directive-name="'red'">
Hello
</p>
Arguments
Vue.directive('directive-name', {
bind(el, binding, vnode) {
if (binding.arg == 'background') {
el.style.backgroundColor = binding.value;
} else {
el.style.color = binding.value;
}
}
});
<p v-directive-name:background="'red'">
Hello
</p>
Modifiers
Vue.directive('directive-name', {
bind(el, binding, vnode) {
let delay = 0;
if (binding.modifiers['delayed']) {
delay = 3000;
}
setTimeout(() => {
if (binding.arg == 'background') {
el.style.backgroundColor = binding.value;
} else {
el.style.color = binding.value;
}
}, delay);
}
});
<p v-directive-name:background.delayed="'red'">
Hello
</p>
<script>
export default {
directives: {
'directive-name': {
bind(el, binding, vnode) {
let delay = 0;
if (binding.modifiers['delayed']) {
delay = 3000;
}
setTimeout(() => {
if (binding.arg == 'background') {
el.style.backgroundColor = binding.value;
} else {
el.style.color = binding.value;
}
}, delay);
}
}
}
}
</script>
Filters
A filter is a syntax feature you can use to transform some output in the template, it basically
filters some data. For example, a filter that does toUppercase() . This will happen in the
template only, not in the data() .
// globally
import Vue from 'vue';
// locally
export default {
filters: {
toUppercase(value) {
return value.toUpperCase();
}
}
}
<p>
{{ text | toUppercase }}
</p>
<script>
export default {
data() {
return {
text: 'Hello'
};
},
filters: {
toUppercase(value) {
return value.toUpperCase();
}
}
}
</script>
Mixins
Mixins are useful to avoid duplicate code, often involving computed properties and filters.
We basically move the code we need to an external file where we export an object with those
methods inside. Then we use the mixins property in out Vue instance:
// mixin
const example = {
data() {
return {
text: 'Hello'
}
},
methods: {
printText() {
console.log(this.text);
}
}
};
export default example;
export default {
mixins: [example]
}
IMPORTANT
A mixin gets merged with the already existing properties in the instance.
Components always act last and mixins can't destroy components stuff.
The mixin object is not really shared between components, every component gets his copy of the
mixin.
Animations
We can use animations and transitions when creating, modifying and deleting components.
Transition
<transition>
<p>
Hey!
</p>
</transition>
We usually attach CSS classes to do animations and Vue does this for us.
We animate in an element with *-enter where * is the name of the animation displayed in the
initial state. After this *-enter-active gets attached.
<transition name="fade">
<p>
Hey!
</p>
</transition>
<style>
/* one frame at the beginning and then gets removed */
.fade-enter {
opacity: 0;
}
/* here we set up the transition */
.fade-enter-active {
transition: opacity 1s;
}
.fade-leave {
}
.fade-leave-active {
trabistion: opacity 1s;
opacity: 0;
}
</style>
Animation
<transition name="slide">
<p>
Hey!
</p>
</transition>
<style>
.slide-enter {
}
.slide-enter-active {
animation: slide_in 1s ease-out forwards;
}
.slide-leave {
}
.slide-leave-active {
animaiton: slide_out 1s ease-out forwards;
}
@keyframes slide-in {
from {
transform: translateY(20px);
}
to {
transation: translationY(0);
}
}
@keyframes slide-out {
from {
transform: translateY(0);
}
to {
transation: translationY(20px);
}
}
</style>
If you want to mix animation and transitions make sure to indicate which one determines the
lenght of the effect with the type='' keyword.
Onload animation
We can use the initial attachment to the DOM with the appear attribute on the transition
element.
<transition
enter-class="custom-name"
enter-active-class="custom-name"
leave-class="custom-name"
leave-active-class="custom-name"
></transition>
If we want to transition between elements instead of animating a single one and then removing it
we should add the key attribute to the elements., often we also need to use mode="out-in" .
Transition JS Hooks
The transition element emits some events in a specific moment. The flow is:
If we don't want to use CSS at all we can explicitly tell Vue with :css="false" .
To animate multiple items we have <transition-group> and we always need to key the
elements
<transition-group>
<ul>
<li v-for="(element, index) in numbers" :key="element">
</li>
</ul>
</transition-group>
Vue Router
If we use history mode every action is sent to the server, on the other hand the hash mode
handles everything client-side in the application itself but we will get the # in the URL.
Route Parameters
data() {
return {
parameterName: this.$route.params.parameterName
};
},
watch: {
'$route': (to, from) {
this.parameterName = to.params.id;
}
}
Query parameters
We can obviously use the ? query parameter and retreive it with $route.query.queryName .
Nested Routes
To have nested routes we add the children: [] property to the route object.
And we need to add the <router-view> inside the 'root' route, in this case the /user route.
Scroll behavior
savedPosition is the position on the page where the user was before switching route.
Navigation Guards
Middlewares that we can run before or after entering a route. This is useful especially for
protected toutes.
Inside index.js in the router folder we use router.beforeEach() to run code before
entering a route:
{
path: '/profile',
name: 'Profile',
component: () => import('../views/Profile.vue'),
meta: {
requiresAuth: true
}
}
Example:
Lazy Loading
Whenever we want to load the User component we execute this function, which will load the User
component only when we need it.
Vuex uses a central Store thats holds the application state. This store is available to the
components, which can access it and modify it. In the central state we place all the shared data.
Install
Store
Example of a store.js
Vue.use(Vuex);
Getters
To avoid duplicated code we can use getters to fetch the store from different places.
export default {
computed: {
...mapGetters([
'doubleCounter',
'anotherGetter'
]),
ourOwnComputedProperty() {
// code
}
}
}
Mutations
mutaions: {
increment: state => {
state.counter++;
}
}
Actions
Extra piece of async code in mutations. The mutation gets committed once the action is finished.
(Actions are triggered with the store.dispatch method).
actions: {
incrementAfterTimeout: ({ commit }, payload) => {
setTimeout(commit('increment'), payload.duration);
}
}
Imagine we have an input field and we want to bind the value in real time to a computed property.
We also have a getter in the store that returns it, a mutation that does something with that and
an action that commits the mutation. In order to set the value in the store in real time we have to
use a setter inside the computed property, which is something really rare.
v-model="value"
computed: {
value: {
get() {
return this.$store.getters.value;
},
set(event) {
this.$store.dispatch('actionName', event.target.value); // this is used to
dispatch an action, before it wasn't necessary because we used mapActions
}
}
}
To organize everything we should organize methods in modules . For example we could create a
counter.js module with mutations, getters and actions related to the counter .
The module:
const state = {
counter: 0
};
const getters = {
doubleCounter(state) {
return state.counter * 2;
}
};
const mutations = {
increment: state => {
state.counter++;
}
};
const actions = {
incrementAfterTimeout: ({ commit }, payload) => {
setTimeout(commit('increment'), payload.duration);
}
};
We could also organize everything by technical role instead of dividing stuff in modules.