Lesson Wednesday

In this lesson, we'll explore a feature in Angular called pipes that will allow us to filter our data in live time. We'll create a pipe to sort our Tasks. This will allow us to choose whether we want to view only completed Tasks, only un-completed Tasks, or all at once. We'll also use our knowledge of event bindings to add checkboxes that allow users to mark Tasks complete.

Angular Pipes

in Angular, a pipe is a special function that transforms values in a template. This can include anything from reformatting a timestamp to filtering a list of data based on certain criteria.

Angular includes built-in pipes (in the Angular documentation) and also allows developers to create custom pipes. We'll walk through creating custom pipes in this lesson. Once you have an understanding of how pipes work, you can also learn about Angular's built-in pipes, too.

Creating a Pipe

First, let's construct our pipe. We'll make a new file in our app directory named completeness.pipe.ts. (Files containing pipes should end in .pipe.ts. Pipes should be named using camelCase and should not contain any non-alphanumeric characters). We'll fill it with the following:

app/completeness.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {Task} from './task.model';

We are importing Pipe and PipeTransform code from the Angular core and also our own Task model.

Let's add a @Pipe decorator and class declaration:

app/completeness.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {Task} from './task.model';

@Pipe({

})


export class CompletenessPipe implements PipeTransform {

}

Similar to the decorators we've used thus far, the @Pipe decorator informs Angular the code following it is meant to create a pipe.

PipeTransform Interface

Notice the class definition above includes implements PipeTransform. PipeTransform is an interface built into Angular. If you've learned Java or another strictly-typed language, you may have already worked with interfaces. If you haven't yet worked with interfaces, an interface is a group of methods multiple different classes may inherit.

Interfaces are often likened to "contracts" that the developer "signs." This is because whenever a class implements an interface, it is obligated to include every method outlined in the interface. If it does not, the class won't compile and there will be errors.

By using the implements keyword, we are saying our CompletenessPipe class must implement all properties or methods outlined in Angular's PipeTransform interface. This ensures that developers are correctly integrating built-in Angular functionality into their own code.

As we can see in the Angular Documentation, the PipeTransform interface contains only one method: transform().

The CompletenessPipe class must also contain a transform() method with the same method signature.

app/completeness.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {Task} from './task.model';

@Pipe({

})
export class CompletenessPipe implements PipeTransform {
  transform(input: Task[], args) {
    return input;
  }
}

Its first parameter, input, is an array of objects to be transformed (or filtered). In our case, this is an array of Tasks.

Next, let's add code to filter incomplete tasks. We'll write a basic function for this logic. Remember, if the done property on a Task is false, it is considered incomplete:

app/completeness.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {Task} from './task.model';

@Pipe({

})


export class CompletenessPipe implements PipeTransform {


