Previously, we talked about some basic concepts in Vue, and in this article, we are going to dig deeper into this JavaScript framework, and discuss 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 article, 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 Basics
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.
<div v-on:click="someAction">...</div>
<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.
<div v-on:click="count = count + 1">...</div>
<div v-on:click="someMethod()">...</div>
Sometimes, the method requires up to pass some extra arguments.
<script>
export default {
...
methods: {
add(num) {
this.count = this.count + num
}
}
}
</script>
<template>
<p>count = {{count}}</p>
<button v-on:click="add(1)">Add 1</button>
<button v-on:click="add(5)">Add 5</button>
<button v-on:click="add(10)">Add 10</button>
<button v-on:click="add(100)">Add 100</button>
</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:
<script>
export default {
data() {...},
methods: {
...
say() {
var msg = 'count = ' + this.count
alert(msg)
}
}
}
</script>
<template>
<p>count = {{count}}</p>
<button v-on:click="add(1), say()">Add 1</button>
...
</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:
<template>
<p>count = {{count}}</p>
<button v-on:click.once="add(1)">Add 1</button>
</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.
<!-- the click event's propagation will be stopped -->
<a @click.stop="doThis"></a>
<!-- the submit event will no longer reload the page -->
<form @submit.prevent="onSubmit"></form>
<!-- modifiers can be chained -->
<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:
<template>
<!-- Right Click -->
<div v-on:click.right="doSomething">Do something</div>
<!-- Control + Click -->
<div v-on:click.ctrl="doSomething">Do something</div>
<!-- Enter Key -->
<div v-on:keyup.enter="doSomething">Do something</div>
<!-- Alt + Enter -->
<div v-on:keyup.alt.enter="doSomething">Do something</div>
</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:
<input v-model="message" />
<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:
<textarea v-model="message"></textarea>
<p>Message is: {{ message }}</p>
Checkbox
<script>
export default {
data() {
return {
checked: false
}
}
}
</script>
<template>
<input type="checkbox" v-model="checked" />
<p v-if="checked">The box is checked.</p>
<p v-else>The box is NOT checked.</p>
</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:
<script>
export default {
data() {
return {
checkedBoxes: []
}
}
}
</script>
<template>
<div id="v-model-multiple-checkboxes">
<input type="checkbox" id="one" value="one" v-model="checkedBoxes" />
<label for="one">one</label>
<input type="checkbox" id="two" value="two" v-model="checkedBoxes" />
<label for="two">two</label>
<input type="checkbox" id="mike" value="three" v-model="checkedBoxes" />
<label for="three">three</label>
<br />
<span>Checked boxes: {{ checkedBoxes }}</span>
</div>
</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.
<div id="v-model-radiobutton">
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<br />
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<br />
<span>Picked: {{ picked }}</span>
</div>
The variable picked
will be a string instead of an array.
Select
For a single select, the variable is a string type.
<script>
export default {
data() {
return {
selected: ''
}
}
}
</script>
<template>
<select v-model="selected">
<option disabled value>Please select one</option>
<!--
If you assign a 'value' attribute, that value will be assigned to the variable 'selected'
-->
<option value="aaaaaaa">A</option>
<!--
If you do not assign a value attribute, whatever is inside the <option> element
will be assigned to the variable 'selected'
-->
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</template>
<style>
</style>
For a muliselect, the variable will be bound to an array.
<script>
export default {
data() {
return {
selected: []
}
}
}
</script>
<template>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</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:
<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:
<div v-bind:class="{ class-one: isClassOneActive, class-two: isClassTwoActive }"></div>
CSS binding
We can also bind CSS codes directly like this:
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
}
</script>
<template>
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
</template>
Although it is usually better to put the object inside the data() method, so that our template section looks cleaner:
<script>
export default {
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
}
</script>
<template>
<div v-bind:style="styleObject"></div>
</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
.
<script>
export default {
data() {
return {
count: 0,
name: '',
}
}
}
</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
.
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
add(num) {
this.count = this.count + num
}
}
}
</script>
<template>
<p>count = {{ count }}</p>
<button @click="add(1)">Add 1</button>
</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.
<script>
export default {
...
computed: {
// This is a getter
showCount() {
return this.count
},
// This is a setter
resetCount() {
this.count = 0
}
}
}
</script>
<template>
<p>count = {{ showCount }}</p>
<button @click="add(1)">Add 1</button>
<button @click="resetCount()">Reset</button>
</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.
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
add(num) {
this.count = this.count + num
}
},
watch: {
count(newCount, oldCount) {
let diff = newCount - oldCount
console.log('diff = ' + diff)
}
}
}
</script>
<template>
<p>count = {{ count }}</p>
<button @click="add(1)">Add 1</button>
<button @click="add(5)">Add 5</button>
<button @click="add(10)">Add 10</button>
<button @click="add(100)">Add 100</button>
</template>
<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
<script>
export default {
...
}
</script>
<template>
<p>This is the component "ComponentOne.vue"</p>
</template>
We can use this component inside our App.vue
file:
<script>
// import the component
import ComponentOne from "./components/ComponentOne.vue"
export default {
...
// Declare the imported component
components: { ComponentOne }
}
</script>
<template>
<p>This is the root component "App.vue"</p>
<!-- Use the component here -->
<ComponentOne></ComponentOne>
</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
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
add(num) {
this.count += num
}
}
}
</script>
<template>
<p>This is the component "ComponentOne.vue"</p>
<p>count = {{count}}</p>
<button @click="add(1)">Add 1</button>
</template>
App.vue
<template>
<p>This is the root component "App.vue"</p>
<!-- Use the multiple component instances -->
<ComponentOne></ComponentOne>
<ComponentOne></ComponentOne>
...
</template>
We can also import a component inside another component, forming a nested structure.
components/ComponentOne.vue
<script>
import ComponentTwo from "./ComponentTwo.vue";
export default {
...
components: { ComponentTwo }
}
</script>
<template>
<p>This is the component "ComponentOne.vue"</p>
<!-- Import another component -->
<ComponentTwo></ComponentTwo>
</template>
components/ComponentTwo.vue
<script>
export default {
...
}
</script>
<template>
<p>This is the component of a component "ComponentTwo.vue"</p>
</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
<script>
import PostListComponent from "./components/PostListComponent.vue";
export default {
...
components: { PostListComponent },
};
</script>
<template>
<PostListComponent></PostListComponent>
</template>
components/PostListComponent.vue
<script>
import PostComponent from "./PostComponent.vue";
export default {
data() {
return {
posts: [
{ id: 1, title: "Article #1" },
{ id: 2, title: "Article #2" },
...
],
};
},
components: { PostComponent },
};
</script>
<template>
<h1>This is a list of recent articles.</h1>
<PostComponent v-for="post in posts"></PostComponent>
</template>
components/PostComponent.vue
<script>
export default {
...
}
</script>
<template>
<h2>This is the title.</h2>
</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
<template>
<h1>This is a list of recent articles.</h1>
<PostComponent v-for="post in posts" v-bind:title="post.title"></PostComponent>
</template>
components/PostComponent.vue
<script>
export default {
props: ["title"],
};
</script>
<template>
<h2>{{ title }}</h2>
</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
<script>
export default {
props: {
// type check
height: Number,
// type check plus other validations
age: {
type: Number,
default: 0,
required: true,
validator: (value) => {
return value >= 0;
},
},
},
};
</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
<script>
export default {
props: ["title"],
data() {
return {
articleTitle: this.title,
};
},
};
</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
<script>
export default {
props: ["title"],
// Declare the emited events
emits: ["enlargeText"],
};
</script>
<template>
<h2>{{ title }}</h2>
<!-- When the button is clicked, it emits a event called 'enlargeText' to the parent -->
<button v-on:click="$emit('enlargeText')">Enlarge Text</button>
</template>
<style></style>
components/PostListComponent.vue
<script>
import PostComponent from "./PostComponent.vue";
export default {
data() {
return {
posts: [
{ id: 1, title: "Article #1" },
{ id: 2, title: "Article #2" },
{ id: 3, title: "Article #3" },
{ id: 4, title: "Article #4" },
],
// Set font size
titleFontSize: 1,
};
},
components: { PostComponent },
};
</script>
<template>
<!-- Dymanically bind the CSS style -->
<div v-bind:style="{ fontSize: titleFontSize + 'em' }">
<!-- listen to the event 'enlargeText' emited from the child component -->
<PostComponent
v-for="post in posts"
v-bind:title="post.title"
v-on:enlargeText="titleFontSize += 0.1"
></PostComponent>
</div>
</template>
<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
<script>
export default {
props: ["title"],
// Declear the emited events
emits: ["changeFontSize"],
};
</script>
<template>
<h2>{{ title }}</h2>
<!--
The attribute 'value' binds with the user input, its initisl value is 1.
$event.target.value contains the current value of 'value'
-->
<input
type="text"
v-bind:value="1"
v-on:change="$emit('changeFontSize', $event.target.value)"
/>
</template>
components/PostListComponent.vue
<script>
import PostComponent from "./PostComponent.vue";
export default {
data() {
return {
posts: [{ id: 1, title: "Article #1" }],
...
titleFontSize: 1,
};
},
components: { PostComponent },
};
</script>
<template>
<div v-bind:style="{ fontSize: titleFontSize + 'em' }">
<!--
listen to the event 'changeFontSize' emited from the child component,
and the variable $event contains the data that is transferred with the event.
-->
<PostComponent
v-for="post in posts"
v-bind:title="post.title"
v-on:changeFontSize="titleFontSize = $event"
></PostComponent>
</div>
</template>
If you liked this article, please also check out my other tutorials:
Top comments (0)