Vue - The Complete Guide (w/ Router, Vuex, Composition API)
Vue.js (Vue) is a javascript framework that makes building interactive and reactive web frontends (= browser-side web application) easier.
Javascript - A programming language that is supported by all browsers. It allows you to manipulate the already running page and hence provide richer user experiences.
Framework - A framework is a (third-party) library that exposes utility functionalities and a "set of rules" ) a clear guidance) on how to build you javascript application.
Reactive - Your app is able to react to user input, update the screen dynamically ( e.g. to show overlays or input validation errors) Look and feel of mobile apps.
Web frontends - What the user sees - Html + css + javascript, running in the browser of you user.
Example 1.1 - vanila script
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>A First App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<div>
<label for="goal">Goal</label>
<input type="text" id="goal" />
<button>Add Goal</button>
</div>
<ul>
<li>Test</li>
</ul>
</div>
<script src="app.js"></script>
</body>
</html>
const buttonEl = document.querySelector('button');
const inputEl = document.querySelector('input');
const listEl = document.querySelector('ul');
function addGoal() {
const enteredValue = inputEl.value;
const listItemEl = document.createElement('li');
listItemEl.textContent = enteredValue;
listEl.appendChild(listItemEl);
inputEl.value = '';
}
buttonEl.addEventListener('click', addGoal);
Example 1.2 - Using vue
<div id="app">
<div>
<label for="goal">Goal</label>
<input type="text" id="goal" v-model="enteredValue" />
<button v-on:click="addGoal">Add Goal</button>
</div>
<ul>
<li v-for="goal in goals">{{ goal }}</li>
</ul>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script src="app.js"></script>
Vue.createApp({
data () {
return {
goals: [],
enteredValue: ''
}
},
methods: {
addGoal() {
this.goals.push(this.enteredValue);
this.enteredValue = '';
}
}
}).mount('#app');
Tips
data: function() {
}
is equal to
data () {
}
Content
Basic - core syntax, templates, directives, data, methods, computed properties, watchers.
Intermediate - components, component communication, behind the scenes, forms, http, routing, animations
Advanced - vuex, authentication, deployment & optimizations, composition API, re-using code
Creating and connecting vue app instance
<!DOCTYPE html>
...
<section id="user-goal">
<h2>My Course Goal</h2>
<p></p>
</section>
...
</html>
const app = Vue.createApp({
data () {
return {
courseGoal: 'Finish the course and learn Vue!'
}
}
});
app.mount('#user-goal');
Interpolation and Data Binding
<!DOCTYPE html>
...
<section id="user-goal">
<h2>My Course Goal</h2>
<p>{{ courseGoal }}</p>
</section>
...
</html>
Binding Attributes with the "v-bind" Directive
{{ }} (interpolation) works between tags, not used for adding to attributes, example <a href="{{ vueLink }}">Link</a> this wouldn't work so use v-bind example v-bind:href="vueLink"
Understanding "methods" in Vue Apps
data() it self was a function, methods takes an object that will be full of functions.
methods: allows you to define functions which should execute when you call them.
methods: {
outputGoal() {
const randomNumber = Math.random();
if ( randomNumber < 0.5) {
return 'Learn Vue!';
} else {
return 'Master Vue!';
}
}
}
{{ outputGoal() }}
Working with Data inside of a vue app
const app = Vue.createApp({
data () {
return {
courseGoalA: 'Finish the course and learn Vue!',
courseGoalB: 'Master Vue and build amazing apps',
vueLink: 'https://vuejs.org/'
}
},
methods: {
outputGoal() {
const randomNumber = Math.random();
if ( randomNumber < 0.5) {
return this.courseGoalA;
} else {
return this.courseGoalB;
}
}
}
});
app.mount('#user-goal');
Outputting Raw HTML content with V-html
<p v-html="outputGoal()"></p>
Understanding Event Binding
<section id="events">
<h2>Events in Action</h2>
<button v-on:click="counter++" >Add</button>
<button v-on:click="counter--">Reduce</button>
<p>Result: {{ counter }}</p>
</section>
const app = Vue.createApp({
data() {
return {
counter: 0,
};
},
});
app.mount('#events');
Events and Methods
<section id="events">
<h2>Events in Action</h2>
<button v-on:click="add" >Add</button>
<button v-on:click="reduce">Reduce</button>
<p>Result: {{ counter }}</p>
</section>
const app = Vue.createApp({
data() {
return {
counter: 0,
};
},
methods: {
add() {
this.counter = this.counter + 1;
},
reduce() {
this.counter = this.counter - 1;
}
}
});
app.mount('#events');
Working with events arguments
<section id="events">
<h2>Events in Action</h2>
<button v-on:click="add(5)" >Add</button>
<button v-on:click="reduce(5)">Reduce</button>
<p>Result: {{ counter }}</p>
</section>
const app = Vue.createApp({
data() {
return {
counter: 0,
};
},
methods: {
add(num) {
this.counter = this.counter + num;
},
reduce(num) {
this.counter = this.counter - num;
}
}
});
app.mount('#events');
Using the native event object
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue Basics</title>
<link
href="https://fonts.googleapis.com/css2?family=Jost:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css" />
<script src="https://unpkg.com/vue@next" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<header>
<h1>Vue Events</h1>
</header>
<section id="events">
<h2>Events in Action</h2>
<button v-on:click="add(5)" >Add</button>
<button v-on:click="reduce(5)">Reduce</button>
<p>Result: {{ counter }}</p>
<input type="text" v-on:input="setName($event, 'madindo')">
<p>Your Name: {{ name }}</p>
</section>
</body>
</html>
const app = Vue.createApp({
data() {
return {
counter: 0,
name: ''
};
},
methods: {
setName(event) {
this.name = event.target.value ;
},
add(num) {
this.counter = this.counter + num;
},
reduce(num) {
this.counter = this.counter - num;
}
}
});
app.mount('#events');
Exploring event modifiers
Main key points... in v-on:keyup.enter, v-on:click.right or left, v-on:submit.prevent
<section id="events">
<h2>Events in Action</h2>
<button v-on:click="add(5)" >Add</button>
<button v-on:click.right="reduce(5)">Reduce</button>
<p>Result: {{ counter }}</p>
<input type="text" v-on:input="setName($event, 'madindo')" v-on:keyup.enter="confirmInput()">
<p>Your Name: {{ confirmedName }}</p>
<form v-on:submit.prevent="submitForm">
<input type="text">
<button>Sign Up</button>
</form>
</section>
const app = Vue.createApp({
data() {
return {
counter: 0,
name: '',
confirmedName: ''
};
},
methods: {
confirmInput() {
this.confirmedName = this.name;
},
submitForm(event) {
event.preventDefault();
alert('submited');
},
setName(event) {
this.name = event.target.value ;
},
add(num) {
this.counter = this.counter + num;
},
reduce(num) {
this.counter = this.counter - num;
}
}
});
app.mount('#events');
Locking Content with V-once
<p v-once>Starting Counter : {{ counter }}</p>
<p>Result: {{ counter }}</p>
Data Binding + Event Binding = Two-way Binding
v-bind:value="name" v-on:input="setName($event, 'madindo')" v-on:keyup.enter="confirmInput()"
is equal to
v-model="name"
Introduction to computed properties
Let's say we have a function in method
method: {
fullname() {
if (this.name === '') {
return '';
}
return this.name + ' ' + 'madindo';
}
},
And
computed: {
fullname() {
if (this.name === '') {
return '';
}
return this.name + ' ' + 'madindo';
}
},
The difference between computed and methods is that when you define a function in computed, it executes the function from the beginning only if the answer changes, but methods executes the function from the beginning every time it is called.
Working with watchers
watch: {
name(value) {
if (value === '') {
this.fullname = '';
} else {
this.fullname = value + ' ' + this.lastName;
}
},
lastName(value) {
if (value === '') {
this.fullname = '';
} else {
this.fullname = this.name + ' ' + value;
}
}
},
too many code, it's better to use computed properties
fullname() {
if (this.name === '' || this.lastName === '') {
return '';
}
return this.name + ' ' + this.lastName;
}
example good use of watchers
watch: {
counter(value) {
if (value > 50) {
this.counter = 0;
}
}
}
Methods vs computed vs watch
Methods
- Use with event binding OR data binding
- Data binding : method is exxecuted for every "re-render" cycle of the component
- Use for events or data that really needs to be re-evaluated all the time.
Computed
- Use with data binding
- Computed properties are only re-evaluated if one of their "used values" changed
- Use for data that depends on other data
Watch
- Not use directly in template
- Allows you to run any code in reaction to some changed data (e.g send Http request etc)
- Use for any non-data update you want to make
V-bind and v-on shorthands
v-on:click = @click , v-bind:value = :value
Dynamic styling with inline styles
<section id="styling">
<div class="demo" :style="{borderColor: boxASelected ? 'red' : '#ccc'}" @click="boxSelected('A')"></div>
<div class="demo" @click="boxSelected('B')"></div>
<div class="demo" @click="boxSelected('C')"></div>
</section>
const app = Vue.createApp({
data() {
return {
boxASelected: false,
boxBSelected: false,
boxCSelected: false,
}
},
methods: {
boxSelected(box) {
if (box === 'A') {
this.boxASelected = true;
} else if (box === 'B') {
this.boxBSelected = true;
} else if (box === 'C') {
this.boxCSelected = true;
}
}
}
});
app.mount('#styling');
Adding Css Classes dynamically
<section id="styling">
<div class="demo" :class="{active: boxASelected}" @click="boxSelected('A')"></div>
<div class="demo" :class="{active: boxBSelected}" @click="boxSelected('B')"></div>
<div class="demo" :class="{active: boxCSelected}" @click="boxSelected('C')"></div>
</section>
const app = Vue.createApp({
data() {
return {
boxASelected: false,
boxBSelected: false,
boxCSelected: false,
}
},
methods: {
boxSelected(box) {
if (box === 'A') {
this.boxASelected = !this.boxASelected;
} else if (box === 'B') {
this.boxBSelected = !this.boxBSelected;
} else if (box === 'C') {
this.boxCSelected = !this.boxCSelected;
}
}
}
});
app.mount('#styling');
Classes & Computed Properties
<section id="styling">
<div class="demo" :class="boxAClasses" @click="boxSelected('A')"></div>
<div class="demo" :class="boxBClasses" @click="boxSelected('B')"></div>
<div class="demo" :class="boxCClasses" @click="boxSelected('C')"></div>
</section>
const app = Vue.createApp({
data() {
return {
boxASelected: false,
boxBSelected: false,
boxCSelected: false,
}
},
computed: {
boxAClasses() {
return { active: this.boxASelected };
},
boxBClasses() {
return { active: this.boxBSelected };
},
boxCClasses() {
return { active: this.boxCSelected };
}
},
methods: {
boxSelected(box) {
if (box === 'A') {
this.boxASelected = !this.boxASelected;
} else if (box === 'B') {
this.boxBSelected = !this.boxBSelected;
} else if (box === 'C') {
this.boxCSelected = !this.boxCSelected;
}
}
}
});
app.mount('#styling');
Module summary
Dom & template
- Vue can be used to define the goal instead of the steps (-> declarative approach)
- Connect vue to html via "mount": Vue then renders the real DOM based on the connected template.
Data & Event Bindings
- You can bind data via interpolation ({{}}) or the v-bind (":") directive
- You listen for events via v-on ("@")
Reactivity
- Vue updates the real DOM for you when bound data changes.
- Computed properties and a=watchers allow you to react to data changes.
Styling
- Dynamic CSS class and inline style bindings are supported by vue.
- Vue offers multple special syntaxes (object based, array based) for efficient bindings.
Rendering Conditionally - v-if v-else
const app = Vue.createApp({
data() {
return {
enteredGoalValue: '',
goals: []
};
},
methods: {
addGoal(){
this.goals.push(this.enteredGoalValue);
}
}
});
app.mount('#user-goals');
if goals is not empty do not show the message in string
<section id="user-goals">
<h2>My course goals</h2>
<input type="text" v-model="enteredGoalValue" />
<button @click="addGoal">Add Goal</button>
<p v-if="goals.length === 0">No goals have been added yet - please start adding some!</p>
<ul v-else>
<li>Goal</li>
</ul>
</section>
Using V-if or V-show
The difference is if V condition is false, using v-if will be gone but with v-show the html elements are still there but it's hidden with css.
Rendering List Data
<ul v-else>
<li v-for="goal in goals">{{goal}}</li>
</ul>
Diving Deeper into v-for
<ul v-else>
<li v-for="(goal, index) in goals">{{goal}} - {{index}}</li>
</ul>
<ul>
<li v-for="(value, key, index) in {name: 'madindo', age:18}">key: {{ key }} value: {{ value }} index: {{ index }}</li>
</ul>
<ul>
<li v-for="num in 10">{{num}}</li>
</ul>
Removing list items
<ul v-else>
<li v-for="(goal, index) in goals" @click="removeGoal(index)">{{goal}} - {{index}}</li>
</ul>
methods: {
addGoal(){
this.goals.push(this.enteredGoalValue);
},
removeGoal(idx) {
this.goals.splice(idx, 1);
}
}
List & Keys
<ul v-else>
<li v-for="(goal, index) in goals" :key="goal" @click="removeGoal(index)">
<p>{{goal}} - {{index}}</p>
<inpput type="text" @click.stop />
</li>
</ul>
Download - Full working code
Course Project : The monster slayer game
Html
<div id="game">
<section id="monster" class="container">
<h2>Monster Health</h2>
<div class="healthbar">
<div class="healthbar__value" :style="monsterBarStyles"></div>
</div>
</section>
<section id="player" class="container">
<h2>Your Health</h2>
<div class="healthbar">
<div class="healthbar__value" :style="playerBarStyles"></div>
</div>
</section>
<section class="container" v-if="winner">
<h2>Game Over!</h2>
<h3 v-if="winner === 'monster'">You lost!</h3>
<h3 v-else-if="winner === 'player'">You won!</h3>
<h3 v-else="winner === 'draw'">It's a draw!</h3>
<button @click="startGame">Start New Game</button>
</section>
<section id="controls" v-else>
<button @click="attackMonster">ATTACK</button>
<button :disabled="mayUsesSpecialAttack" @click="specialAttackMonster">SPECIAL ATTACK</button>
<button @click="healPlayer">HEAL</button>
<button @click="surrender">SURRENDER</button>
</section>
<section id="log" class="container">
<h2>Battle Log</h2>
<ul>
<li v-for="logMessage in logMessages">
<span :class="{'log--player': logMessage.actionBy === 'player', 'log--monster': logMessage.actionBy === 'monster'}">{{ logMessage.actionBy === 'player' ? 'Player' : 'Monster' }}</span>
<span v-if="logMessage.actionType === 'heal'"> heals himself</span><span class="log--heal">{{ logMessage.actionValue }}</span>
<span v-else="logMessage.actionType === 'attack' || logMessage.actionType === 'special-attack"> attacks and deal</span><span class="log--damage">{{ logMessage.actionValue }}</span>
</li>
</ul>
</section>
</div>
App.js
function getRandomValue(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
const app = Vue.createApp({
data() {
return {
playerHealth: 100,
monsterHealth: 100,
currentRound: 0,
winner: null,
logMessages: []
}
},
computed: {
monsterBarStyles() {
if (this.monsterHealth < 0) {
return { width: '0%' };
}
return { width: this.monsterHealth + '%' }
},
playerBarStyles() {
if (this.playerHealth < 0) {
return { width: '0%' };
}
return { width: this.playerHealth + '%' }
},
mayUsesSpecialAttack() {
return this.currentRound % 3 !== 0;
}
},
watch: {
playerHealth(value) {
if (value <= 0 && this.monsterHealth <= 0) {
// draw
this.winner = 'draw';
} else if (value <= 0) {
// lose
this.winner = 'monster';
}
},
monsterHealth(value) {
if (value <= 0 && this.playerHealth <= 0) {
// draw
this.winner = 'draw';
} else if (value <= 0) {
// lose
this.winner = 'player';
}
}
},
methods: {
startGame() {
this.playerHealth = 100;
this.monsterHealth = 100;
this.winner = null;
this.currentRound = 0;
this.logMessages = [];
},
surrender() {
this.winner = 'monster';
},
attackMonster() {
this.currentRound++;
const attackValue = getRandomValue(5,12);
this.monsterHealth -= attackValue;
this.addLogMessage('player', 'attack', attackValue);
this.attackPlayer();
},
attackPlayer() {
const attackValue = getRandomValue(8,15);
this.playerHealth -= attackValue;
this.addLogMessage('monster', 'attack', attackValue);
},
specialAttackMonster() {
this.currentRound++;
const attackValue = getRandomValue(10,25);
this.monsterHealth -= attackValue;
this.addLogMessage('player', 'special-attack', attackValue);
this.attackPlayer();
},
healPlayer() {
this.currentRound++;
const healValue = getRandomValue(8, 20);
if (this.playerHealth + healValue > 100) {
this.playerHealth = 100;
} else {
this.playerHealth += healValue;
}
this.addLogMessage('player', 'heal', healValue);
this.attackPlayer();
},
addLogMessage(who, what, value) {
this.logMessages.unshift({
actionBy: who,
actionType: what,
actionValue: value
});
}
}
});
app.mount('#game');
What I learned from this is watch, if the data playerHealth then you can set watch playerHealth(value) to do something with the value.
Vue behind the scene
Example of vanila js reactive
const data = {
message: 'Hello!',
longMessage: 'Hello! World!'
};
const handler = {
set(target, key, value) {
if (key === 'message') {
target.longMessage = value + 'World!';
}
target.message = value;
}
};
const proxy = new Proxy(data, handler);
proxy.message = 'Hello!!!';
console.log(proxy.longMessage);
Multiple apps
<section id="app">
<h2>How Vue Works</h2>
<input type="text" @input="saveInput">
<button @click="setText">Set Text</button>
<p>{{ message }}</p>
</section>
<section id="app2">
<p>{{favoriteMeal}}</p>
</section>
const app2 = Vue.createApp({
data() {
return {
favoriteMeal: 'Pizza'
}
}
});
app2.mount('#app2');
Templates
const app2 = Vue.createApp({
template: `
<p>{{ favoriteMeal }}</p>
`,
data() {
return {
favoriteMeal: 'Pizza'
}
}
});
app2.mount('#app2');
Working with Refs
<input type="text" ref="userText">
methods: {
setText() {
this.message = this.$refs.userText.value;
},
},
How vue updates the DOM
Vue instance -
- stores data, computed properties, methods.
- title: 'hello', text: 'not the title'
- Data and computed properties may change (e.g because of user input)
- title: 'hi there', text: 'not the title'
Browser DOM -
- vue-controlled templates is rendered in the DOM
- <h2>hello</h2><p>Not the title</p>
- Updates should reflected, but Vue should not re-render everything.
- <h2>Hi there</h2><p>Not the title</p>
Virtual DOM
- Js-based DOM which exists only in memory
- {el: 'h2', child: 'hello' }
- -
- Updates are made to the virtual DOM first, only differences are then rendered to the real DOM
Vue Instance Lifecycle
- createApp({...})
- beforeCreate() // called before create app
- created() // after createApp
- -> Compile template
- beforeMount() // right before we see something on screen
- mounted() // now we see something on screen
- Mounted Vue Instance
- Data Changed
- beforeUpdate() // before update has fully processed
- updated() // when that has been processed
- Instance Unmounted // instance can be unmounted all content remove from the screen
- beforeUnmount() // just before instance unmounted
- unmounted() // after instance unmounted
const app = Vue.createApp({
methods: {
},
beforeCreate() {
console.log('beforeCreate()');
},
created() {
console.log('created()');
},
beforeMount() {
console.log('beforeMount()');
},
mounted() {
console.log('mounted()');
},
beforeUpdate() {
console.log('beforeUpdate()');
},
updated() {
console.log('updated()');
},
beforeUnmount() {
console.log('beforeUnmount()');
},
unmount() {
console.log('unmount()');
}
});
app.mount('#app');
setTimeout(function() {
app.unmount();
}, 3000);