Building Vue.js Applications With TypeScript - Part Two

A while ago, I posted an article titled "Building VueJS Applications with TypeScript". If you haven't seen that yet, make sure you take a look. You can find it by clicking here.

At the end of that post, I noted that I would be writing a follow-up article explaining methods, props, child components, etc. While it has been a while since I posted that, this is the second post in the series.

In this article, we will carry on where we left off. But we are going to create a very simple to-do list application to demonstrate a variety of concepts. Let's get started.

Setting Up

The first thing that we need to do is to create a new component for our to-do list. Within your src/components folder, create a new file called ToDo.ts and add the following boilerplate.

import { Component, Vue } from 'vue-property-decorator';
import WithRender from './to-do.html';

@WithRender
@Component
export default class ToDo extends Vue {

}

Also, create a new file in the same directory called to-do.html. Here is some very basic HTML to get us started.

<div>
  <h1>My To-Do App!</h1>
  <form>
    <input type="text" placeholder="Enter task...">
    <button type="submit">Add Task</button>
  </form>
</div>

Finally, we need to update our Home.vue file within the views directory so that it loads our new ToDo.ts component. Update the component to look like this.

<template>
 <div class="home">
  <img alt="Vue logo" src="../assets/logo.png">
  <ToDo />
 </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ToDo from '@/components/ToDo.ts';

@Component({
 components: {
  ToDo,
 },
})
export default class Home extends Vue {}
</script>

Our First Component

The first thing I would like to do is abstract our form to its own component. Let's create a new component called ToDoForm.ts and copy the HTML template also. We should have two new files that look like this.

ToDoForm.ts

import { Component, Vue } from 'vue-property-decorator';
import WithRender from './to-do-form.html';

@WithRender
@Component
export default class ToDoForm extends Vue {

}

to-do-form.html

<form>
  <input type="text" placeholder="Enter task...">
  <button type="submit">Add Task</button>
</form>

Now that we have abstracted the component, we need to include this child component within our parent. In order to do this, we need to import the component into the TypeScript file, register it and then update the template to display it. Let's go through that now. To get started, import the ToDoForm.ts component into the ToDo.ts component.

import ToDoForm from './ToDoForm';

Next, we need to register the component. We can do this by passing an object to our @Component decorator. Here we can configure the component like we would any normal Vue component.

@Component({
 components: {
  'to-do-form': ToDoForm
 }
})

Our ToDo.ts file should now look like this:

import { Component, Vue } from 'vue-property-decorator';
import WithRender from './to-do.html';
import ToDoForm from './ToDoForm';

@WithRender
@Component({
 components: {
  'to-do-form': ToDoForm
 }
})
export default class ToDo extends Vue {

}

The final step is to now update our to-do.html template so that we are referencing the child component. Simply remove the form and replace it with a <to-do-form /> tag. Once done, our template file should look like this.

<div>
  <h1>My To-Do App!</h1>
  <to-do-form />
</div>

You should now see that the form is being displayed on our page.

Showing The Tasks

The next thing we are going to tackle is showing the tasks to the user. To start with we need to store the tasks as an array. In our ToDo.ts component, we will add a new property. This is essentially the same as adding any data property in a standard .vue component.

Let's define a type for a task. Create a types folder within src and then create a file called Task.ts. Our task is going to be relatively simple. Each task will consist of a description and a completion status. Here is our type definition.

type Task {
 completed: boolean;
 description: string;
}

export default Task;

Now we can create our data property in our ToDo.ts component. Import the type into our component...

import Task from '@/types/Task';

...and then add the following property to the class.

public tasks: Task[] = [];

In order to see the results on the page, we need to render them using a v-for loop. Here you can see I have updated the template within the to-do.html template to output each task item in an unordered list.

<div>
  <h1>My To-Do App!</h1>
  <to-do-form></to-do-form>
  <ul>
    <li v-for="task in tasks"><input type="checkbox" :checked="task.completed"> {{ task.description }}</li>
  </ul>
</div>

For now, let's hard-code some tasks so we know that it is working. Update the tasks property in the ToDo.ts component to have some items in the array by default.

public tasks: Task[] = [
 { description: 'Make Coffee', completed: false },
 { description: 'Feed Dragons', completed: false },
];

