Vue is a frontend JavaScript framework that helps us quickly create user interfaces. It is more lightweight and beginner-friendly compared to other frameworks such as React or Angular. The core library of Vue focuses on the view layer only, which is the part that the users can see. That is also why the author named the framework Vue (pronounced as view).
Installation
To create a new Vue application, go to the terminal and run the following command:
1npm init vue@latest
This command will prompt you with multiple options such as TypeScript, Vue Router, and Pinia. For this tutorial, we don't need any of these features, but you can play around with them if you want.
1✔ Project name: … <your-project-name>
2✔ Add TypeScript? … No / Yes
3✔ Add JSX Support? … No / Yes
4✔ Add Vue Router for Single Page Application development? … No / Yes
5✔ Add Pinia for state management? … No / Yes
6✔ Add Vitest for Unit testing? … No / Yes
7✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
8✔ Add ESLint for code quality? … No / Yes
9✔ Add Prettier for code formatting? … No / Yes
10
11Scaffolding project in ./<your-project-name>...
12Done.
Next, go to the project folder, install the necessary packages, and run the development server.
1cd <your-project-name>
2npm install
3npm run dev
Open your browser and go to http://localhost:3000/
, you should see the welcome page.
Introduction
Before we start, let's take a look at what has been installed into our project folder.
There are a few things we are already familiar with. The node_modules
contains the packages we installed. The public
folder contains the files and resources that we wish to make public. The package-lock.json
and package.json
files are both for managing packages, which we talked about before in the JavaScript Basics tutorial, and the index.html
file is the start point of our project.
For this tutorial, we'll only focus on the files inside the src
directory. The assets
folder stores the images, CSS files and other resources. The main.js
file mounts and configures all the Vue apps in our project, and it is also the script that we import into the index.html
file.
The App.vue
is the actual Vue app, this is where we do most of the coding. However, sometimes the app gets too big, it makes more sense if we divide the app into multiple components, we'll store these components inside the components
folder.
We'll take a closer look at what happens when you go to http://localhost:3000/
. Let's start from index.html
, and notice what we have inside the <body>
tag.
1<body>
2 <div id="app"></div>
3</body>
The only line of the code that matters is <div id="app"></div>
. Why? Let's go to main.js
.
1import { createApp } from "vue";
2import App from "./App.vue";
3
4createApp(App).mount("#app");
This file imports the Vue app, and mounts that app to the HTML element with id="app"
. Recall that #
represents id and .
represents class. This is why that <div>
element is so important, even though it is empty.
Next, go to the App.vue
file:
1<script setup>
2import HelloWorld from "./components/HelloWorld.vue";
3</script>
4
5<template>
6 <img alt="Vue logo" src="./assets/logo.png" />
7 <HelloWorld msg="Hello Vue 3 + Vite" />
8</template>
9
10<style>
11#app {
12 font-family: Avenir, Helvetica, Arial, sans-serif;
13 ...;
14}
15</style>
We immediately see that the file is divided into three sections. The <script>
section contains the JavaScript code, the <template>
contains HTML elements, and <style>
contains CSS codes.
Notice in the <script>
section, we imported a component from the components
folder and used it in the <template>
section.
And finally, go to the HelloWorld
component. You can see that it has the exact same structure. You can also try to edit something inside this file and see if the webpage changes.
Basics
Now, let's go back to the App.vue
file, and delete everything unnecessary so we'll have a clean and empty vue document.
1<script></script>
2
3<template></template>
4
5<style></style>
Methods and properties
As you know, the <script>
section is where we write JavaScript code, but since Vue is a framework, there are a few restrictions and requirements. This section usually has the following structure:
App.vue
1<script>
2export default {
3 data() {
4 return {
5 name: "value"
6 }
7 },
8
9 methods: {
10 xxx() {...}
11 },
12 ...
13}
14</script>
This means when you are importing this Vue app into main.js
, you are actually importing a bunch of methods and properties. Each property/method serves a different purpose.
For example, the data()
method returns an object containing all the variables that are used in the app. Be careful that data
must be a method and not just a property, this is what makes Vue reactive, meaning if the value of the variable changes, the web pages changes without having to reload. The methods
property contains all the methods that are created by you, the coder. Of course, there are other properties allowed such as props
, computed
, inject
and setup
. We'll discuss them in detail in the future.
A simple counter app
Knowing just these two simple concepts, the data
method and the methods
property, is enough for us to start creating apps. For example, we'll create an app that counts how many times a button has been clicked.
App.vue
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8};
9</script>
10
11<template>
12 <button v-on:click="count++">click me</button>
13 <p>count = {{ count }}</p>
14</template>
First, we declare a variable count
, whose initial value is 0, and in the <template>
section, we set up an event listener (v-on:click
), each time the button is clicked, count
increments by 1. The variable will then be rendered using double curly braces ({{ }}
). We'll talk about these syntaxes later.
What if we want another button that resets the value of count
? This is what we can do:
App.vue
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8
9 methods: {
10 clear() {
11 this.count = 0;
12 },
13 },
14};
15</script>
16
17<template>
18 <button v-on:click="count++">click me</button>
19 <button v-on:click="clear()">clear</button>
20 <p>count = {{ count }}</p>
21</template>
Remember to use the keyword this
when referring to variables that belong to this application instance. The variable that we defined in the data method is unique to this instance, meaning it cannot be accessed by other instances or components. For example, we can create another counter, and import it into App.vue
as a component.
components/Counter.vue
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8 methods: {
9 clear() {
10 this.count = 0;
11 },
12 },
13 components: { Counter },
14};
15</script>
16
17<template>
18 <button v-on:click="count++">click me</button>
19 <button v-on:click="clear()">clear</button>
20 <p>count = {{ count }}</p>
21</template>
22
23<style></style>
App.vue
1<script>
2import Counter from "./components/Counter.vue";
3export default {
4 data() {
5 return {
6 count: 0,
7 };
8 },
9 methods: {
10 clear() {
11 this.count = 0;
12 },
13 },
14 components: { Counter },
15};
16</script>
17
18<template>
19 <button v-on:click="count++">click me</button>
20 <button v-on:click="clear()">clear</button>
21 <p>count = {{ count }}</p>
22
23 <Counter />
24</template>
25
26<style></style>
Try this in your own browser, and you will find that even though the variable we defined for Counter.vue
and App.vue
are both counter
, they do not seem to affect each other, and when you reset the variable's value, only the one in the same instance becomes 0.
Lifecycles
Finally, I'd like to introduce another important concept in Vue, it's called lifecycles.
When an app instance is been created, it goes through a series of processes, such as initializing data, compiling the template, mounting the template onto the DOM, and updating the template as the data changes. This allows us to divide the life of an application instance into several stages, and Vue provides us with several lifecycle hooks that allow us to add our own code at different stages.
For example, the function created()
allows us to add code that is supposed to run right after the instance has been created.
1<script>
2export default {
3 data() {
4 return { count: 1 };
5 },
6 created() {
7 console.log("initial count is: " + this.count);
8 },
9};
10</script>
There are other lifecycle hooks that we could use. Here is a diagram showing all of them and where they are in the lifecycle.
Interpolations
In the first section of this tutorial, we learned that a Vue file is divided into three sections, <template>
, <script>
, and <style>
. However, we merely touched the surface of Vue.js last time, starting from now we are going to talk about the details of each of these sections, and we'll start with the easiest, the template section.
We know that the template section only contains HTML codes, it shows what the Vue file will eventually be rendered into. However, it can't be that simple, since we want the page to be reactive, we want it to change as the data changes. To do that, we need to inform Vue.js where to put the data.
Text
Text interpolation is the most basic form of data binding, which uses double curly braces like this:
1<script>
2export default {
3 data() {
4 return { msg: "This is a message." };
5 },
6};
7</script>
8
9<template>
10 <p>Message: {{ msg }}</p>
11</template>
Try to change the value of msg
, and you'll see that the page changes without having to be refreshed.
Raw HTML
However, what if we want the data to be more complex? Say we want to bind a piece of HTML code to a variable, see what happens when you try to output HTML with double curly braces:
1<script>
2export default {
3 data() {
4 return { msg: '<span style="color: red">This is a message.</span>' };
5 },
6};
7</script>
8
9<template>
10 <p>Message: {{ msg }}</p>
11</template>
The data will be treated as plain text instead of HTML codes. To solve this problem, we need to tell Vue.js that the data we are trying to render is HTML, by using an HTML directive:
1<p>Message: <span v-html="msg"></span></p>
This time, when the data is being rendered, the original <span>
tag will be replaced.
Attributes
Sometimes it might be useful if we bind an attribute to a variable. For instance, we want to enable a button when the user is verified, and disable it when the user is not verified. We can bind the disabled
attribute to the verified
variable by using the v-bind
directive.
1<script>
2export default {
3 data() {
4 return { verified: false };
5 },
6};
7</script>
8
9<template>
10 <button v-bind:disabled="!verified">Button</button>
11</template>
Remember that the exclamation mark (!
) inverts the value of varified
.
JavaScript expressions
It is also possible for us to use simple JavaScript expressions inside the template. In fact, the !varified
we just saw is a very simple example. We can also do something more complicated like these:
1{{ number + 1 }}
2
3{{ ok ? "YES" : "NO" }}
4
5{{ message.split("").reverse().join("") }}
6
7<div v-bind:id="'list-' + id"></div>
However, there are some restrictions, for example, statements that declare new variables are not going to work. Loops and flow controls (if
statements) are not going to work either.
Directives
In Vue.js, directives are special attributes with the prefix v-
. Their primary function is to bind a side effect to a DOM node.
For instance, the following example binds a variable to the <p>
element using the v-if
directive. It works just like a regular if
statement. When verified
is true
, the first <p>
element will be rendered, and when verified
is false
, the second <p>
element will be rendered.
1<p v-if="verified">You are verified.</p>
2<p v-if="!verified">You are NOT verified.</p>
Arguments
Some directives can take extra arguments. For example, the v-bind
directive, which we've already seen, is used to bind an HTML attribute to a variable and it takes the name of that attribute as an argument.
1<p v-bind:color="colorName">...</p>
2<button v-bind:class="className">click me</button>
Another example is the v-on
directive. It is the event listener in Vue.js.
1<a v-on:click="action">...</a>
When this link is clicked, the function that is bonded to the variable action
will be executed.
It is also possible to bind the argument itself to a variable. For example:
1<a v-on:[event]="action">...</a>
In this case, if var event = "click"
, this example will be equivalent to v-on:click="action"
.
In fact, v-bind
and v-on
are the two most commonly used directives, that is why Vue.js has created special shortcuts for them. The v-bind
can be shortened to just a colon (:
), and v-on
can be represented using just @
.
The following codes are equivalent:
1<a v-bind:href="url">...</a>
2<a :href="url">...</a>
1<a v-on:click="action">...</a>
2<a @click="action">...</a>
Flow Control
Next, let's talk about the if
statements in Vue. Like we've seen before, the v-if
directive binds the element with a boolean value. If the boolean value is true
, the element will be rendered, and if it is false
, the element will simply be ignored by Vue.
Other than v-if
, there is also a v-else
directive, which works with the v-if
directive:
1<p v-if="verified">You are verified.</p>
2<p v-else>You are NOT verified.</p>
What if you need more than just two conditions? The v-else-if
directive, as the name suggests, creates an else if
block. It can be chained multiple times, hence creating multiple conditions.
1<p v-if="num === 1">The number is 1.</p>
2<p v-else-if="num === 2">The number is 2.</p>
3<p v-else-if="num === 3">The number is 3.</p>
4<p v-else-if="num === 4">The number is 4.</p>
5<p v-else>The number is 5.</p>
Loops
Finally, other than if
statements, Vue also allows us to create simple for
loops inside the template. Its syntax actually resembles the for
loops in Python, if you are familiar with the language.
We can render a list of items in an array like this:
1<script>
2export default {
3 data() {
4 return {
5 items: [{ num: 1 }, { num: 2 }, { num: 3 }, { num: 4 }, { num: 5 }],
6 };
7 },
8};
9</script>
10
11<template>
12 <ul>
13 <li v-for="item in items">The number is {{ item.num }}.</li>
14 </ul>
15</template>
Vue also supports an optional second argument for index number:
1<template>
2 <ul>
3 <li v-for="(item, index) in items">
4 #{{ index }} - The number is {{ item.num }}.
5 </li>
6 </ul>
7</template>
Previously, we talked about some basic concepts in Vue, and now, we are going to dig deeper into this JavaScript framework, and event handling, data options, and components.
Event handling
From our course on JavaScript basics, we learned that event handling is the most important concept in frontend development, and Vue.js, being a JavaScript frontend framework must have the same concept built in.
In this tutorial, we are going to focus on two aspects, event handling with the directive v-on
, and form input handling with the directive v-model
. And before we could start talking about the script section of Vue.js, we are going to quickly go through style bindings, and class bindings.
An event is a user input, it could be a keyboard input or a mouse click, the user would usually expect some kind of response after the event takes place. The event handler listens to that event and it would perform some actions in the background and return something as the response. If you are not familiar with what an event is, there is a detailed explanation here: JavaScript and Event Handlers
The v-on
directive, which we can shorten to just the @
symbol, is used to listen to events in Vue.js. We can use it to specify what kind of event we are listening to, and what kind of action we are going to take after this event has been received.
1<div v-on:click="someAction">...</div>
2<div @click="someAction">...</div>
That someAction
could be a simple JavaScript expression or a very complicated method, which allows us to build more complex logic.
1<div v-on:click="count = count + 1">...</div>
2<div v-on:click="someMethod()">...</div>
Sometimes, the method requires up to pass some extra arguments.
1<script>
2export default {
3 ...
4 methods: {
5 add(num) {
6 this.count = this.count + num
7 }
8 }
9}
10</script>
11
12<template>
13 <p>count = {{ count }}</p>
14 <button v-on:click="add(1)">Add 1</button>
15 <button v-on:click="add(5)">Add 5</button>
16 <button v-on:click="add(10)">Add 10</button>
17 <button v-on:click="add(100)">Add 100</button>
18</template>
It is also possible for one event to trigger multiple event handlers, and the handlers are separated using a comma. For example, this time, when a button is clicked, the browser will pop out an alert box as well as re-render the webpage:
1<script>
2export default {
3 data() {...},
4
5 methods: {
6 ...
7 say() {
8 var msg = 'count = ' + this.count
9 alert(msg)
10 }
11 }
12}
13</script>
14
15<template>
16 <p>count = {{ count }}</p>
17 <button v-on:click="add(1), say()">Add 1</button>
18 ...
19</template>
Modifiers
Modifiers are used to pass along extra details about the event. For example, we can use the .once
modifier to tell Vue that this event will only be triggered once:
1<template>
2 <p>count = {{ count }}</p>
3 <button v-on:click.once="add(1)">Add 1</button>
4</template>
This time, the "Add 1" button will only work once.
There are some other modifiers such as .prevent
, which stops the default action of an event. Or .stop
, which stops the event propagation. If you don't know what they are, please read the article on Event Handling in the JavaScript course.
1<!-- the click event's propagation will be stopped -->
2<a @click.stop="doThis"></a>
3
4<!-- the submit event will no longer reload the page -->
5<form @submit.prevent="onSubmit"></form>
6
7<!-- modifiers can be chained -->
8<a @click.stop.prevent="doThat"></a>
There is also a different type of modifier which makes the event handler listen to events from a specific key or a mouse button, or any of the combinations:
1<template>
2 <!-- Right Click -->
3 <div v-on:click.right="doSomething">Do something</div>
4
5 <!-- Control + Click -->
6 <div v-on:click.ctrl="doSomething">Do something</div>
7
8 <!-- Enter Key -->
9 <div v-on:keyup.enter="doSomething">Do something</div>
10
11 <!-- Alt + Enter -->
12 <div v-on:keyup.alt.enter="doSomething">Do something</div>
13</template>
Form input binding
The form is a very important component in web development, it provides a portal for the user to communicate with the backend. However, we know from our course on HTML Forms that forms could have a lot of different types of inputs, and each of them is associated with a different data type. It would be a pain in the neck if we try to process all those data types one by one.
Luckily, with Vue.js, we can use one single directive, v-model
, to bind all the input data, regardless of their data types. For instance, here we have a standard text input:
1<input v-model="message" />
2<p>Message is: {{ message }}</p>
Here the user input has the type string
, and it will be bound to the variable massage
.
Multiline text input works exactly the same:
1<textarea v-model="message"></textarea>
2<p>Message is: {{ message }}</p>
Checkbox
1<script>
2export default {
3 data() {
4 return {
5 checked: false,
6 };
7 },
8};
9</script>
10
11<template>
12 <input type="checkbox" v-model="checked" />
13 <p v-if="checked">The box is checked.</p>
14 <p v-else>The box is NOT checked.</p>
15</template>
As for the checkbox, the user input is a Boolean value, either true
or false
. In this example, the user input is bound to the variable checked
, and the directive v-if
will be used to check the truthiness of checked
.
However, sometimes in a form, there are multiple checkboxes, which means having only two values (true
or false
) would not be enough. In this case, we'll need to add a value
attribute to each of the checkboxes:
1<script>
2export default {
3 data() {
4 return {
5 checkedBoxes: [],
6 };
7 },
8};
9</script>
10
11<template>
12 <div id="v-model-multiple-checkboxes">
13 <input type="checkbox" id="one" value="one" v-model="checkedBoxes" />
14 <label for="one">one</label>
15 <input type="checkbox" id="two" value="two" v-model="checkedBoxes" />
16 <label for="two">two</label>
17 <input type="checkbox" id="mike" value="three" v-model="checkedBoxes" />
18 <label for="three">three</label>
19 <br />
20 <span>Checked boxes: {{ checkedBoxes }}</span>
21 </div>
22</template>
Notice this time, the variable checkedBoxes
is bound to an array, and when a box is checked, its value (whatever you assigned to its value
attribute) will be appended to that array.
Radio
Radio is kind of like a multi-checkboxes group, except, you can only pick one option. So in this case, the user input will always be a single string.
1<div id="v-model-radiobutton">
2 <input type="radio" id="one" value="One" v-model="picked" />
3 <label for="one">One</label>
4 <br />
5 <input type="radio" id="two" value="Two" v-model="picked" />
6 <label for="two">Two</label>
7 <br />
8 <span>Picked: {{ picked }}</span>
9</div>
The variable picked
will be a string instead of an array.
Select
For a single select, the variable is a string type.
1<script>
2export default {
3 data() {
4 return {
5 selected: "",
6 };
7 },
8};
9</script>
10
11<template>
12 <select v-model="selected">
13 <option disabled value>Please select one</option>
14 <!--
15 If you assign a 'value' attribute, that value will be assigned to the variable 'selected'
16 -->
17 <option value="aaaaaaa">A</option>
18 <!--
19 If you do not assign a value attribute, whatever is inside the <option> element
20 will be assigned to the variable 'selected'
21 -->
22 <option>B</option>
23 <option>C</option>
24 </select>
25 <span>Selected: {{ selected }}</span>
26</template>
27
28<style></style>
For a muliselect, the variable will be bound to an array.
1<script>
2export default {
3 data() {
4 return {
5 selected: [],
6 };
7 },
8};
9</script>
10
11<template>
12 <select v-model="selected" multiple>
13 <option>A</option>
14 <option>B</option>
15 <option>C</option>
16 </select>
17 <span>Selected: {{ selected }}</span>
18</template>
Style binding
Class binding
From our course on CSS Basics, we know that class is how we can assign the same CSS code to different HTML elements, and by changing the class name, we can easily change the CSS code associated with that element.
We can change the class name of an HTML element dynamically in Vue.js like this:
1<div v-bind:class="{ active: isActive }"></div>
In this example, active
is a class name, and isActive
is a variable with a Boolean value. If isActive
is true
, then the class name active
will be rendered.
We can have multiple class names in here:
1<div
2 v-bind:class="{ class-one: isClassOneActive, class-two: isClassTwoActive }"></div>
CSS binding
We can also bind CSS codes directly like this:
1<script>
2export default {
3 data() {
4 return {
5 activeColor: "red",
6 fontSize: 30,
7 };
8 },
9};
10</script>
11
12<template>
13 <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
14</template>
Although it is usually better to put the object inside the data() method, so that our template section looks cleaner:
1<script>
2export default {
3 data() {
4 return {
5 styleObject: {
6 color: "red",
7 fontSize: "13px",
8 },
9 };
10 },
11};
12</script>
13
14<template>
15 <div v-bind:style="styleObject"></div>
16</template>
Now, it is finally time for us to dive into the most important part of this course, the script section of a Vue application. To master a web framework, the most important step is to understand how data could circulate inside your project, and how different types of data are treated differently. That would be the focus of this article.
In this article, we are going to talk about several different types of data options. Do not confuse data options with the data
method we talked about before. The data
method is a method where we declare a list of variables that we are going to use in the component instance, and data options is a collection of properties and methods that deals with data in Vue.js, which includes the data
method.
After that, we are going to discuss the lifecycle hooks, which are interfaces that allow us to inject codes at different stages of a component instance's creation.
Data options
data
First of all, the data
method. Like we've seen over and over again, it is a method that returns an object, and inside that object, we define all the variables we need for this component instance. Vue.js will automatically wrap these variables inside its reactivity system, meaning that when the value of the variable changes, the webpage automatically rerenders to reflect the changes.
The variables are only added as the instance was being created. You can, in fact, assign variables after the instance has already been created, but that variable will not be a part of the reactivity system. So, you should always create them inside the data
method, if there isn't an initial value, you can use a placeholder value such as null
or undefined
.
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 name: "",
7 };
8 },
9};
10</script>
methods
The methods
is another data option we are already familiar with. It is the place where we define all the logic for our application. When you create a method, Vue.js will automatically bind the this
keyword to that method. So, to access the value of a variable for the current instance, you need to use this.variableName
.
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8
9 methods: {
10 add(num) {
11 this.count = this.count + num;
12 },
13 },
14};
15</script>
16
17<template>
18 <p>count = {{ count }}</p>
19 <button @click="add(1)">Add 1</button>
20</template>
computed
The computed
property is very similar to the methods
property. It is also a place for us to store methods that deal with data. However, computed is usually for getters and setters. The getters are methods that return the value of a variable, and setters are methods that assign a new value for a variable.
1<script>
2export default {
3 ...
4 computed: {
5 // This is a getter
6 showCount() {
7 return this.count
8 },
9 // This is a setter
10 resetCount() {
11 this.count = 0
12 }
13 }
14}
15</script>
16
17<template>
18 <p>count = {{ showCount }}</p>
19 <button @click="add(1)">Add 1</button>
20 <button @click="resetCount()">Reset</button>
21</template>
It seems like we could have done this with methods
, so why does Vue have both methods
and computed
, and what exactly is their difference? The two approaches here indeed produce the same result, their difference, however, is that the computed
is cached while the methods
is not.
When a computed
method is invoked, the computations will perform once and the result will be stored in the cache. The method will not reevaluate as long as the variables that it depends on have not changed. While in the case of methods
, every time a re-render happens, it will perform the computation all over again.
Using computed can save you a lot of trouble if you are dealing with a large amount of data that would be very expensive to compute over and over again.
watch
The watch
property defines methods that will run whenever a variable changes its value. It essentially provides us with a way to customize our own reactivity system.
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8
9 methods: {
10 add(num) {
11 this.count = this.count + num;
12 },
13 },
14
15 watch: {
16 count(newCount, oldCount) {
17 let diff = newCount - oldCount;
18 console.log("diff = " + diff);
19 },
20 },
21};
22</script>
23
24<template>
25 <p>count = {{ count }}</p>
26 <button @click="add(1)">Add 1</button>
27 <button @click="add(5)">Add 5</button>
28 <button @click="add(10)">Add 10</button>
29 <button @click="add(100)">Add 100</button>
30</template>
31
32<style></style>
In this example, whenever the value of count
changes, the page will not only re-render, it will also output a message in the console, telling you the difference between the old value and the new value. Rember that the name of the method and the name of the variable must match.
That's not all, in fact, there are three other data option, props
, emit
and expose
. However, to understand these data options, we need to first dig deeper into the component system of Vue.js. We'll talk about them in the next article.
Lifecycle hooks
Lifecycle Hooks | Details |
beforeCreate | Called immediately after the component instance has been initialized. This is before the data and the event listener has been setup. You cannot access them at this stage. |
created | This is after the component has been created, and the data options has been processed. However, the mounting has not started, which means the component hasn't yet appeared on the webpage. |
beforeMount | Right before the mounting starts. |
mounted | Called after the mounting has finished. This does not guarantee that all child components has been rendered. |
beforeUpdate | After the data has changed but before the DOM structure change. This would be a good place to access the existing DOM before any changes happen. |
updated | Called after the DOM has been re-rendered. It is usually better to use watchers to react to data change instead. |
In the final part of the course, we are going to investigate the component system of Vue.js. Here is an example of a component.
components/ComponentOne.vue
1<script>
2export default {
3 ...
4}
5</script>
6
7<template>
8 <p>This is the component "ComponentOne.vue"</p>
9</template>
We can use this component inside our App.vue
file:
1<script>
2// import the component
3import ComponentOne from "./components/ComponentOne.vue"
4
5export default {
6 ...
7 // Declare the imported component
8 components: { ComponentOne }
9}
10</script>
11
12<template>
13 <p>This is the root component "App.vue"</p>
14
15 <!-- Use the component here -->
16 <ComponentOne></ComponentOne>
17</template>
Components are reusable, we can create multiple instances of the same component on one page. And they are all independent of each other, if the state of one instance changes, it will not affect the others.
components/ComponentOne.vue
1<script>
2export default {
3 data() {
4 return {
5 count: 0,
6 };
7 },
8 methods: {
9 add(num) {
10 this.count += num;
11 },
12 },
13};
14</script>
15
16<template>
17 <p>This is the component "ComponentOne.vue"</p>
18 <p>count = {{ count }}</p>
19 <button @click="add(1)">Add 1</button>
20</template>
App.vue
1<template>
2 <p>This is the root component "App.vue"</p>
3
4 <!-- Use the multiple component instances -->
5 <ComponentOne></ComponentOne>
6 <ComponentOne></ComponentOne>
7 ...
8</template>
We can also import a component inside another component, forming a nested structure.
components/ComponentOne.vue
1<script>
2import ComponentTwo from "./ComponentTwo.vue";
3
4
5export default {
6 ...
7 components: { ComponentTwo }
8}
9</script>
10
11<template>
12 <p>This is the component "ComponentOne.vue"</p>
13
14 <!-- Import another component -->
15 <ComponentTwo></ComponentTwo>
16</template>
components/ComponentTwo.vue
1<script>
2export default {
3 ...
4}
5</script>
6
7<template>
8 <p>This is the component of a component "ComponentTwo.vue"</p>
9</template>
Organizing components
In fact, it is very common for a real-life application to have a nested structure like this:
For example, here we are trying to build a frontend for a blog, and we need a list of recent articles. In real-life applications, the data is usually stored inside a database, and we have a backend that will retrieve the correct information and send it to the frontend. For now, we'll just assume we have a fully functional backend, and the data has already been sent to us.
App.vue
1<script>
2import PostListComponent from "./components/PostListComponent.vue";
3
4export default {
5 ...
6 components: { PostListComponent },
7};
8</script>
9
10<template>
11 <PostListComponent></PostListComponent>
12</template>
components/PostListComponent.vue
1<script>
2import PostComponent from "./PostComponent.vue";
3
4export default {
5 data() {
6 return {
7 posts: [
8 { id: 1, title: "Article #1" },
9 { id: 2, title: "Article #2" },
10 ...
11 ],
12 };
13 },
14 components: { PostComponent },
15};
16</script>
17
18<template>
19 <h1>This is a list of recent articles.</h1>
20
21 <PostComponent v-for="post in posts"></PostComponent>
22</template>
components/PostComponent.vue
1<script>
2export default {
3 ...
4}
5</script>
6
7<template>
8 <h2>This is the title.</h2>
9</template>
As you can see, we only have one PostComponent.vue
, and it is reused multiple times using a v-for
loop. This will save us a lot of trouble since we don't have to rewrite the same code over and over again.
Passing data to child
Now we face a new problem, we know that by default, component instances are isolated from each other, the data change in one instance does not affect others since we cannot access the data in another instance. However, what if we need that to happen?
For instance, in our previous example, in the place where it should be the title of the article, we had to use a placeholder text instead, because the data about the post are in the parent component (PostListComponent.vue
), and we cannot access them in the child component (PostComponent.vue
). We need to somehow pass the data from the parent to the child.
That can be achieved using the props
option.
components/PostListComponent.vue
1<template>
2 <h1>This is a list of recent articles.</h1>
3 <PostComponent
4 v-for="post in posts"
5 v-bind:title="post.title"></PostComponent>
6</template>
components/PostComponent.vue
1<script>
2export default {
3 props: ["title"],
4};
5</script>
6
7<template>
8 <h2>{{ title }}</h2>
9</template>
Let's take a closer look at how data flows in this example. First, we bind the title of the post to the variable title
, and pass that variable to the PostComponent
. The PostComponent
receives the variable title
with props
property, and then uses it in the template.
It is also possible for us to validate the transferred data in the child component using the object syntax instead of an array.
components/PostComponent.vue
1<script>
2export default {
3 props: {
4 // type check
5 height: Number,
6 // type check plus other validations
7 age: {
8 type: Number,
9 default: 0,
10 required: true,
11 validator: (value) => {
12 return value >= 0;
13 },
14 },
15 },
16};
17</script>
However, it doesn't matter which syntax you are using, this data flow is one way only. It is always from the parent to the child, if the parent changes, the child changes, but not the other way around. You should not try to update a props
in a child component. Instead, the best practice is to declare a new variable in the child and use the transferred props
as its initial value.
components/PostComponent.vue
1<script>
2export default {
3 props: ["title"],
4 data() {
5 return {
6 articleTitle: this.title,
7 };
8 },
9};
10</script>
Passing event to parent
When we are building a web application, sometimes it is necessary to communicate from the child component to the parent component. For example, let's go back to our post list example, this time we add a button in the PostComponent
, and every time the user clicks on the button, it enlarges the font for the entire page.
To do this, it would require us to transfer the click event which happens in the child component to the parent component, and when the parent component catches that event, it would change the value of the corresponding variable (the variable that stores the size of the font). This can be done using the emits
property.
components/PostComponent.vue
1<script>
2export default {
3 props: ["title"],
4
5 // Declare the emited events
6 emits: ["enlargeText"],
7};
8</script>
9
10<template>
11 <h2>{{ title }}</h2>
12 <!-- When the button is clicked, it emits a event called 'enlargeText' to the parent -->
13 <button v-on:click="$emit('enlargeText')">Enlarge Text</button>
14</template>
15
16<style></style>
components/PostListComponent.vue
1<script>
2import PostComponent from "./PostComponent.vue";
3
4export default {
5 data() {
6 return {
7 posts: [
8 { id: 1, title: "Article #1" },
9 { id: 2, title: "Article #2" },
10 { id: 3, title: "Article #3" },
11 { id: 4, title: "Article #4" },
12 ],
13
14 // Set font size
15 titleFontSize: 1,
16 };
17 },
18 components: { PostComponent },
19};
20</script>
21
22<template>
23 <!-- Dymanically bind the CSS style -->
24 <div v-bind:style="{ fontSize: titleFontSize + 'em' }">
25 <!-- listen to the event 'enlargeText' emited from the child component -->
26 <PostComponent
27 v-for="post in posts"
28 v-bind:title="post.title"
29 v-on:enlargeText="titleFontSize += 0.1"></PostComponent>
30 </div>
31</template>
32
33<style></style>
The event starts from the child component, when the button is clicked, it emits an event called enlargeText
using a built-in function $emit
, and that event is declared in the script section using the emits
property. And when the event gets caught by the parent component, the parent changes the value of the variable titleFontSize
.
Now, what if we want to try something more complex? What if we want to specify font size using a text box instead of just a button? This would require us to transfer some data to the parent along with the event.
components/PostComponent.vue
1<script>
2export default {
3 props: ["title"],
4
5 // Declear the emited events
6 emits: ["changeFontSize"],
7};
8</script>
9
10<template>
11 <h2>{{ title }}</h2>
12 <!--
13 The attribute 'value' binds with the user input, its initisl value is 1.
14 $event.target.value contains the current value of 'value'
15 -->
16 <input
17 type="text"
18 v-bind:value="1"
19 v-on:change="$emit('changeFontSize', $event.target.value)" />
20</template>
components/PostListComponent.vue
1<script>
2import PostComponent from "./PostComponent.vue";
3
4export default {
5 data() {
6 return {
7 posts: [{ id: 1, title: "Article #1" }],
8 ...
9
10 titleFontSize: 1,
11 };
12 },
13 components: { PostComponent },
14};
15</script>
16
17<template>
18 <div v-bind:style="{ fontSize: titleFontSize + 'em' }">
19 <!--
20 listen to the event 'changeFontSize' emited from the child component,
21 and the variable $event contains the data that is transferred with the event.
22 -->
23 <PostComponent
24 v-for="post in posts"
25 v-bind:title="post.title"
26 v-on:changeFontSize="titleFontSize = $event"></PostComponent>
27 </div>
28</template>