Angular 5 To-Do List App - Part 6
Until now we have completed fully implementing task adding, canceling, uncanceling, editing, deleting, and clearing in our to-do application. The code of the previous article can be found in article itself and the complete project can be found in the Github repository Learn Angular With Sabuj. Browse to the branch named todo-app-part-5 to get the complete project code of the previous article. To get the complete project of the current article browse the branch named todo-app-part-6.
In this article we will implement task filtering. We will also solve some issues that will arise during implementing filtering.
Filtering Tasks
We usually start with one or a few tasks and then it starts to grow. The number of tasks may grow very large, so that it becomes very difficult to browse through them page after page. But, if we make them searchable or filterable, our life will become much easier.
We need a filter input field under the add task input field. So, we will need to add an input box and a filter button by following Bootstrap’s grid system. So, our markup for the input box should look like below:
<input type="text" class="form-control" name="filter" placeholder="Filter Keyword(s)">
We will need a reference to this input later, so we need to create a template variable for that purpose:
<input type="text" class="form-control" name="filter" placeholder="Filter Keyword(s)" #filterInput>
The button template markup will be:
<button class="btn btn-info">Filter</button>
We will need a click handler later for filtering out tasks, so we are going to put a method call as a click handler like below:
<button class="btn btn-info" (click)="filterTasks(filterInput)">Filter</button>
We will implement the method called filterTasks later in our component class. The method will accept the input field of filter as an argument.
By wrapping the above markups inside the Bootstrap grid system, the final template markup for the filtering part will look like below:
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control" name="filter" placeholder="Filter Keyword(s)" #filterInput>
</div>
<div class="col-xs-4">
<button class="btn btn-info" (click)="filterTasks(filterInput)">Filter</button>
</div>
</div>
Go to the browser and you will see that our design does not look good at all, because the add input and the filter input are sticking together very closely—this is very unexpected. So we are going to add an hr tag between them that will add both a nice line and some spacing resulting in a better look.
So, our template looks below now:
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1> To-Do Application </h1>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control" name="task" placeholder="Task" #taskInput>
</div>
<div class="col-xs-4">
<button class="btn btn-info" (click)="addTask(taskInput)">Add</button>
</div>
</div>
<hr>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control" name="filter" placeholder="Filter Keyword(s)" #filterInput>
</div>
<div class="col-xs-4">
<button class="btn btn-info" (click)="filterTasks(filterInput)">Filter</button>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table table-striped">
<thead>
<tr>
<th>To-Do list</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let task of tasks; let idx = index">
<td *ngIf="task.is_canceled; else elseTd">
<s>{{ idx + 1 }}. {{task.title}}</s>
</td>
<ng-template #elseTd>
<td>{{ idx + 1 }}. {{task.title}}</td>
</ng-template>
<td>
<button class="btn btn-primary btn-xs" (click)="editTask(idx)"> Edit </button>
</td>
<td>
<button class="btn btn-warning btn-xs" (click)="cancelTask(idx)"> Cancel </button>
</td>
<td>
<button class="btn btn-danger btn-xs" (click)="deleteTask(idx)"> Delete </button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<button class="btn btn-danger" (click)="clearToDo()">Clear</button>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
Now, we need to implement the method that will help with filtering.
filterTasks(filter_input){
let filter_by: string = filter_input.value;
let filtered_tasks: Array<Task> = [];
for(let task of this.tasks){
if (task.title.includes(filter_by)){
filtered_tasks.push(task);
}
}
}
But, wait before moving forward in updating it further. The template only knows about the tasks property now. It will render only the tasks from the tasks property and it will result in not rendering the filtered tasks. To solve this issue, we can create another property called filtered_tasks and instruct the template to render from it. Again, we can set filtered_tasks's initial value with the tasks values. Assuming there is a filtered_tasks property we can update the filterTasks() method as below:
filterTasks(filter_input){
let filter_by: string = filter_input.value;
let filtered_tasks: Array<Task> = [];
for(let task of this.tasks){
if (task.title.includes(filter_by)){
filtered_tasks.push(task);
}
}
this.filtered_tasks = filtered_tasks;
}
Now, we can add the filtered_tasks property to our component and initialize it with tasks. Now, our component should look like below:
import { Component, TemplateRef, Input, ElementRef } from '@angular/core';
interface Task{
title: string,
is_canceled: boolean
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
tasks: Array<Task> = [
{
title: "Go home",
is_canceled: false
},
{
title:"Take a nap",
is_canceled: false
},
{
title: "Start learning Angular with Sabuj",
is_canceled: false
}
];
filtered_tasks: Array<Task> = this.tasks;
clearToDo(){
let do_delete = confirm("Are you sure to delete all tasks?");
if (do_delete){
this.tasks.splice(0);
}
}
addTask(input){
let value = input.value;
input.value = "";
this.tasks.push(
{
title: value,
is_canceled: false
});
}
cancelTask(idx: number){
if (this.tasks[idx].is_canceled){
this.tasks[idx].is_canceled = false;
}else{
this.tasks[idx].is_canceled = true;
}
}
deleteTask(idx: number){
let do_delete = confirm("Are you sure to delete the task?");
if (do_delete){
this.tasks.splice(idx, 1);
}
}
editTask(idx: number){
let title = this.tasks[idx].title;
let result = prompt("Edit Task Title", title);
if (result !== null && result !== ""){
this.tasks[idx].title = result;
}
}
filterTasks(filter_input){
let filter_by: string = filter_input.value;
let filtered_tasks: Array<Task> = [];
for(let task of this.tasks){
if (task.title.includes(filter_by)){
filtered_tasks.push(task);
}
}
this.filtered_tasks = filtered_tasks;
}
}
Still, we need to update our template and replace the *ngFor as follows:
<tr *ngFor="let task of filtered_tasks; let idx = index">
<td *ngIf="task.is_canceled; else elseTd">
<s>{{ idx + 1 }}. {{task.title}}</s>
</td>
<ng-template #elseTd>
<td>{{ idx + 1 }}. {{task.title}}</td>
</ng-template>
...
...
...
</tr>
Go to the browser and you will see that your application is behaving as before. Put some text inside the filter box and click filter to see that things are going as we expected. Below is an screenshot of our work.
Conclusion
Our application is working the way we expected it to do. But there is a big problem! The original numbering is not preserved after we do the filtering. And thus if you click cancel, edit or delete, the wrong task will be affected. Try yourself to reproduce the issue and I am going to solve it in the next article. If you have a question, create an issue on the Github repository Learn Angular with Sabuj and I will try my best to help you.
About the Author
My name is Md. Sabuj Sarker. I am a Software Engineer, Trainer and Writer. I have over 10 years of experience in software development, web design and development, training, writing and some other cool stuff including few years of experience in mobile application development. I am also an open source contributor. Visit my github repository with username SabujXi.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment