By Md. Sabuj Sarker | 4/2/2018 | General |Beginners

Angular 5 To-Do List App - Part 7

Angular 5 To-Do List App - Part 7

In the previous article we learned to implement task filtering for our to-do application. But we had one very annoying problem. When we clicked cancel, edit, or delete after filtering we ended up affecting the wrong task. We are going to solve that problem and make other improvements to our application in this article. The code of the previous article can be found in the 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-6 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-7.

Why the Wrong Tasks Got Affected

The wrong tasks are being affected after we filter tasks with the filtering functionality we have. This is happening because the methods responsible for canceling, editing and deleting depend on the tasks property that contains all the tasks we have. Again, new tasks are added to the tasks—not to the filtered_tasks.

Solving the Indexing Issue

So, to solve the issue do we just need some syncing between tasks and filtered_tasks? No, not like that. We need to somehow save the index of the tasks in the tasks array to the filtered_tasks. We can update the Task interface to save the index in objects casted with Task, and this information will not be used wherever tasks property is used. It will be used solely for filtered_tasks property.

The updated Task interface will hold an extra property named f_idx and will have a default value of null when added for the first time.

interface Task{
 title: string,
 is_canceled: boolean,
 f_idx: number
}

So, the tasks property will be initialized like below:

tasks: Array<Task> = [
   {
     title: "Go home",
     is_canceled: false,
     f_idx: null
   },
   {
     title:"Take a nap",
     is_canceled: false,
     f_idx: null
   },
   {
     title: "Start learning Angular with Sabuj",
     is_canceled: false,
     f_idx: null
   }
 ];

And, the addTask() method will look like this:

 addTask(input){
   let value = input.value;
   input.value = "";
   this.tasks.push(
     {
       title: value,
       is_canceled: false,
       f_idx: null
     });
 }

So, the most important method for solving our issue looks like below:

filterTasks(filter_input){
   let filter_by: string = filter_input.value;
   let filtered_tasks: Array<Task> = [];
   for(let idx=0; idx < this.tasks.length; idx++){
     let task = this.tasks[idx];
     if (task.title.includes(filter_by)){
       filtered_tasks.push(
         <Task>{
           title: task.title,
           is_canceled: task.is_canceled,
           f_idx: idx
         }
       );
     }
   }

   this.filtered_tasks = filtered_tasks;
 }

Notice that we are using the ordinary for loop instead of for-of loop to iterate over the array to get the index. If we wanted we could create an indexing number with the for-of loop too. But the code would not look pretty.

Also, notice that we are creating a brand new Task object and setting the f_idx. This is kind of a work functional programming so that the original objects stay intact.

In our component we are initializing the filtered_tasks directly with task objects from the task’s property. So, filtered_tasks is just referencing tasks and thus the f_idx is not getting updated. To work the way we did in filter_tasks for the initialization, we will initialize filtered_tasks inside the constructor of the component class. So, our constructor method will look like this:

 constructor(){
   for(let idx=0; idx < this.tasks.length; idx++){
     let task = this.tasks[idx];
     this.filtered_tasks.push(
       <Task>{
         title: task.title,
         is_canceled: task.is_canceled,
         f_idx: idx
       }
     );
   }
 }

Don't forget to initialize filtered_tasks to empty array during declaration:

filtered_tasks: Array<Task> = [];

Our problem is not solved yet. We need to update our template. Instead of depending on the index provided by the *ngFor we need to take it from the f_idx property of Task objects. We will need to remove the indexing from *ngFor.

<tr *ngFor="let task of filtered_tasks;">
   <td *ngIf="task.is_canceled; else elseTd">
       <s>{{ task.f_idx + 1 }}. {{task.title}}</s>
   </td>
   <ng-template #elseTd>
       <td>{{ task.f_idx + 1 }}. {{task.title}}</td>
   </ng-template>

   <td>
       <button class="btn btn-primary btn-xs" (click)="editTask(task.f_idx)"> Edit </button>
   </td>

   <td>
       <button class="btn btn-warning btn-xs" (click)="cancelTask(task.f_idx)"> Cancel </button>
   </td>
   <td>
       <button class="btn btn-danger btn-xs" (click)="deleteTask(task.f_idx)"> Delete </button>
   </td>
</tr>

