上一章我们实现了 Mark all as completed 选择框的相关操作。接下来,我们要实现的是另外一个需求:
计数器
以复数形式显示当前有效的待办。将数字放在<strong>标签中。同时需要正确显示item的复数形式:0 items、1 item、2 items。例如,2 items left。
很明显,计数器的值应该根据todoService.#todoList数组计算而来。那么,我们就在TodoService里面增加一个属性:
...
get uncompletedCount(): number {
return this.#todoList.filter(it => !it.completed).length;
}
... 这里,我们使用了 TypeScript 的 getter 函数,返回#todoList中所有completed属性为false的待办事项的个数。
然后我们修改TodoListComponent的模板:
...
<!-- This should be `0 items left` by default -->
<span class="todo-count">
<strong>{{ todoService.uncompletedCount }}</strong> {{ todoService.uncompletedCount === 1 ? 'item' : 'items' }} left
</span>
... 我们使用插值将todoService.uncompletedCount显示出来,后面的items则要根据todoService.uncompletedCount的值判断显示为item还是items。
到目前为止,一切都很顺利。
下面,我们简单修改一下uncompletedCount的实现:
...
get uncompletedCount(): number {
console.log('uncompletedCount');
return this.#todoList.filter(it => !it.completed).length;
}
... 这么做的目的是,我们要看这个 getter 函数究竟调用了多少次。重新编译运行一下页面就会发现,这个函数每次都会调用很多次。即便我们并没有修改待办事项的状态,uncompletedCount也会被调用。
这是由于当某些事件发生时,Angular 会对页面做变更检测,比如鼠标点击事件。可以看到,即便我们在页面随意点击鼠标,函数都会被调用。因此,不应该在模板中进行任何函数调用,特别是消耗非常巨大的函数调用。这会直接导致页面响应变慢。
为解决这一问题,我们要将uncompletedCount修改为普通变量,然后与之前的allCompleted类似,在数组有变化时,同时要修改uncompletedCount的值。
export class TodoService {
allCompleted = false;
uncompletedCount = 0;
get todoList(): Todo[] {
return this.#todoList;
}
#todoList: Todo[] = [];
constructor() { }
addTodo(todo: Omit<Todo, 'id'>): void {
this.#todoList.push({
id: this.#todoList.length,
...todo
});
this.allCompleted = false;
this.uncompletedCount++;
}
deleteTodo(todo: Todo): void {
this.#todoList = this.#todoList.filter(it => it.id !== todo.id);
this.allCompleted = this.#todoList.filter(it => !it.completed).length === 0;
this.uncompletedCount = this.#todoList.filter(it => !it.completed).length;
}
toggleTodo(completed: boolean, todo?: Todo): void {
if (!todo) {
this.#todoList.forEach(it => it.completed = completed);
this.allCompleted = completed;
} else {
todo.completed = completed;
this.allCompleted = this.#todoList.filter(it => !it.completed).length === 0;
}
this.uncompletedCount = this.#todoList.filter(it => !it.completed).length;
}
} 这样,页面模板绑定的是一个变量,而不是函数,就不会引起调用的问题了。
下面我们来看另外一个需求:
Clear completed 按钮
点击之后移除已完成的待办。如果没有已完成的待办,隐藏该按钮。
Clear completed 按钮在TodoListComponent组件,因此,我们首先修改TodoListComponent的模板:
...
<!-- Hidden if no completed items are left ↓ -->
<button *ngIf="todoService.uncompletedCount !== todoService.todoList.length"
(click)="clearCompletedTodos()"
class="clear-completed">
Clear completed
</button>
... 隐藏按钮使用todoService.uncompletedCount !== todoService.todoList.length进行判断。如果未完成总数与所有待办事项的总数不同,则说明有已完成的项目,此时需要显示按钮。然后需要给按钮的click事件回调:
...
clearCompletedTodos(): void {
this.todoService.clearCompletedTodos();
}
... 最主要的是todoService.clearCompletedTodos()的实现。
...
clearCompletedTodos(): void {
this.#todoList = this.#todoList.filter(it => !it.completed);
this.allCompleted = false;
this.uncompletedCount = this.#todoList.length;
}
... 这个实现并不复杂:#todoList应该保留所有未完成的待办事项,然后要记得修改allCompleted和uncompletedCount的值。
本章完整代码:https://files.devbean.net/code/todomvc-app-5.zip