Let's think about this for a moment
Part 3 is the project's final stage — once the core features are done, we'll add polish that improves user experience and code quality. The custom pipe, async pipe, form validation, and route guard lessons apply directly to this stage. We'll round out the app by formatting the task count summary with a custom pipe, switching to the async pipe to avoid manual subscribe/unsubscribe leaks in the template, adding validation so the title field can't be empty, and using a guard to block access to the detail page when a task ID isn't found.
Let's build it for real
Build a custom pipe called `TaskCountPipe` and write a transform() that returns text like "3/5 completed". In the TaskListComponent template, bind the tasks$ Observable directly with the `| async` pipe and remove the manual subscribe(). Add Validators.required to the title field of the task form, and disable the submit button when it's invalid. Build a `taskExistsGuard` CanActivate guard that checks whether a task exists for the id parameter on the TaskDetailComponent route, and redirects back to the root path if it doesn't. Finally, write a README.md that summarizes the project.
Code Example
// task-count.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { Task } from './task.model';
@Pipe({ name: 'taskCount' })
export class TaskCountPipe implements PipeTransform {
transform(tasks: Task[]): string {
const completed = tasks.filter(t => t.completed).length;
return `${completed}/${tasks.length} completed`;
}
}
// task-exists.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { TaskService } from './task.service';
import { map, take } from 'rxjs/operators';
export const taskExistsGuard: CanActivateFn = (route) => {
const taskService = inject(TaskService);
const router = inject(Router);
const id = Number(route.paramMap.get('id'));
return taskService.tasks$.pipe(
take(1),
map(tasks => {
const found = tasks.some(t => t.id === id);
return found ? true : router.parseUrl('/');
})
);
};When you reload the app, a completed-count summary shows up on the list, submitting without a title is blocked, and navigating directly to a nonexistent task id via the URL redirects back to the root page.5-Minute Try-It
Within 5 minutes, keep TaskCountPipe as a default pure pipe (don't set pure: false) and test whether it still updates correctly with an immutable array update.
A Quick Word of Caution
Keeping custom pipes as default (pure) is good for performance, but be aware that mutating an array won't be detected as a change — always use an immutable update pattern.