Ebin - Pub Become A Ninja With Vue 2023-05-19nbsped
Ebin - Pub Become A Ninja With Vue 2023-05-19nbsped
1. Introduction
2.3. Constants
2.4. Shorthands in object creation
2.5. Destructuring assignment
2.6. Default parameters and values
4.4. Interfaces
4.5. Optional arguments
4.6. Functions as property
4.7. Classes
4.8. Working with other libraries
5. Advanced TypeScript
5.1. readonly
5.2. keyof
5.3. Mapped type
6.4. Template
6.5. Frameworks on top of Web Components
8.5. create-vue
8.6. Single File Components
9.1. Interpolation
9.2. Using other components in our templates
10. Directives
12.5. PostCSS
15.3. defineProps
15.4. defineEmits
15.5. defineOptions
16.4. @vue/test-utils
16.5. Snapshot testing
16.7. Cypress
17.3. Interceptors
17.4. Tests
18. Slots
19. Suspense
20. Router
20.1. En route
20.2. Navigation
20.3. Parameters
21. Lazy-loading
21.1. Async components
24.4. Pinia
24.5. Testing Pinia
24.6. Why use a store?
28. Internationalization
28.1. vue-i18n setup
28.2. Translating text
30. Performances
30.1. First load
30.7. Compression
30.8. Lazy-loading
30.9. Server side rendering
30.13. v-memo
30.14. Conclusion
31. This is the end
Appendix A: Changelog
A.1. v3.3.0 - 2023-05-12
A.2. v3.2.45 - 2023-01-05
A.3. v3.2.37 - 2022-07-06
If you did not buy the “Pro package” (but really you
should), don’t worry: you’ll learn everything that’s
needed. But you will not build this awesome
application with beautiful ponies in pixel art. Your
loss 😊!
The ebook is using Vue version 3.3.2 for the examples.
2. A GENTLE
INTRODUCTION TO
ECMASCRIPT 2015+
If you’re reading this, we can be pretty sure you
have heard of JavaScript. What we call JS is one
implementation of a standard specification, called
ECMAScript. The spec version you know the most
about is version 5, that has been used these last
years.
2.1. Transpilers
The sixth version of the specification reached its
final state in 2015. So it’s now supported by modern
browsers, but there are still browsers in the wild
that don’t support it yet, or only support it partially.
And of course, now that we have a new specification
every year (ES2016, ES2017, etc.), some browsers
will always be late. You might be thinking: what’s the
point of all this, if I need to be careful on what I can
use? And you’d be right, because there aren’t that
many apps that can afford to ignore older browsers.
But, since virtually every JS developer who has tried
ES2015+ wants to write ES2015+ apps, the
community has found a solution: a transpiler.
2.2. let
If you have been writing JS for some time, you know
that the var declaration is tricky. In pretty much any
other language, a variable is declared where the
declaration is done. But in JS, there is a concept,
called “hoisting”, which actually declares a variable
at the top of the function, even if you declared it
later.
function getPonyFullName(pony) {
if (pony.isChampion) {
var name = 'Champion ' + pony.name;
return name;
}
return pony.name;
}
function getPonyFullName(pony) {
var name;
if (pony.isChampion) {
name = 'Champion ' + pony.name;
return name;
}
// name is still accessible here
return pony.name;
}
ES2015 introduces a new keyword for variable
declaration, let, behaving much more like what you
would expect:
function getPonyFullName(pony) {
if (pony.isChampion) {
let name = 'Champion ' + pony.name;
return name;
}
// name is not accessible here
return pony.name;
}
2.3. Constants
Since we are on the topic of new keywords and
variables, there is another one that can be of
interest. ES2015 introduces const to declare…
constants! When you declare a variable with const, it
has to be initialized, and you can’t assign another
value later.
const poniesInRace = 6;
poniesInRace = 7; // SyntaxError
As for variables declared with let, constants are not
hoisted and are only declared at the block level.
Example:
function createPony() {
const name = 'Rainbow Dash';
const color = 'blue';
return { name: name, color: color };
}
function createPony() {
const name = 'Rainbow Dash';
const color = 'blue';
return { name, color };
}
function createPony() {
return {
run: () => {
console.log('Run!');
}
};
}
function createPony() {
return {
run() {
console.log('Run!');
}
};
}
function randomPonyInRace() {
const pony = { name: 'Rainbow Dash' };
const position = 2;
// ...
return { pony, position };
}
function randomPonyInRace() {
const pony = { name: 'Rainbow Dash' };
const position = 2;
// ...
return { pony, position };
}
getPonies(20, 2);
getPonies(); // same as getPonies(10, 1);
getPonies(15); // same as getPonies(15, 1);
function addPonies(ponies) {
for (var i = 0; i < arguments.length; i++) {
poniesInRace.push(arguments[i]);
}
}
function addPonies(...ponies) {
for (const pony of ponies) {
poniesInRace.push(pony);
}
}
2.8. Classes
One of the most emblematic new features: ES2015
introduces classes to JavaScript! You can now easily
use classes and inheritance in JavaScript. You always
could, using prototypal inheritance, but that was not
an easy task, especially for beginners.
class Pony {
constructor(color) {
this.color = color;
}
toString() {
return `${this.color} pony`;
// see that? It is another cool feature of ES2015, called
template literals
// we'll talk about these quickly!
}
}
class Pony {
static defaultSpeed() {
return 10;
}
}
class Pony {
get color() {
console.log('get color');
return this._color;
}
set color(newColor) {
console.log(`set color ${newColor}`);
this._color = newColor;
}
}
const pony = new Pony();
pony.color = 'red';
// 'set color red'
console.log(pony.color);
// 'get color'
// 'red'
class Animal {
speed() {
return 10;
}
}
class Pony extends Animal {
speed() {
return super.speed() + 10;
}
}
const pony = new Pony();
console.log(pony.speed()); // 20, as Pony overrides the parent
method
class Animal {
constructor(speed) {
this.speed = speed;
}
}
class Pony extends Animal {
constructor(speed, color) {
super(speed);
this.color = color;
}
}
const pony = new Pony(20, 'blue');
console.log(pony.speed); // 20
2.9. Promises
Promises are not so new, and you might know them
or use them already, as they were available via
third-party libraries. But since you will use them a
lot in Vue, and even if you’re just using JS, I think it’s
important to make a stop.
With callbacks:
getUser(login)
.then(function (user) {
return getRights(user);
})
.then(function (rights) {
updateMenu(rights);
})
getUser(login)
.then(function (user) {
console.log(user);
})
getUser(login)
.then(function (user) {
return getRights(user) // getRights is returning a promise
.then(function (rights) {
return updateMenu(rights);
});
})
getUser(login)
.then(function (user) {
return getRights(user); // getRights is returning a promise
})
.then(function (rights) {
return updateMenu(rights);
})
getUser(login)
.then(
function (user) {
return getRights(user);
},
function (error) {
console.log(error); // will be called if getUser fails
return Promise.reject(error);
}
)
.then(
function (rights) {
return updateMenu(rights);
},
function (error) {
console.log(error); // will be called if getRights fails
return Promise.reject(error);
}
)
getUser(login)
.then(function (user) {
return getRights(user);
})
.then(function (rights) {
return updateMenu(rights);
})
.catch(function (error) {
console.log(error); // will be called if getUser or
getRights fails
})
getUser(login)
.then(function (user) {
return getRights(user); // getRights is returning a promise
})
.then(function (rights) {
return updateMenu(rights);
})
getUser(login)
.then(user => getRights(user))
.then(rights => updateMenu(rights))
getUser(login)
.then(user => {
console.log(user);
return getRights(user);
})
.then(rights => updateMenu(rights))
In ES5:
var maxFinder = {
max: 0,
find: function (numbers) {
// let's iterate
numbers.forEach(function (element) {
// if the element is greater, set it as the max
if (element > this.max) {
this.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
var maxFinder = {
max: 0,
find: function (numbers) {
var self = this;
numbers.forEach(function (element) {
if (element > self.max) {
self.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
var maxFinder = {
max: 0,
find: function (numbers) {
numbers.forEach(
function (element) {
if (element > this.max) {
this.max = element;
}
}.bind(this)
);
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
var maxFinder = {
max: 0,
find: function (numbers) {
numbers.forEach(function (element) {
if (element > this.max) {
this.max = element;
}
}, this);
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
const maxFinder = {
max: 0,
find: function (numbers) {
numbers.forEach(element => {
if (element > this.max) {
this.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
2.11. Async/await
We were talking about promises earlier, and it’s
worth knowing that another keyword was
introduced to handle them more synchronously:
await.
2.14. Modules
A standard way to organize functions in namespaces
and to dynamically load code in JS has always been
lacking. Node.js has been one of the leaders in this,
with a thriving ecosystem of modules using the
CommonJS convention. On the browser side, there is
also the AMD (Asynchronous Module Definition) API,
used by RequireJS. But none of these were a real
standard, thus leading to endless discussions on
what’s best.
In races.service.js:
In another file:
// later
startRace(race);
// pony.js
export default class Pony {}
// races.service.js
import Pony from './pony';
2.15. Conclusion
That ends our gentle introduction to ES2015+. We
skipped some other parts, but if you’re comfortable
with this chapter, you will have no problem writing
your apps in ES2015+. If you want to have a deeper
understanding of this, I highly recommend
Exploring JS by Axel Rauschmayer or Understanding
ES6 from Nicholas C. Zakas… Both ebooks can be
read online for free, but don’t forget to buy it to
support their authors. They have done great work!
Actually I’ve re-read Speaking JS, Axel’s previous
book, and I again learned a few things, so if you
want to refresh your JS skills, I definitely
recommend it!
3. GOING FURTHER THAN
ES2015+
The type can also come from your app, as with the
following class Pony:
4.2. Enums
TypeScript also offers enum. For example, a race in
our app can be either ready, started or done.
enum RaceStatus {
Ready,
Started,
Done
}
enum Medal {
Gold = 1,
Silver,
Bronze
}
interface HasScore {
score: number;
}
interface PonyModel {
name: string;
speed: number;
}
const pony: PonyModel = { name: 'Light Shoe', speed: 56 };
interface CanRun {
run(meters: number): void;
}
const ponyOne = {
run: (meters: number) => logger.log(`pony runs ${meters}m`)
};
startRunning(ponyOne);
4.7. Classes
A class can implement an interface. For us, the Pony
class should be able to run, so we can write:
eat() {
logger.log(`pony eats`);
}
}
run() {
logger.log(`pony runs at ${this.speed}m/s`);
}
}
class NamedPony {
constructor(public name: string, private speed: number) {}
run() {
logger.log(`pony runs at ${this.speed}m/s`);
}
}
class NamedPonyWithoutShortcut {
public name: string;
private speed: number;
run() {
logger.log(`pony runs at ${this.speed}m/s`);
}
}
5.1. readonly
You can use the readonly keyword to mark the
property of a class or interface as… read only! That
way, the compiler will refuse to compile any code
trying to assign a new value to the property:
interface Config {
readonly timeout: number;
}
5.2. keyof
The keyof keyword can be used to get a type
representing the union of the names of the
properties of another type. For example, you have a
PonyModel interface:
interface PonyModel {
name: string;
color: string;
speed: number;
}
interface PartialPonyModel {
name?: string;
color?: string;
speed?: number;
}
type Partial<T> = {
[P in keyof T]?: T[P];
};
5.3.1. Readonly
Readonly makes all the properties of an object
readonly:
5.3.2. Pick
Pick helps you build a type with only some of the
original properties:
5.3.3. Record
Record helps you build a type with the same
properties as another type, but with a different type:
interface FormValue {
value: string;
valid: boolean;
}
There are even more than that, but these are the
most useful.
interface User {
type: 'authenticated' | 'anonymous';
name: string;
// other fields
}
interface BaseUser {
name: string;
// other fields
}
▪ Custom elements
▪ Shadow DOM
▪ Template
constructor() {
super();
console.log("I'm a pony!");
}
customElements.define('ns-pony', PonyComponent);
<ns-pony></ns-pony>
constructor() {
super();
console.log("I'm a pony!");
}
/**
* This is called when the component is inserted
*/
connectedCallback() {
this.innerHTML = '<h1>General Soda</h1>';
}
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const title = document.createElement('h1');
title.textContent = 'General Soda';
shadow.appendChild(title);
}
<ns-pony>
#shadow-root (open)
<h1>General Soda</h1>
</ns-pony>
6.4. Template
A template specified in a <template> element is not
displayed in your browser. Its main goal is to be
cloned in an element at some point. What you
declare inside will be inert: scripts don’t run, images
don’t load, etc. Its content can’t be queried by the
rest of the page using usual methods like
getElementById() and it can be safely placed
anywhere in your page.
To use a template, it needs to be cloned:
<template id="pony-template">
<style>
h1 { color: orange; }
</style>
<h1>General Soda</h1>
</template>
constructor() {
super();
const template = document.querySelector('#pony-template');
const clonedTemplate =
document.importNode(template.content, true);
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(clonedTemplate);
}
Navbar Race
Listing 1. Race.vue
<template>
<div>
<h1>{{ race.name }}</h1>
<ul v-for="pony in race.ponies">
<li>{{ pony.name }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
setup() {
return {
race: {
name: 'Buenos Aires',
ponies: [{ name: 'Rainbow Dash' }, { name: 'Pinkie Pie'
}]
}
};
}
});
</script>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
</head>
<body>
</body>
</html>
Now let’s add some HTML for Vue to handle:
Listing 3. index.html
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
</body>
</html>
Listing 4. index.html
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
</body>
</html>
Listing 5. index.html
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
<script>
const RootComponent = {
setup() {
return { user: 'Cédric' };
}
};
</script>
</body>
</html>
Listing 6. index.html
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
<script>
const RootComponent = {
setup() {
return { user: 'Cédric' };
}
};
const app = Vue.createApp(RootComponent);
app.mount('#app');
</script>
</body>
</html>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
<script>
const UnreadMessagesComponent = {
setup() {
return { unreadMessagesCount: 4 };
}
};
const RootComponent = {
setup() {
return { user: 'Cédric' };
}
};
const app = Vue.createApp(RootComponent);
app.mount('#app');
</script>
</body>
</html>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
<script type="text/x-template" id="unread-messages-
template">
<div>You have {{ unreadMessagesCount }} messages</div>
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
<script>
const UnreadMessagesComponent = {
template: '#unread-messages-template',
setup() {
return { unreadMessagesCount: 4 };
}
};
const RootComponent = {
setup() {
return { user: 'Cédric' };
}
};
const app = Vue.createApp(RootComponent);
app.mount('#app');
</script>
</body>
</html>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
<script type="text/x-template" id="unread-messages-
template">
<div>You have {{ unreadMessagesCount }} messages</div>
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
</div>
<script>
const UnreadMessagesComponent = {
template: '#unread-messages-template',
setup() {
return { unreadMessagesCount: 4 };
}
};
const RootComponent = {
components: {
UnreadMessages: UnreadMessagesComponent
},
setup() {
return { user: 'Cédric' };
}
};
const app = Vue.createApp(RootComponent);
app.mount('#app');
</script>
</body>
</html>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Vue - the progressive framework</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">
</script>
<script type="text/x-template" id="unread-messages-
template">
<div>You have {{ unreadMessagesCount }} messages</div>
</script>
</head>
<body>
<div id="app">
<h1>Hello {{ user }}</h1>
<unread-messages></unread-messages>
</div>
<script>
const UnreadMessagesComponent = {
template: '#unread-messages-template',
setup() {
return { unreadMessagesCount: 4 };
}
};
const RootComponent = {
components: {
UnreadMessages: UnreadMessagesComponent
},
setup() {
return { user: 'Cédric' };
}
};
const app = Vue.createApp(RootComponent);
app.mount('#app');
</script>
</body>
</html>
8.4. Vite
The idea behind Vite is that, as modern browsers
support ES Modules, we can now use them directly,
at least during development, instead of generating a
bundle.
8.5. create-vue
create-vue is built on top of Vite, and provides
templates for Vue 3 projects.
▪ a project name
▪ if you want TypeScript or not
Done?
import './assets/main.css';
createApp(App).mount('#app');
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg"
width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<TheWelcome />
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
</style>
<template>
<h1>Ponyracer</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
setup() {
return { numberOfUsers: 146 };
}
});
9.1. Interpolation
Interpolation is a big word for a simple concept.
Quick example:
Listing 15. App.vue
<template>
<div>
<h1>Ponyracer</h1>
<h2>{{ numberOfUsers }} users</h2>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
setup() {
return { numberOfUsers: 146 };
}
});
</script>
<div>
<h1>PonyRacer</h1>
<h2>146 users</h2>
</div>
<template>
<div>
<h1>Ponyracer</h1>
<h2>{{ numberOfUsers }} users</h2>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
setup() {
const result = { numberOfUsers: 146 };
// update the counter after 3 seconds
// but the template does not reflect the new value
setTimeout(() => (result.numberOfUsers = 147), 3000);
return result;
}
});
</script>
<script lang="ts">
import { defineComponent, ref } from 'vue';
setup() {
const numberOfUsers = ref(146);
// update the counter after 3 seconds
setTimeout(() => (numberOfUsers.value = 147), 3000);
return { numberOfUsers };
}
});
</script>
<div>
<h1>PonyRacer</h1>
<h2>147 users</h2>
</div>
<template>
<div>
<h1>Ponyracer</h1>
<h2>Welcome {{ user.name }}</h2>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
setup() {
const user = ref({
name: 'Cédric'
});
return { user };
}
});
</script>
<div>
<h1>PonyRacer</h1>
<h2>Welcome Cédric</h2>
</div>
<template>
<div>
<h1>Ponyracer</h1>
<h2>{{ user }}</h2>
<!-- displays { "id": 1, "name": "Cédric" } -->
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
setup() {
const user = ref({
id: 1,
name: 'Cédric'
});
return { user };
}
});
</script>
<template>
<div>
<h1>Ponyracer</h1>
<!-- Note the typo: `users` instead of `user` -->
<h2>Welcome {{ users.name }}</h2>
</div>
</template>
You only get this warning when you open your application
in your browser. But, sadly, the compilation raises no
warning if you have an error in a template, unlike some
other frameworks like Angular. Hopefully you also have
unit tests in your application that would catch the problem
early enough, before going to production. We’ll see how to
unit test components and their template in a few chapters.
<template>
<div>
<h2>Races</h2>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts">
import { defineComponent } from 'vue';
<template>
<div>
<h1>Ponyracer</h1>
<Races />
</div>
</template>
<script lang="ts">
import Races from '@/chapters/templates/Races.vue';
import { defineComponent } from 'vue';
createApp(App)
.component('CustomButton', CustomButton)
.mount('#app');
<div :textContent="user.name"></div>
setup() {
return {
buttonProperties: ref({
disabled: true,
type: 'button' as const
}),
};
}
For example,
setup() {
return {
isPrimary: ref(true),
isSmall: ref(false),
};
}
setup() {
return {
buttonClasses: ref({
'btn-primary': true,
'btn-small': false
}),
};
}
setup() {
return {
textStyle: ref({
color: 'red',
fontWeight: 'bold' as const
})
};
}
<button v-on:click="save()">Save</button>
<button @click="save()">Save</button>
<div @click="save()">
<button>Save</button>
</div>
Even though the user clicks on the button embedded
inside the div, the save() function will be called,
because the event bubbles up.
<form @submit.prevent="save()">
<!-- a form -->
<button type="submit">Save</button>
</form>
<button @click.stop="save()">Save</button>
<button @click.prevent.stop="save()">Save</button>
<textarea @keydown.space="onSpacePress()"></textarea>
<textarea @keydown.ctrl.space="showHint()"></textarea>
For example:
<button @mousedown.right="save()">Save</button>
9.4.5. .once modifier
If you want an event listener to execute only once,
you can add the .once modifier:
<button @click.once="save()">Save</button>
<template>
<div>
<h2>Welcome {{ (user!.name as string).toLowerCase() }}</h2>
</div>
</template>
9.6. Summary
The Vue templating system gives us a powerful
declarative syntax to express the dynamic parts of
our HTML. It allows expressing attribute and
property binding, event binding and templating
concerns, clearly, each with their own symbols:
Try our quiz and the exercise Templates It’s free and
part of our online training (Pro Pack), where you’ll learn
how to build a complete application step by step. The
exercise is all about building a small component, a
responsive navbar, and play with its template.
setup() {
const races = ref([
{ id: 1, name: 'Lyon' },
{ id: 2, name: 'Amsterdam' }
]);
return {
races,
};
}
<ul>
<li v-for="race in races">{{ race.name }}</li>
</ul>
<ul>
<li v-for="race of races">{{ race.name }}</li>
</ul>
Now we have a beautiful list, with one li tag per
race in our array!
<ul>
<li v-for="race in races" :key="race.id">{{ race.name }}</li>
</ul>
<ul>
<li>Lyon</li>
<li>Amsterdam</li>
</ul>
<ul>
<li v-for="(race, index) in races" :key="race.id">{{ index }}
- {{ race.name }}</li>
</ul>
<ul>
<li>0 - Lyon</li>
<li>1 - Amsterdam</li>
</ul>
<ul>
<li v-for="(pony, position) in podium" :key="position">{{
position }}: {{ pony }}</li>
</ul>
setup() {
const podium = ref({
gold: 'Rainbow Dash',
silver: 'Pinkie Pie',
bronze: 'Sweet Milk'
});
return {
podium,
};
}
<ul>
<li v-for="(pony, position, index) in podium"
:key="position">{{ index + 1 }} - {{ position }}: {{ pony }}
</li>
</ul>
<ul>
<li>1 - gold: Rainbow Dash</li>
<li>2 - silver: Pinkie Pie</li>
<li>3 - bronze: Sweet Milk</li>
</ul>
Be careful though: if the object comes from the
server as JSON, there is no guarantee regarding the
order of the properties in the object. So it’s usually
best to stick to arrays when the order matters.
<ul>
<li v-for="number in 3" :key="number">{{ number }}</li>
</ul>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
with:
setup() {
return {
dynamicHTML: ref('<strong>Cédric</strong>')
};
}
But that does not work and displays the raw HTML…
We can render the HTMl by using the property
innerHTML:
<div :innerHTML="dynamicHTML"></div>
<div v-html="dynamicHTML"></div>
displays:
<template>
<div>
<h1>Ponyracer</h1>
<h2>{{ numberOfUsers }} users</h2>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
setup() {
const numberOfUsers = ref(146);
// update the counter after 3 seconds
setTimeout(() => (numberOfUsers.value = 147), 3000);
return { numberOfUsers };
}
});
</script>
setup() {
const pony = ref({
name: 'Rainbow Dash',
color: 'GREEN'
});
// update the name after 3 seconds
setTimeout(() => {
pony.value.name = 'Pinkie Pie';
}, 3000);
return { pony };
}
});
That’s where reactive() can help! It is very similar
to ref in the sense that it declares a property as
reactive, and Vue will watch the changes and refresh
the template accordingly.
Listing 27. Pony.vue
setup() {
const pony = reactive({
name: 'Rainbow Dash',
color: 'GREEN'
});
// update the name after 3 seconds
setTimeout(() => {
pony.name = 'Pinkie Pie';
}, 3000);
return { pony };
}
});
setup() {
const pony = reactive({
name: 'Rainbow Dash',
color: 'GREEN',
origin: {
country: 'FRANCE'
}
});
// update the country of origin after 3 seconds
setTimeout(() => {
pony.origin.country = 'GERMANY';
}, 3000);
return { pony };
}
});
<template>
<div>{{ price }} * {{ quantity }}</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
setup() {
const price = ref(10);
const quantity = ref(1);
// update the price and quantity after 3 seconds
setTimeout(() => {
price.value = 9;
quantity.value = 3;
}, 3000);
return { price, quantity };
}
});
</script>
<template>
<div>{{ state.price }} * {{ state.quantity }}</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
setup() {
const state = reactive({
price: 10,
quantity: 1
});
// update the price and quantity after 3 seconds
setTimeout(() => {
state.price = 9;
state.quantity = 3;
}, 3000);
return { state };
}
});
</script>
You may be tempted to destructure the state object to
directly use price and quantity in the template instead of
state.price and state.quantity. But this breaks the
reactivity, so the template will not reflect any update of
state.
So this fails:
Listing 31. Product.vue
<template>
<div>{{ price }} * {{ quantity }}</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
setup() {
const state = reactive({
price: 10,
quantity: 1
});
// update the price and quantity after 3 seconds
setTimeout(() => {
state.price = 9;
state.quantity = 3;
}, 3000);
// destructure the state
// to use price and quantity directly in the template
return { ...state };
}
});
</script>
<template>
<div>{{ price }} * {{ quantity }}</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
setup() {
const state = reactive({
price: 10,
quantity: 1
});
// update the price and quantity after 3 seconds
setTimeout(() => {
state.price = 9;
state.quantity = 3;
}, 3000);
// use toRefs to destructure the state
return { ...toRefs(state) };
}
});
</script>
With reactive:
<template>
<div>{{ price }} * {{ quantity }} = {{ total }}</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from
'vue';
setup() {
const state = reactive({
price: 10,
quantity: 1
});
function stop() {
stopRecording();
}
<template>
<div>
<h1>{{ name }}</h1>
<div>
<h2>{{ pony1.name }}</h2>
<small>{{ pony1.color }}</small>
</div>
<div>
<h2>{{ pony2.name }}</h2>
<small>{{ pony2.color }}</small>
</div>
<div>
<h2>{{ pony3.name }}</h2>
<small>{{ pony3.color }}</small>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';
setup() {
const name = ref('Paris');
const pony1 = reactive({
name: 'Rainbow Dash',
color: 'BLUE'
});
const pony2 = reactive({
name: 'Pinkie Pie',
color: 'PINK'
});
const pony3 = reactive({
name: 'Fluttershy',
color: 'YELLOW'
});
return { name, pony1, pony2, pony3 };
}
});
</script>
<template>
<div>
<h1>{{ name }}</h1>
<Pony />
<Pony />
<Pony />
</div>
</template>
<template>
<div>
<h2>{{ name }}</h2>
<small>{{ color }}</small>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
props: {
name: String,
color: String
}
});
</script>
<template>
<div>
<h1>{{ name }}</h1>
<Pony :name="pony1.name" :color="pony1.color" />
<Pony :name="pony2.name" :color="pony2.color" />
<Pony :name="pony3.name" :color="pony3.color" />
</div>
</template>
If you want to pass a number, a boolean, an array, etc. you
must use v-bind:, even for a static data. Otherwise, Vue will
pass the string value: <Pony speed="16" isRunning="false"
/> would pass "16" as the speed and "false" as the
isRunning flag, not 16 and false. Note that for a true
boolean value, you can also simply use <Pony isRunning />
<template>
<div>
<h1>{{ name }}</h1>
<Pony :ponyModel="pony1" />
<Pony :ponyModel="pony2" />
<Pony :ponyModel="pony3" />
</div>
</template>
props: {
ponyModel: Object
}
});
props: {
ponyModel: Object as PropType<PonyModel>
}
});
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
}
}
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
default: () => ({
name: 'Rainbow Dash',
color: 'BLUE'
})
}
}
speed: {
type: Number,
validator: (speed: number) => speed > 5
}
All these runtime checks (types, required, validation) are
omitted when running the application in production mode.
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
}
},
setup(props) {
const ponyImageUrl = computed(() =>
`images/pony-${props.ponyModel.color.toLowerCase()}.gif`);
return { ponyImageUrl };
}
Race
prop ponyModel
Pony
Race
event selected
Pony
How do we emit such an event? It turns out that
setup(props) can have a second parameter, the
context: setup(props, context). context is an object
containing several things, and we’ll see some of
them later. But right now, we’re only interested in
the emit function it contains, so we can use
destructuring:
Listing 50. Pony.vue
<template>
<div @click="selectPony()">
<h2 :style="{ color: ponyModel.color }">{{ ponyModel.name
}}</h2>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { PonyModel } from '@/models/PonyModel';
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
}
},
emits: {
// We declare that the only event emitted by the component
will be 'selected'
// and that the emitted value is of type `PonyModel`
// The function is a validator of the event argument,
checked by Vue in development mode
selected: (pony: PonyModel) => pony.color !== 'PURPLE'
},
setup(props, { emit }) {
function selectPony() {
// we emit a 'selected' event containing the pony entity
emit('selected', props.ponyModel);
}
return { selectPony };
}
});
</script>
The emits property is optional, but it’s nice to have. At
compile time, TypeScript will check that you only emit the
events that are declared in the emits property, and that the
events you emit have the appropriate type. In this example,
the compilation would thus fail if you emit an event that
isn’t named selected, or if the event is not a PonyModel. At
runtime, during development only, Vue also checks that the
given predicate accepts the emitted events. If it doesn’t, the
event will still be emitted, but you’ll get a warning in the
console. In this example, you’ll thus get a warning if you
emit a purple pony. If you don’t want to validate the event
value, you can just return true in the predicate function.
Try our exercise Pony component to build a new
component with a custom event.
props: {
raceId: {
type: Number,
required: true
}
},
setup(props) {
const race = ref<RaceModel | null>(null);
return { race };
}
setup(props) {
const race = ref<RaceModel | null>(null);
onMounted(() => {
// we fetch the details of the race based on its ID every
10s
window.setInterval(async () => (race.value = await
fetchRace(props.raceId)), 10000);
});
return { race };
}
setup(props) {
const race = ref<RaceModel | null>(null);
let intervalRef: number | undefined;
onMounted(() => {
intervalRef =
// we fetch the details of the race based on its ID every
10s
window.setInterval(async () => (race.value = await
fetchRace(props.raceId)), 10000);
});
onUnmounted(() => {
//we cancel the timeout
window.clearInterval(intervalRef);
});
return { race };
}
Try our quiz to check if you got everything!
12. STYLE YOUR
COMPONENTS
Let’s stop to talk about styles and CSS for a minute. I
know right? Why talk about CSS?
<div>
<img :src="ponyModel.imageUrl" :alt="ponyModel.name" />
<span class="highlight">{{ ponyModel.name }}</span>
</div>
<style scoped>
.highlight {
color: red;
}
</style>
<style scoped>
.highlight {
color: v-bind('ponyModel.color');
}
</style>
/* deep selectors */
::v-deep(.foo) {}
/* shorthand */
:deep(.foo) {}
12.5. PostCSS
Vite and Vue CLI use PostCSS under the hood to
transform our CSS.
But if you want to use more than raw CSS, then Vite
and the CLI also have a built-in support for the most
common pre-processors:
▪ SCSS
▪ Less
▪ Stylus
<style lang="scss">
</style>
To sum up, Vite and the CLI are very helpful for the
style part an application. You probably want to use
scoped styles most of the time, and Vue will do the
heavy lifting !
13. COMPOSITION API
We already introduced the Composition API in the
previous chapter in the context of building
components.
setup(props) {
const product = ref<ProductModel | null>(null);
const quantity = ref(0);
const price = computed(() => quantity.value *
(product.value?.unitPrice ?? 0));
onMounted(async () => {
// log a trace that the user entered this page
await trace('enter');
});
function increaseQuantity() {
quantity.value += 1;
}
function decreaseQuantity() {
if (quantity.value > 0) {
quantity.value -= 1;
}
}
onUnmounted(async () => {
// log a trace that the user left this page
await trace('exit');
});
function useTracking() {
onMounted(() => {
// log a trace that the user entered this page
trace('enter');
});
onUnmounted(() => {
// log a trace that the user left this page
trace('exit');
});
}
function decreaseQuantity() {
if (quantity.value > 0) {
quantity.value -= 1;
}
}
setup(props) {
useTracking();
// ...
// ...
setup() {
const credentials = {
login: '',
password: ''
};
const userService = useUserService();
async function login() {
await userService.authenticate(credentials.login,
credentials.password);
}
return { credentials, login };
}
// ...
<script lang="ts">
export default defineComponent({
name: 'Home',
setup() {
const { userModel } = useUserService();
return { userModel };
}
});
</script>
<template>
<div v-if="userModel">Hello {{ userModel.name }}!</div>
<div v-else>Welcome</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from 'vue';
import { useUserService } from './UserService';
setup() {
const { userModel, trace } = useUserService();
onMounted(() => trace('home:enter'));
onUnmounted(() => trace('home:exit'));
return { userModel };
}
});
</script>
setup() {
const { userModel } = useUserService('home');
return { userModel };
}
});
<template>
<div>{{ coords.latitude }} - {{ coords.longitude }}</div>
<img :src="qrcode" alt="Become a Ninja with Vue 3" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTitle, useGeolocation } from '@vueuse/core';
import { useQRCode } from '@vueuse/integrations/useQRCode';
setup() {
// change the title
useTitle('VueUse is awesome');
// get the position of the user
const { coords } = useGeolocation();
// generate a QR code for the ebook
const qrcode = useQRCode('https://books.ninja-
squad.com/vue');
return { coords, qrcode };
}
});
</script>
There are also integrations for firebase, axios, etc. A
lot of other tiny little gems are available, think about
it next time you need something!
14. THE MANY WAYS TO
DEFINE COMPONENTS
As we were saying in the previous chapter, there are
many ways to build a component since Vue 3.
<template>
<div>{{ price }} * {{ quantity }} = {{ total }}</div>
<CustomButton @click="addOne()">Add 1 unit</CustomButton>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import CustomButton from '@/chapters/many-
ways/CustomButton.vue';
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
import CustomButton from '@/chapters/many-
ways/CustomButton.vue';
components: {
CustomButton
},
setup() {
const price = ref(10);
const quantity = ref(1);
const total = computed(() => price.value * quantity.value);
const addOne = () => quantity.value++;
return { price, quantity, total, addOne };
}
});
</script>
<template>
<div>{{ price }} * {{ quantity }} = {{ total }}</div>
<CustomButton @click="addOne()">Add 1 unit</CustomButton>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import CustomButton from '@/chapters/many-
ways/CustomButton.vue';
const price = ref(10);
const quantity = ref(1);
const total = computed(() => price.value * quantity.value);
const addOne = () => quantity.value++;
</script>
<template>
<div>{{ price }} * {{ quantity }} = {{ total }}</div>
<CustomButton @click="addOne()">Add 1 unit</CustomButton>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import CustomButton from '@/chapters/many-
ways/CustomButton.vue';
@Options({
components: {
CustomButton
}
})
export default class Product extends Vue {
price = 10;
quantity = 1;
addOne(): void {
this.quantity++;
}
}
</script>
<template>
<figure @click="clicked()">
<CustomImage :src="ponyImageUrl" :alt="ponyModel.name" />
<figcaption>{{ ponyModel.name }}</figcaption>
</figure>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import CustomImage from './CustomImage.vue';
import { PonyModel } from '@/models/PonyModel';
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
},
isRunning: {
type: Boolean,
default: false
}
},
emits: {
selected: () => true
},
setup(props, { emit }) {
const ponyImageUrl = computed(() =>
`/pony-${props.ponyModel.color}${props.isRunning ? '-running' :
''}.gif`);
function clicked() {
emit('selected');
}
components: { CustomImage },
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
},
isRunning: {
type: Boolean,
default: false
}
},
emits: {
selected: () => true
},
function clicked() {
emit('selected');
}
props: {
ponyModel: {
type: Object as PropType<PonyModel>,
required: true
},
isRunning: {
type: Boolean,
default: false
}
},
emits: {
selected: () => true
},
function clicked() {
emit('selected');
}
15.3. defineProps
Vue offers a defineProps helper that you can use to
define your props. It’s a compile-time helper (a
macro), so you don’t need to import it in your code:
Vue automatically understands it when it compiles
the component.
interface Props {
ponyModel: PonyModel;
isRunning?: boolean;
}
interface Props {
ponyModel: PonyModel;
isRunning?: boolean;
}
<template>
<figure @click="clicked()">
<CustomImage :src="ponyImageUrl" :alt="ponyModel.name" />
<figcaption>{{ ponyModel.name }}</figcaption>
</figure>
</template>
function clicked() {
emit('selected');
}
</script>
15.5. defineOptions
Vue v3.3 introduced a new macro called
defineOptions. It can be handy to declare a few
things like the name of a component, if the inferred
name based on the file name is not good enough or
to set the inheritAttrs option:
Try the exercise Script setup to migrate our application to
this syntax.
16. TESTING YOUR APP
▪ unit tests
▪ end-to-end tests
We’re going to cover all this, but let’s begin with the
unit test part.
16.3. Vitest
Vitest is a really cool solution: it’s really similar to
Jest (same API, based on jsdom) but faster and more
modern.
class Pony {
constructor(public name: string, public speed: number) {}
describe('Pony', () => {
test('should be faster than a slow speed', () => {
const pony = new Pony('Rainbow Dash', 10);
expect(pony.isFasterThan(8)).toBe(true);
});
});
describe('Pony', () => {
test('should not be faster than a fast speed', () => {
const pony = new Pony('Rainbow Dash', 10);
expect(pony.isFasterThan(20)).not.toBe(true);
});
});
The test file is a separate file from the code you want
to test, usually with an extension like .spec.ts. The
test for a Pony component written in a Pony.vue file
will likely be in a file named Pony.spec.ts. Where to
put the .spec.ts files is a matter of preferences:
some people like to put them next to the file being
tested in a __tests__ directory, others prefer putting
them in a separate, dedicated directory. I tend to do
what create-vue does by default, and use a dedicated
__tests__ directory.
describe('Pony', () => {
let pony: Pony;
beforeEach(() => {
pony = new Pony('Rainbow Dash', 10);
});
test('should be faster than a slow speed', () => {
expect(pony.isFasterThan(8)).toBe(true);
});
class Race {
constructor(private ponies: Array<Pony>) {}
start(): Array<Pony> {
return (
this.ponies
// start every pony
// and only keeps the ones that started running
.filter(pony => pony.run(10))
);
}
}
describe('Race', () => {
let rainbowDash: Pony;
let pinkiePie: Pony;
let race: Race;
beforeEach(() => {
rainbowDash = new Pony('Rainbow Dash');
// first pony agrees to run
vi.spyOn(rainbowDash, 'run').mockReturnValue(true);
});
16.4. @vue/test-utils
Vue has an official testing library called @vue/test-
utils.
It is super handy to unit test Vue components, as you
can mount a component, and then test not only its
functions and state, but also its template and events.
<template>
<figure @click="clicked()">
<img :src="ponyImageUrl" :alt="ponyModel.name" />
<figcaption>{{ ponyModel.name }}</figcaption>
</figure>
</template>
function clicked() {
emit('ponySelected');
}
</script>
describe('Pony.vue', () => {
test('should display an image and a legend', () => {
const ponyModel: PonyModel = {
id: 1,
name: 'Fast Rainbow',
color: 'PURPLE'
};
expect(wrapper.element).toMatchSnapshot();
});
When you execute the test for the first time, the test
won’t actually verify anything, but will generate the
snapshot. You can then check if it contains the
expected DOM structure (and fix the code until it
does).
Once you’re happy with the result, you can save the
snapshot in your VCS system (it becomes part of
your sources), so that every subsequent test
execution verifies that the DOM generated by the
test still matches the saved snapshot.
expect(received).toMatchSnapshot()
- Expected - 1
+ Received + 1
alt=\"Fast Rainbow\"
src=\"/images/pony-purple.gif\"
/>
<figcaption>
- Fast Rainbow
+ Fast Rainbow!
</figcaption>
</figure>"
You can then spot you have an issue and fix it, or
accept the changes if you want to update the
snapshot with the new result.
This can be a nice tool, but I’m not a huge fan of this
practice, as developers sometimes blindly accept the
changes without really paying attention. I consider
them more like a complement for conventional tests
than a replacement. They are nice to have and super
easy to write though!
16.7. Cypress
▪ easy to set up
▪ easy to mock HTTP responses
You will write your test suite using Mocha, and you’ll
see that it really looks like what we had in unit tests
with Vitest, plus the Cypress API to interact with
your app.
describe('Search', () => {
it('should display a title', () => {
cy.visit('/search');
cy.contains('h1', 'Ponyracer');
});
cy.visit('/search');
// search the London race
cy.get('input').type('London');
// click on the search button
cy.get('button').click();
// we should have an API call
cy.wait('@searchRace');
});
});
All our Pro Pack exercises come with unit and e2e tests! If
you want to learn more, we strongly encourage you to take
a look at them: we tested every possible part of the
application (100% code coverage)! In the end you’ll have
dozens of test examples, which you can get inspiration
from for your own projects. More advanced use cases are
even covered, like time manipulation in the exercise Boost
a pony , or Websocket communication in the exercise
Reactive score .
17. SEND AND RECEIVE
DATA THROUGH HTTP
It won’t come as a surprise, but a lot of our job
consists in asking a backend server to send data to
our webapp, and then sending data back.
▪ get
▪ post
▪ put
▪ delete
▪ patch
▪ head
const params = {
sort: 'ascending',
page: '1'
};
const response = await axios.get<Array<RaceModel>>
('/api/races', { params });
// will call the URL /api/races?sort=ascending&page=1
17.3. Interceptors
One of the reasons to use Axios over the Fetch API is
the “interceptors” feature. Interceptors are
interesting when you want to… intercept requests or
responses in your application.
axios.interceptors.request.use((config:
InternalAxiosRequestConfig) => {
if (user) {
config.headers!.Authorization = `Bearer ${user.token}`;
}
return config;
});
axios.interceptors.response.use(
(response: AxiosResponse) => response,
error => {
// do whatever you want with the error
return Promise.reject(error);
}
);
17.4. Tests
We now have an application calling an HTTP
endpoint to fetch the races. How do we test it?
<div class="card">
<div class="card-body">
<h4 class="card-title">Card title</h4>
<p class="card-text">Some quick example text</p>
</div>
</div>
<template>
<div class="card">
<div class="card-body">
<h4 class="card-title">{{ title }}</h4>
<p class="card-text">{{ text }}</p>
</div>
</div>
</template>
<template>
<div class="card">
<div class="card-body">
<h4 class="card-title">{{ title }}</h4>
<p class="card-text">
<slot></slot>
</p>
</div>
</div>
</template>
<Card>
<template v-slot:title>Card title</template>
<template v-slot:default>Some quick <strong>example</strong>
text</template>
</Card>
<Card>
<template #title>Card title</template>
<template #default>Some quick <strong>example</strong>
text</template>
</Card>
<Card>
<template #title>Card title</template>
Some quick <strong>example</strong> text
</Card>
<template>
<div class="card">
<div class="card-body">
<h4 class="card-title">
<slot name="title">Default title</slot>
</h4>
<p class="card-text">
<slot></slot>
</p>
</div>
</div>
</template>
<Card>
<template #title>Card title</template>
Some quick <strong>{{ example }}</strong> text
</Card>
<template>
<div v-if="!isClosed" class="card">
<div class="card-body">
<h4 class="card-title">
<slot name="title">Default title</slot>
</h4>
<p class="card-text">
<slot></slot>
</p>
<button class="btn btn-primary"
@click="close()">Close</button>
</div>
</div>
</template>
<h4 class="card-title">
<slot name="title" :closeCard="close">Default title</slot>
</h4>
This means that the Card component decides to make
its close function available in the template of its title
slot, under the name closeCard (we could of course
use the same name).
<Card>
<template #title="titleProps">
<div class="title" @click="titleProps.closeCard()">Card
title</div>
</template>
Some quick <strong>example</strong> text
</Card>
<Card>
<template #title="{ closeCard }">
<div class="title" @click="closeCard()">Card title</div>
</template>
Some quick <strong>example</strong> text
</Card>
defineSlots<{
// default slot has no props
default: (props: Record<string, never>) => void;
// title slot has a prop `closeCard`, which is a function
title: (props: { closeCard: () => void }) => void;
}>();
<Card>
<template #title="{ close }">...</template>
</Card>
<!-- error TS2339: Property 'close' does not exist on type '{
closeCard: () => void; }'. -->
<div>
<h1>Pony {{ ponyModel.name }}</h1>
<TrackRecord :ponyId="ponyModel.id" />
</div>
<div>
<h2>Track record</h2>
<ul>
<li v-for="record in trackRecord" :key="record.id">{{
record.position }} - {{ record.race.name }}</li>
</ul>
</div>
setup(props) {
const trackRecord = ref<Array<TrackRecordModel>>([]);
const ponyService = usePonyService();
onMounted(async () => (trackRecord.value = await
ponyService.getTrackRecord(props.ponyId)));
return { trackRecord };
}
async setup(props) {
const ponyService = usePonyService();
const trackRecord = ref(await
ponyService.getTrackRecord(props.ponyId));
return { trackRecord };
}
<div>
<h1>Pony {{ ponyModel.name }}</h1>
<Suspense>
<div>
<TrackRecord :ponyId="ponyModel.id" />
</div>
</Suspense>
</div>
<div>
<h1>Pony {{ ponyModel.name }}</h1>
<Suspense>
<div>
<TrackRecord :ponyId="ponyModel.id" />
</div>
<template #fallback>Loading...</template>
</Suspense>
</div>
<div>
<h1>Pony {{ ponyModel.name }}</h1>
<Suspense>
<div>
<TrackRecord :ponyId="ponyModel.id" />
<BirthCertificate :ponyId="ponyModel.id" />
</div>
<template #fallback>Loading...</template>
</Suspense>
</div>
<div>
<h1>Pony {{ ponyModel.name }}</h1>
<div v-if="errorMessage">An error occurred while loading
data: {{ errorMessage }}</div>
<Suspense>
<div>
<TrackRecord :ponyId="ponyModel.id" />
<BirthCertificate :ponyId="ponyModel.id" />
</div>
<template #fallback>Loading...</template>
</Suspense>
</div>
Try our exercise Suspense to see how we can handle our
asynchronous data fetching with class!
20. ROUTER
It is fairly common to want to map a URL to a
component of the application. That makes sense: you
want your user to be able to bookmark a page and
come back, and it provides a better experience
overall.
20.1. En route
Let’s start using the router. It is an optional plugin,
that is thus not included in the core framework.
createApp(App)
.use(router)
.mount('#app');
As you can see, the routes is an array of objects, each
one being a… route. A route configuration is usually
a triplet of properties:
App
NavBar
RouterView
where our component goes
Footer
This is, of course, a Vue component, provided by
vue-router, whose only job is to act as a placeholder
for the template of the component of the current
route. Our app template would look like:
<div>
<NavBar />
<RouterView />
<Footer />
</div>
20.2. Navigation
How can we navigate between the different
components? Well, you can manually type the URL
and reload the page, but that’s not very convenient.
And we don’t want to use “classic” links, with <a
href="…"></a>. Indeed, clicking on such a link makes
the browser load the page at that URL, and restart
the whole Vue application. But the goal of Vue is to
avoid such page reloads: we want to create a Single
Page Application. Of course, there is a solution built-
in.
In a template, you can insert a link by using the
component RouterLink pointing to the path you want
to go to. The RouterLink component expects a prop,
named to, representing the path you want to go to,
or an array of strings, representing the path and its
parameters. For example in our Races template, if we
want to navigate to the Home, we can imagine
something like:
<RouterLink to="/">Home</RouterLink>
20.3. Parameters
It is also possible to have parameters in the URL, and
it’s really useful to define dynamic URLs. For
example, we want to display a detail page for a pony,
with a meaningful URL for this page, like races/id-
of-the-race/ponies/id-of-the-pony.
{
path: '/races/:raceId/ponies/:ponyId',
name: 'pony',
component: Pony
},
{
path: '/users/:userId',
name: 'user',
component: User,
props: true
},
20.6. Redirects
A common use-case is to have a URL simply redirect
to another URL in the application. This can happen
because you want, for example, the root URL of your
news app to redirect to the /breaking news category,
or an old URL to redirect to a new one after a
refactoring. This is possible using:
{
path: '/news',
redirect: '/breaking'
},
{
path: '/:catchAll(.*)',
redirect: '/404'
}
{
path: '/ponies/:ponyId',
component: Pony,
children: [
{ path: 'birth-certificate', component: BirthCertificate },
{ path: 'track-record', component: TrackRecord },
{ path: 'reviews', component: Reviews }
]
},
When going to the URL /ponies/42/reviews, for
example, the router will insert the Pony component
at the location indicated by the main RouterView, in
the App component. The template of Pony, besides the
name and the portrait of the pony, contains a second
RouterView. This is where the child Reviews
component will be inserted.
App
...
RouterView
Pony
...
RouterView
Reviews
...
{
path: '/ponies/:ponyId',
component: Pony,
children: [
{ path: '', redirect: 'birth-certificate' },
{ path: 'birth-certificate', component: BirthCertificate },
{ path: 'track-record', component: TrackRecord },
{ path: 'reviews', component: Reviews }
]
},
{
path: '/ponies/:ponyId',
component: Pony,
children: [
{ path: '', component: BirthCertificate },
{ path: 'track-record', component: TrackRecord },
{ path: 'reviews', component: Reviews }
]
},
beforeEach
The most common one is beforeEach:
afterEach
There is also a global afterEach that you can define.
This is not really a guard, as it does not prevent the
navigation. It’s more a hook that allows you to do
something when the navigation is confirmed.
20.9.2. Route guard
There is only one guard that you can define in the
route declaration itself: beforeEnter
beforeEnter
This guard will be checked after the global ones (if
any). The signature is the same has the global
beforeEach:
{
path: '/search',
component: Search,
beforeEnter: (
to: RouteLocationNormalized,
from: RouteLocationNormalized
): boolean | RouteLocationNormalized | string => {
// check that the user has the permissions to see the races
}
},
{
path: '/search',
component: Search,
meta: {
requiresAuth: true
}
},
beforeEach(() => {
injectRouterMock(router);
router.setParams({
raceId: '1',
ponyId: '2'
});
});
expect(router.push).toHaveBeenCalledWith('/');
});
app.js 100KB
App, NavBar,
Home components...
admin.js 50KB
Admin components
<div>
<h1>Ponyracer</h1>
<button @click="showRaces = true">Show races</button>
<div v-if="showRaces">
<PendingRaces />
</div>
</div>
app.js 100KB
App, NavBar,
Home components...
races.js 250KB
PendingRaces comp
VeryBigLibrary.js
{
path: '/races',
component: () => import('@/views/Races.vue')
},
<form @submit.prevent="register()">
▪ Vuelidate
▪ VeeValidate
Try our exercise Register ! It’s part of our Pro Pack, and
you’ll learn how to build a complete form with v-model.
This exercise lets you handle the validation and error
messages yourself!
defineRule('min', min);
defineRule('required', required);
defineRule('confirmed', confirmed);
<template>
<label for="name">Name</label>
<input id="name" v-model="name" name="name" />
<div v-if="nameMeta.dirty && !nameMeta.valid" class="error">
{{ nameErrorMessage }}</div>
</template>
<template>
<form @submit="register()">
<label for="name">Name</label>
<input id="name" v-model="name" />
<div class="error">{{ errors.name }}</div>
<label for="password">Password</label>
<input id="password" v-model="password" type="password" />
<div class="error">{{ errors.password }}</div>
<Form @submit="register($event)">
<label for="name">Name</label>
<Field id="name" name="name" rules="required" value="JB" />
<label for="password">Password</label>
<Field id="password" name="password" type="password"
rules="required" />
<button type="submit">Register</button>
</Form>
configure({
validateOnInput: true,
generateMessage: localize('en', {
messages: {
// use a function
required: context => `The ${context.field} is required.`,
confirmed: context => `The ${context.field} does not
match.`,
// or use the special syntax offered by the library
min: 'The {field} must be at least 0:{min} characters.',
}
})
});
You can also rename the field if you need to, with
the label prop:
Listing 159. Register.vue
<Field
v-slot="{ field, meta }"
v-model="user.confirmPassword"
name="confirmPassword"
rules="required|confirmed:@password"
label="password confirmation"
>
<label for="confirm-password-input" :class="{ 'text-danger':
meta.dirty && !meta.valid }">Confirm password</label>
<input
id="confirm-password-input"
type="password"
:class="{ 'is-invalid': meta.dirty && !meta.valid }"
v-bind="field"
/>
<ErrorMessage name="confirmPassword" class="error" />
</Field>
VeeValidate comes with a devtools plugin, allowing to
directly inspect the state of your form in the devtools of
your browser when you are developing 😍.
Try our exercise Login ! It’s part of our Pro Pack, and
you’ll learn how to build a complete form with validation
rules with VeeValidate.
You then add the rule, as you do for the built-in ones:
Listing 163. forms.ts
defineRule('between18And130', between18And130);
Try our exercise Custom validators ! It’s part of our Pro
Pack, and you’ll learn how to write your own validators in
practice.
<template>
<div>
<button
v-for="pickableValue of pickableValues"
:key="pickableValue"
:class="{ selected: modelValue != null && pickableValue
<= modelValue }"
type="button"
@click="setValue(pickableValue)"
>
{{ pickableValue }}
</button>
</div>
</template>
<form>
<div>
<label for="title">Title</label>
<input id="title" v-model="movieTitle" />
</div>
<div>
<label for="rating">Rating</label>
<Rating id="rating" v-model="movieRating" />
</div>
<template>
<input :value="modelValue" @input="setValue(($event.target as
HTMLInputElement).value)" />
</template>
<template>
<input v-model="modelValue" />
</template>
...
Pony inject(key)
So we create an InjectionKey:
createApp(App)
.provide(colorKey, ref('#987654'))
.mount('#app');
▪ etc.
▪ if not, it looks into the root component
▪ if not, it looks into the provided values of the
application
createApp(App)
.use(router)
.mount('#app');
// ...
export function useUserService() {
return {
userModel,
authenticate,
logout
};
}
<template>
<div v-if="userModel">Hello {{ userModel.name }}!</div>
<div v-else>Welcome, anonymous comrade!</div>
</template>
24.3. Vuex
The most popular state management library in the Vue 2
ecosystem was Vuex. Vuex v4 works with Vue 3, but is not
ideal, as it is not type-safe, and not super well integrated
with the Composition API. In Vue 3, the recommandation is
now to use Pinia, which is basically Vuex v5, but with a
different name and a cute logo. Feel free to skip this section
and go straight to the Pinia section below!
createApp(App)
.use(store, storeKey)
.use(router)
.mount('#app');
24.4. Pinia
The author of the Vue router, @posva, created an
alternative to Vuex, with a nice Composition API.
The library is called Pinia, and it comes with the
cutest logo.
The project started as an experiment for Vuex v5,
but it was so good (and with such a cute name and
logo) that it ended up being the official
recommendation for the state-management library
of Vue.
function logout() {
userModel.value = null;
}
createApp(App)
.use(createPinia())
.use(router)
.mount('#app');
describe('NavbarWithPinia.vue', () => {
test('should logout the user', async () => {
// mount the component
const wrapper = mount(NavbarWithPinia, {
global: {
// with a "fake" pinia
plugins: [
createTestingPinia({
createSpy: vi.fn
})
]
}
});
// you can get the store, and change its state
const store = useAppStore();
const logout = wrapper.get('#logout');
await logout.trigger('click');
// actions are already spied,
// so you just have to check if they are properly called
expect(store.logout).toHaveBeenCalled();
});
});
await wrapper.get('#logout').trigger('click');
// the action really logged out the user
expect(wrapper.text()).toContain('Welcome, anonymous
comrade!');
});
});
Try our exercise State management with Pinia where
you’ll refactor our state management by using Pinia.
25. ANIMATIONS AND
TRANSITION EFFECTS
Animations are a very nice addition to an
application. It’s not the first thing you do, of course,
but it can really improve the user experience.
@keyframes shake {
10%,
50%,
90% {
transform: translateX(0.5rem);
}
30%,
70% {
transform: translateX(-0.5rem);
}
}
.pill {
transition: transform 300ms ease-out;
}
.pill.pill-selected {
transform: scale(1.1);
}
You’re saying that, for elements with the class pill,
every change in the value of the transform property
will be done in a progressive way, over 300
milliseconds.
<Transition name="fade">
<div v-if="display">Content</div>
</Transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
Here the transition will last one second, and will
progressively change the opacity from 0 to 1 when
the element enters, and from 1 to 0 when it leaves.
Opacity 0 Opacity 1
.fade‐enter‐from .fade‐enter‐to
.fade‐enter‐active
Opacity 1 Opacity 0
.fade‐leave‐from .fade‐leave‐to
.fade‐leave‐active
.list-leave-active,
.list-enter-active {
transition: 1s;
}
.list-enter-from {
transform: translateX(100%);
}
.list-leave-to {
transform: translateX(-100%);
}
.list-move {
transition: transform 1s ease;
}
▪ @beforeEnter/@enter/@afterEnter
▪ @beforeLeave/@leave/@afterLeave
▪ @enterCancelled/@leaveCancelled
onMounted(() => {
nameInput.value?.focus();
});
This is exactly what we showcase in the Pro Pack 😉 . Try
our exercise Charts in your app to learn how to use
template refs to integrate a third-party library.
26.2. Component references
This pattern also works with Vue components! Let’s
say you have a dropdown component (yours, or a
third-party one, it doesn’t matter).
defineExpose({
toggle
});
<Dropdown ref="dropdown">
<button>First choice</button>
<button>Second choice</button>
</Dropdown>
We have a nice use case in the Pro Pack, where you build a
custom directive to add classes on form inputs depending
on their validation. Try our exercise Custom directive to
learn how to build it.
28.
INTERNATIONALIZATION
Alors comme ça, tu veux internationaliser ton
application?
▪ translating text
▪ formatting numbers and dates
{
"chart": {
"title": "Score history",
"legend": "Legend",
}
}
createApp(App)
.use(i18n)
.mount('#app');
The function will get the text related to the key from
the messages of the current locale and display it.
28.3. Message parameters
You sometimes want to have parameters in your
messages. For example, we’d like to display the login
of the user in a message:
{
"chart": {
"title": "Score history",
"legend": "Legend",
"user": "Score for user { login }",
}
}
28.4. Pluralization
It is also possible to handle pluralization with |:
{
"chart": {
"title": "Score history",
"legend": "Legend",
"ponies": "no ponies | one pony | {n} ponies"
}
}
<select v-model="locale">
<option v-for="availableLocale in availableLocales"
:key="availableLocale" :value="availableLocale">
{{ availableLocale }}
</option>
</select>
28.6. Formatting
vue-i18n also helps with the formatting of dates with
d(Date|string|number), and numbers with n(number).
Both functions are based on the browser Intl
support, and allow specifying a pattern as a second
parameter if needed.
To get started and learn a few more tricks, try our exercise
Internationalization ! It’s part of our Pro Pack, and you’ll
learn how to internationalize a full application.
29. UNDER THE HOOD
I have a confession to make: I particularly enjoy
learning how things really work. There are a lot of JS
frameworks out there, but very few people know
what the differences are between them under the
hood.
<div>
<span>Hello {{ user.name }}</span>
<small v-if="user.isAdmin">admin</small>
</div>
<input type="checkbox" :value="user.isAdmin"
@change="updateAdminRights()" />
▪ a parser
▪ several transformers
▪ a code generator producing the render function.
▪…
▪…
▪…
▪ ‘:’: Oh this is a Vue binding
▪ etc.
const greetingsAST = {
type: 'ELEMENT',
tag: 'div',
props: [],
children: [
{
type: 'ELEMENT',
tag: 'span',
props: [],
children: [
{ type: 'TEXT', content: 'Hello ' },
{ type: 'INTERPOLATION', content: 'user.name' }
]
},
{
type: 'ELEMENT',
tag: 'small',
props: [{ type: 'DIRECTIVE', name: 'if', exp:
'user.isAdmin' }],
children: [{ type: 'TEXT', content: 'admin' }]
}
// etc.
]
};
When the parsing is done, the Vue compiler applies
transformations on the AST. The transformers
massage the AST for the codegen process, making it
look like the structure of a program, with function
calls and arguments. This is also the phase where
each directive has their logic hooked in: the
transform for v-if changes the AST to have an if
condition.
/**
* <div>
* <div>
* <span>Hello {{ user.name }}</span>
* <small v-if="user.isAdmin">admin</small>
* </div>
* <input type="checkbox" :value="user.isAdmin"
@change="updateAdminRights()" />
* </div>
*/
function render(component: Greetings): VNode {
// div
return createVirtualElement('div', {}, [
// div
createVirtualElement('div', {}, [
// span
createVirtualElement('span', {}, [
// text: Hello {{ user.name }}
createVirtualText('Hello ' + component.user.name)
]),
// small only if v-if is true
component.user.isAdmin
? createVirtualElement('small', {}, [
// text: admin
createVirtualText('admin')
])
: null
]),
// input
createVirtualElement(
'input',
{
type: 'checkbox',
value: component.user.isAdmin,
onChange: () => component.updateAdminRights()
},
[]
)
]);
}
Let’s start with the first case: the new VNode is null,
indicating that the corresponding DOM element
must be removed. We could just remove it from the
DOM, but, to make things simpler, we’ll keep the
same number of elements (and thus the same
indices), and replace it with a comment:
with createChildElement:
// first render
let previousRender = greetings.render();
// state update
greetings.user.isAdmin = true;
setup() {
const user = reactive({
name: 'Cyril',
isAdmin: true
});
function updateAdminRights() {
user.isAdmin = !user.isAdmin;
}
return () =>
h(
'div',
/* children */ [
h(
'div',
/* children */ [
h('span', 'Hello ' + user.name),
/* v-if becomes a simple condition */
user.isAdmin ? h('small', 'admin') : null
]
),
h(
'input',
/* props */ {
type: 'checkbox',
value: user.isAdmin,
onChange: () => updateAdminRights()
}
)
]
);
}
});
29.4. JSX
Coming soon
29.5. Reactivity
Now that we know how Vue renders state changes,
let’s see how Vue detects state changes.
29.5.1. getter/setter
JavaScript always had some meta-programming
capabilities. The use of get and set is a good
example.
const pony = {
get name() {
console.log('get name');
return 'Rainbow Dash';
}
};
console.log(pony.name);
// logs 'get name'
// logs 'Rainbow Dash'
Object.defineProperty(component, 'user', {
set(user) {
this.user = user;
heyVue2APropertyChanged(); //
}
});
component.newProperty = 'hello';
// won't call heyVue2APropertyChanged() 😢
const handler = {
get(obj: any, prop: any) {
console.log(`${prop} was accessed`);
return obj[prop];
}
};
const user = { name: 'Cédric' };
const proxy = new Proxy(user, handler);
console.log(proxy.name);
// logs 'name was accessed'
// logs 'Cédric'
They are not easy tools to master, but Vite and the
Vue CLI do a pretty good job at hiding their
complexity. If you don’t use Vite or the CLI, you can
build your application with Rollup or Webpack
directly, or you can pick another tool that may
produce even better results. But be warned that this
requires quite a lot of expertise (and work) to not
mess things up, just to save a few extra kilobytes. I
would recommend staying with Vite (ideally) or the
CLI.
30.4. Tree-shaking
Rollup and Webpack (or other tools you use) start
from the entry point of your application (the main.ts
file generated for you), then resolves all the imports
tree, and outputs the bundle. This is cool because the
bundle will only contain the files from your
codebase and your third party libraries that have
been imported. The rest is not embedded. So even if
you have a dependency in your package.json that
you don’t use anymore (so you don’t import it
anymore), it will not end up in the bundle.
30.7. Compression
All the modern browsers accept a compressed
version of an asset when they ask the server for it.
That means you can serve a compressed version to
your users, and the browser will unzip it before
parsing it. This is a must-do because it will save you
tons of bandwidth and loading time!
30.8. Lazy-loading
Sometimes, despite doing your best to keep your JS
bundle small, you end up with a big file because
your app has grown to several dozens of
components, using various third party libraries. And
not only will this big bundle increase the time
needed to fetch the JavaScript, it will also increase
the time needed to parse it and execute it.
The good news is that Vue (and its router) makes this
task relatively easy to achieve. You can read our
chapter about lazy-loading if you want to learn
more.
When you add a new race, Vue will add a DOM node
in the proper position. If you update the name of one
of the ponies, Vue will change just the text content of
the right li.
<ul>
<li v-for="race in races" :key="race.id">{{ race.name }}</li>
</ul>
30.13. v-memo
Vue 3.2 introduced another trick to help with
performances: the v-memo directive, which allows
you to aggressively optimize templates in some edge
cases. You can think of v-memo as an equivalent of
shouldComponentUpdate in React, but available for
elements or components in Vue.
<ul>
<li v-for="race in races" :key="race.id" v-memo="
[race.name]">{{ race.name }}</li>
</ul>
<ul>
<li v-for="race in races" :key="race.id" v-memo="[race.name,
selectedRaceId === race.id]">
<span :class="{ selected: selectedRaceId === race.id }">{{
race.name }}</span>
</li>
</ul>
<ul>
<li v-for="race in races" :key="race.id" v-memo="
[race.name]">{{ race.name }} - {{ race.country }}</li>
</ul>
This trick is really only useful for very large list, and
you should not need it for more typical use-cases.
30.14. Conclusion
This chapter hopefully taught you some techniques
which can help solve performance problems. But
remember the golden rules of performance
optimization:
▪ don’t
▪ don’t… yet
If you liked what you read, tell your friends about it!
Stay tuned.
APPENDIX A: CHANGELOG
Here are all the major changes since the first
version. It should help you to see what changed
since your last read!
Current versions:
▪ Vue: 3.3.2
Script setup
Forms
▪ Add a section about the defineModel macro
introduced in Vue v3.3. (2023-05-12)
▪ Add a section about how to build custom form
components. (2023-05-12)
Slots
Router
Custom directives
Internationalization
Lazy-loading
Performances
Script setup
Composition API
State Management
Router
Script setup
▪ New chapter about the script setup syntax! All
examples of the ebook and exercises have been
migrated to this new recommended syntax,
introduced in Vue 3.2. (2021-09-29)
Suspense
Performances
Forms
▪ VeeValidate v4.3.0 introduced a new url validator.
(2021-05-05)
Provide/inject
State Management
Forms
Suspense
Router
Slots
Router
Slots