You should now see those tasks being rendered on the page. Good job!

Creating New Tasks

So we now have a form and a way of displaying the current tasks on the page. Next, we need to actually add the functionality so that when a user adds a task in the form it updates the data property.

To do this we will first need to edit our ToDoForm.ts component. We need to use v-model so that we can capture the input with data binding. Within your ToDoForm.ts file, add a new property called task.

public task: string = '';

Now update the to-do-form.html template so that the input field has v-model.

<form>
  <input type="text" placeholder="Enter task..." v-model="task">
  <button type="submit">Add Task</button>
</form>

Great, we now have a way of capturing the user input. Next, we need to ensure that when the "Add Task" button is clicked, we emit an event to the parent component. Let's add an event listener to the form tag within our template.

<form @submit.prevent="emitTask">
  <input type="text" placeholder="Enter task..." v-model="task">
  <button type="submit">Add Task</button>
</form>

Next, we need to create the emitTask method on in our ToDoForm.ts component. Simply add a new method to the class. Within this method we want to emit a new event, passing the value entered in the form. We then want to reset the value ready for the next input.

public emitTask(): void {
 this.$emit('added', this.task);
 this.task = '';
}

Now that we have an event emitting, we can listen for this event in our parent component. First, let's add an event listener to the component in our to-do.html template file.

<to-do-form @added="addTask" />

Now we need to create the listener. Add a new method to the ToDo.ts class called addTask. In this method, we want to push a new item to the tasks property with the description from the event.

public addTask(description: string): void {
  this.tasks.push({ description, completed: false });
}

And now you are ready to check it out in the browser. You should now be able to add a new task, submit the form and see it added to the list below.

What About Props?

So far I have shown how to use child components, data properties, and events. But you'll commonly have to use props in any real-world use case.

Let's add a new feature so that we can easily customise the text of the form button. We want to be able to pass the value through a prop called button-text.

Firstly, we'll update the template so we are passing a prop through to the child component.

<to-do-form @added="addTask" button-text="Create Task" />

The next thing we need to do is accept the prop in our child component. In order to do this, we are going to use another decorator, @Prop. Update the import statement within our ToDoForm.ts file so that we can use the decorator.

import { Component, Vue, Prop } from 'vue-property-decorator';

Now we can go ahead and use it. Add the decorator to the class to accept the prop. Your ToDoForm.ts file should now look like this.

import { Component, Vue, Prop } from 'vue-property-decorator';
import WithRender from './to-do-form.html';

@WithRender
@Component
export default class ToDoForm extends Vue {
  
 @Prop(String) readonly buttonText!: string

 public task: string = '';

 public emitTask(): void {
  this.$emit('added', this.task);
  this.task = '';
 }
}

You'll notice that for the prop decorator, we are declaring the type twice. Let me explain just a little what is going on here. The first time we specify it, we are passing it as a parameter to the decorator. This is for Vue's type checking. This is similar to how you would declare the property in the following way.

buttonText: {
  type: String
}

We also specify the type at the end of the property. This is for TypeScript's type checking.

We should now be able to update our to-do-form.html template to reference the property as opposed to a hard-coded value.

<form @submit.prevent="emitTask">
  <input type="text" placeholder="Enter task..." v-model="task">
  <button type="submit">{{ buttonText }}</button>
</form>

However, what if we don't pass through a property? You'll notice that we will just get an empty button. Let's add a default just to be safe. In order to do this, we need to pass more information to the @Prop decorator. As we are already providing the type, we instead need to pass in an object so that we can configure multiple options.

@Prop({ type: String, default: 'Add Task'}) readonly buttonText!: string

That is all there is to it. We now have a default value for the button text if we don't provide one.

Conclusion

This has been somewhat of a lengthy article, but I hope it has been useful. We have looked at how we can use child components, data properties, events, and props.

If you have any questions at all, please feel free to get in touch and I'll do my best to answer them.

If you would like to view the code for this, I have pushed it to a git repository. You can find it here - https://github.com/georgehanson/vue-todo-typescript

Homework

If you fancy it, here is a little task for you to do.

Firstly fork the repository. Next add a feature so that you can mark the task as complete. But rather than using v-model, make each task it's own component and use events.

Good luck!