Go to the browser to check if the numbering of the tasks is fixed. Yes, it is working perfectly now. But, there are some other issues hanging around.

  • When filtering is in place, if we add new tasks that yield positive for the filter, they are not displayed on the filtered to-do list until we empty the filter field and click the filter button.
  • Tasks are not updated with cancel, edit, or delete until we click the filter button again.

To solve these we need to change our filtering logic. Every time there is an update operation of some kind (adding, filtering, canceling, editing, clearing or deleting) we need to update the filtered_tasks list. To do so we need to preserve the filter text in our component. Create a new property called filter_by and make the add a method called addFilter(). The handler method of the Filter button will be replaced by addFilter() and filterTasks(), and will only be responsible for updating the filtered_tasks array from different places of the component class. As we will have the filtered text saved in our component, we will no longer need the filterInput argument with it. We will call the filterTasks() method from everywhere we update the list from.

Making a lot of changes in a lot of places, our component class will look like below now:

import { Component, TemplateRef, Input, ElementRef } from '@angular/core';

interface Task{
 title: string,
 is_canceled: boolean,
 f_idx: number
}

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent {
 tasks: Array<Task> = [
   {
     title: "Go home",
     is_canceled: false,
     f_idx: null
   },
   {
     title:"Take a nap",
     is_canceled: false,
     f_idx: null
   },
   {
     title: "Start learning Angular with Sabuj",
     is_canceled: false,
     f_idx: null
   }
 ];

 filtered_tasks: Array<Task> = [];

 filter_by: string = "";
 
 constructor(){
   this.filterTasks();
 }

 clearToDo(){
   let do_delete = confirm("Are you sure to delete all tasks?");
   if (do_delete){
     this.tasks.splice(0);
   }
   this.filterTasks();
 }

 addTask(input){
   let value = input.value;
   input.value = "";
   this.tasks.push(
     {
       title: value,
       is_canceled: false,
       f_idx: null
     });
   this.filterTasks();
 }

 cancelTask(idx: number){
   if (this.tasks[idx].is_canceled){
     this.tasks[idx].is_canceled = false;
   }else{
     this.tasks[idx].is_canceled = true;
   }
   this.filterTasks();
 }

 deleteTask(idx: number){
   let do_delete = confirm("Are you sure to delete the task?");
   if (do_delete){
     this.tasks.splice(idx, 1);
     this.filterTasks();
   }
 }

 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;
     this.filterTasks();
   }
 }

 filterTasks(){
   let filtered_tasks: Array<Task> = [];
   for(let idx=0; idx < this.tasks.length; idx++){
     let task = this.tasks[idx];
     if (task.title.includes(this.filter_by)){
       filtered_tasks.push(
         <Task>{
           title: task.title,
           is_canceled: task.is_canceled,
           f_idx: idx
         }
       );
     }
   }

   this.filtered_tasks = filtered_tasks;
 }

 addFilter(filter_input){
   let filter_by: string = filter_input.value;
   this.filter_by = filter_by;
   this.filterTasks();
 }
}

Notice that we also have changed the constructor to call the filterTasks() method to initialize the filtered_tasks array.

Our application will not act properly until we change the template. The only place we need change in our template is the filter button:

<button class="btn btn-info" (click)="addFilter(filterInput)">Filter</button>

Go to the browser, test the application in every possible way to make sure that it is working perfectly now.

Conclusion

I understand that this article was perhaps a tiresome journey for you. But the journey is not over yet. We still need a lot of improvements to it to scale it properly without introducing any prospective bug. 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.

 

Previous article: Angular 5 to do list app - Part 6

Next article: Angular 5 To-Do List App - Part 8

 

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.

By Md. Sabuj Sarker | 4/2/2018 | General

{{CommentsModel.TotalCount}} Comments

Your Comment

{{CommentsModel.Message}}

Recent Stories

Top DiscoverSDK Experts

User photo
3355
Ashton Torrence
Web and Windows developer
GUI | Web and 11 more
View Profile
User photo
3220
Mendy Bennett
Experienced with Ad network & Ad servers.
Mobile | Ad Networks and 1 more
View Profile
User photo
3060
Karen Fitzgerald
7 years in Cross-Platform development.
Mobile | Cross Platform Frameworks
View Profile
Show All
X

Compare Products

Select up to three two products to compare by clicking on the compare icon () of each product.

{{compareToolModel.Error}}

Now comparing:

{{product.ProductName | createSubstring:25}} X
Compare Now