  transform(input: Task[]){
    var output: Task[] = [];
    for (var i = 0; i < input.length; i++) {
      if (input[i].done === false) {
        output.push(input[i]);
      }
    }
    return output;
  }


}
  • Here, we define an output array to return the result of the transform() method's filtering.

  • We loop through each Task in our input array. If its done property is false (if the Task hasn't yet been completed), we push it to the output array.

  • We return output, which should now only contain Tasks that fit our criteria.

Let's fill in the @Pipe decorator:

app/completeness.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
import {Task} from './task.model';

@Pipe({
  name: "completeness",
  pure: false
})


export class CompletenessPipe implements PipeTransform {
  transform(input: Task[]){
    var output: Task[] = [];
    for (var i = 0; i < input.length; i++) {
      if (input[i].done === false) {
        output.push(input[i]);
      }
    }
    return output;
  }
}

We've added two properties here:

  • The first is called name. This will be used in our templates.

  • The second is called pure. Set this to false for now. By setting pure to true, our pipe becomes stateless. This means it transforms input to output but doesn't do anything else or store any information until we explicitly tell it to.

  • Conversely, we can make our pipe stateful by setting pure to false. This tells Angular to check if output has changed more often, causing it to update as soon as we change something about a Task, not only when the menu option changes.

Implementing a Pipe

Next, let's see how to fit this into the rest of our program. We'll need to import it in app.module.ts and add it to the declarations array just as we do with components.

app/app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
import { FormsModule }  from '@angular/forms';
import { TaskListComponent }  from './task-list.component';
import { EditTaskComponent }  from './edit-task.component';
import { NewTaskComponent } from './new-task.component';
import { CompletenessPipe } from './completeness.pipe';

@NgModule({
  imports: [ BrowserModule,
            FormsModule ],
  declarations: [ AppComponent,
                  TaskListComponent,
                  EditTaskComponent,
                  NewTaskComponent, 
                  CompletenessPipe],
  bootstrap:    [ AppComponent ]
})

export class AppModule { }

Now we can add our pipe to the task-list template. We include the pipe's name, completeness, wherever we'd like to implement its filtering capabilities:

app/task-list.component.ts
...
<li *ngFor="let currentTask of childTaskList | completeness">
...

By adding it to our loop, we are instructing the template to filter all Tasks in childTaskList through the completeness pipe before displaying them. Only Tasks that meet the criteria outlined in completeness will be rendered.

Additionally, notice the | character prefacing our pipe in the code above. In Angular, this character is called a pipe operator. It instructs Angular to flow the data before the | operator (childTaskList) through the pipe that appears after the | operator (completeness).

The transform() method in our pipe currently returns all incomplete Task objects so our loop will only display incomplete Task objects.

Everything should appear the same in the browser. All Tasks are created with a done property that defaults to false so all Tasks are currently incomplete. We'll add the ability to mark Tasks done in just a moment. Note: If you receive an error at this point, restart your server and rebuild your project.

Filtering Based on Menu Criteria

Let's add a menu to our TaskListComponent that allows users to choose how to filter their Tasks. We'll offer options for "All Tasks", "Completed Tasks", and "Incomplete Tasks":

app/task-list.component.ts
...
  template: `
    <select>
      <option value="allTasks">All Tasks</option>
      <option value="completedTasks">Completed Tasks</option>
      <option value="incompleteTasks" selected="selected">Incomplete Tasks</option>
    </select>


    <ul>
      <li (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList | completeness">{{currentTask.description}} {{currentTask.priority}}
        <input *ngIf="currentTask.done === true" type="checkbox" checked (click)="toggleDone(currentTask, false)"/>
        <input *ngIf="currentTask.done === false" type="checkbox" (click)="toggleDone(currentTask, true)"/>
        <button (click)="editButtonHasBeenClicked(currentTask)">Edit!</button>
      </li>
    </ul>
  `
...

Change Event Emitter

Now we need to retrieve the value of our drop-down menu when the user selects a new option. Let's create a filterByCompleteness property in our TaskListComponent. It will contain the user's filtering choice. We'll set it to a default value of "incompleteTasks":

app/task-list.component.ts
...
filterByCompleteness: string = "incompleteTasks";
...

Next, we'll add an event binding to our <select> field that listens for a change. This is a DOM event that is triggered whenever a new item is selected from a drop-down menu:

app/task-list.component.ts
...
<select (change)="onChange($event.target.value)">
  <option value="allTasks">All Tasks</option>
  <option value="completedTasks">Completed Tasks</option>
  <option value="incompleteTasks" selected="selected">Incomplete Tasks</option>
</select>
...

When a change is registered in the <select> field, onChange() will be called. It includes this argument: $event.target.value. This is special Angular syntax used to fetch the value of the <option> selected in our <select> drop-down menu.

Let's create an onChange() method now:

app/task-list.component.ts
...
onChange(optionFromMenu) {
  this.filterByCompleteness = optionFromMenu;
}
...

Here, onChange() takes the value of the <option> the user selected in our dropdown ("allTasks", "completedTasks", or "incompleteTasks") and resets our filterByCompleteness property to the user's selected option, allowing the user to filter Tasks in different ways.

Next, let's make sure our Pipe is prepared to handle the three different filtering options we're offering to users.

The transform() method will require a second parameter to contain the filtering option selected by the user in our drop-down menu. We'll call it desiredCompleteness. Using a series of if and else statements, let's push different Tasks to the output array depending on how the user chooses to filter the list:

app/completeness.pipe.ts
export class CompletenessPipe implements PipeTransform {


  transform(input: Task[], desiredCompleteness) {
    var output: Task[] = [];
    if(desiredCompleteness === "incompleteTasks") {
      for (var i = 0; i < input.length; i++) {
        if (input[i].done === false) {
          output.push(input[i]);
        }
      }
      return output;
    } else if (desiredCompleteness === "completedTasks") {
      for (var i = 0; i < input.length; i++) {
        if (input[i].done === true) {
          output.push(input[i]);
        }
      }
      return output;
    } else {
      return input;
    }
  }


}

We've also added branching:

  • If the user selected to view completed Tasks, only Task objects with a done property of true are added to the output array.

  • If the user selected to view incomplete Tasks, only those with a false done property are pushed to the output array.

  • If the user selected to view all Tasks, we simply return the input array containing all Task objects without filtering it.

Implementing a Pipe

Next, we can provide our user's filtering selection to the pipe like this:

app/task-list.component.ts
...
      <li (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList | completeness:filterByCompleteness">{{currentTask.description}} {{currentTask.priority}}
...

As you can see, we've added a : directly after the name of our pipe. In Angular, parameters are passed into pipes using colons like this. So, by saying completeness:filterByCompleteness, we're passing our filterByCompleteness property into the completeness pipe's transform() method.

Now our pipe should work. If we select "All Tasks" or "Incomplete Tasks" we will see all our Tasks. However, if we select "Completed Tasks", there are no Tasks. Let's implement checkboxes so we can change a task from incomplete to complete and back again.

Marking Tasks Done

We'll add a checkbox next to each Task. When checked, the Task's done property will be updated:

app/task-list.component.ts
…
  template: `
    <select (change)="onChange($event.target.value)">
      <option value="allTasks">All Tasks</option>
      <option value="completedTasks">Completed Tasks</option>
      <option value="incompleteTasks" selected="selected">Incomplete Tasks</option>
    </select>
    <ul>
      <li (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList | completeness:filterByCompleteness">{{currentTask.description}} {{currentTask.priority}}
        <input *ngIf="currentTask.done === true" type="checkbox" checked (click)="toggleDone(currentTask, false)"/>
        <input *ngIf="currentTask.done === false" type="checkbox" (click)="toggleDone(currentTask, true)"/>
        <button (click)="editButtonHasBeenClicked(currentTask)">Edit!</button>
      </li>
    </ul>
  `
})
...

You'll notice we add two checkboxes. Each has an event binding that triggers a toggleDone() method when clicked. The method takes a Task and a boolean.

  • Each checkbox also includes an *ngIf directive. If a Task is already completed, we'll display a checkbox that can mark the Task incomplete by calling toggleDone() with an argument of false. This will mark a completed Task incomplete.

  • If a Task is already incomplete, we'll display a checkbox that can mark it complete by calling toggleDone() with an argument of true. This will mark a Task complete.

Let's create our toggleDone() method now:

app/task-list.component.ts
…
  toggleDone(clickedTask: Task, setCompleteness: boolean) {
     clickedTask.done = setCompleteness;
   }
…

Now we can launch the application and click a checkbox to mark a Task done. This should immediately remove it from the set of "incomplete" Tasks. When we opt to show "complete" Tasks instead, we can see the newly-completed Task is on this list.

Great work! For more information on pipes, check out their entry in the Angular Documentation.