DEV Community

Cover image for Vue.js for Beginners #2
Eric Hu
Eric Hu

Posted on • Originally published at ericsdevblog.com

Vue.js for Beginners #2

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

App.vue

<template>
  <p>This is the root component "App.vue"</p>


  <!-- Use the multiple component instances -->
  <ComponentOne></ComponentOne>
  <ComponentOne></ComponentOne>
  ...
</template>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

components/ComponentTwo.vue

<script>
export default {
    ...
}
</script>

<template>
    <p>This is the component of a component "ComponentTwo.vue"</p>
</template>
Enter fullscreen mode Exit fullscreen mode

Organizing components

In fact, it is very common for a real-life application to have a nested structure like this:

Organizing Vue Components

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

components/PostComponent.vue

<script>
export default {
  ...
}
</script>

<template>
  <h2>This is the title.</h2>
</template>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

components/PostComponent.vue

<script>
export default {
  props: ["title"],
};
</script>

<template>
  <h2>{{ title }}</h2>
</template>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

If you liked this article, please also check out my other tutorials:

Top comments (0)