上一章我们实现了 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