上一章我们简单地把 TodoMVC 的组件进行了拆分。
TodoMVC 很明显是一个以数据为中心的应用:整个系统围绕着待办列表,界面上几乎所有操作都是针对待办列表进行一系列操作。因此,我们可以使用数组保存这个待办列表,将页面显示绑定到列表中的数据,通过修改数据实现页面显示的修改。这个数组应该是全局可用的,方便我们对其进行管理。虽然 JavaScript 提供了全局变量供我们使用,但本着能不用就不用的目的,我们使用服务来保存这个数组。
那么,这个数组应该是什么类型的呢?TypeScript 是带有类型系统的 JavaScript。所以,我们应该对待办事项进行建模,这样才方便我们以后进行各种操作,也能够在编译期就可以发现一些意外的错误,比如拼写问题等。那么,我们现在创建一个文件 todo.model.ts:
export interface Todo { id: number; content: string; completed: boolean; editing: boolean; }
Angular 推荐文件名中包含类型,比如 aaa.component.ts,bbb.service.ts。那么,我们的待办事项的模型Todo
就应该命名为 todo.model.ts。
TypeScript 的类型可以使用interface
描述。与 Java 的interface
关键字不同,Java 的interface
只能描述类的方法和常量值,TypeScript 的interface
不仅可以包含函数声明,还可以包含属性字段等。一个类型实现interface
,意味着这个类型符合这个interface
描述的字段、函数等。
这里我们的Todo
有四个字段(目前我们仅能想到这些字段,以后还可能对其进行增改,这都是很正常的。很少会有始终不变的设计。):
- id,数字类型的唯一标识符
- content,字符串类型的待办事项内容
- completed,布尔类型,该事项是否完成
- editing,布尔类型,该事项是否正在编辑
接下来,我们要创建这个服务:
ng g s todo
Angular 会为我们生成一个服务,我们给这个服务添加一个todoList
属性,其类型是一个Todo[]
,并且初始化为[]
:
import { Injectable } from '@angular/core'; import { Todo } from './todo.model'; @Injectable({ providedIn: 'root' }) export class TodoService { todoList: Todo[] = []; constructor() { } }
下面我们来看第一个需求:当没有待办时,应当隐藏#main
和#footer
。
当没有待办时,也就是todoService.todoList
的数组长度为 0 时,要隐藏#main
和#footer
。 #main
和#footer
在TodoListComponent
组件中,因此,我们需要在这个组件注入TodoService
服务。前面我们介绍过,服务使用构造函数的依赖注入,所以我们将TodoListComponent
组件的类修改一下:
import { Component, OnInit } from '@angular/core'; import { TodoService } from '../todo.service'; import { Todo } from '../todo.model'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.css'] }) export class TodoListComponent implements OnInit { todoList: Todo[] = this.todoService.todoList; constructor( private readonly todoService: TodoService ) { } ngOnInit(): void { } }
第15行,我们在构造函数注入了TodoService
实例。前面我们已经介绍过这种语法,这里不再赘述。一旦服务注入,我们就可以直接使用,正如第12行所示。之所以要将todoService.todoList
单独作为独立的属性,是因为所有模板中要用到的变量,都必须是类的公有属性。todoService
作为private
属性,是不能在模板中使用的。因此,我们将todoService.todoList
单独拿出来。
当然,这里我们也可以使用public
注入todoService
:
constructor( public readonly todoService: TodoService ) { }
这样,我们就可以直接在模板中使用todoService.todoList
了。不过个人并不喜欢在模板直接使用服务对象,所以更习惯于创建一个public
变量。这是因人而异的做法,并没有好坏之分。
一旦有了todoList
对象,就可以在模板中使用:
<section *ngIf="todoList.length > 0" class="main"> <input id="toggle-all" class="toggle-all" type="checkbox"> <label for="toggle-all">Mark all as complete</label> ... </section> <!-- This footer should be hidden by default and shown when there are todos --> <footer *ngIf="todoList.length > 0" class="footer"> <!-- This should be `0 items left` by default --> <span class="todo-count"><strong>0</strong> item left</span> ... </footer>
由于需要在没有待办时,隐藏#main
和#footer
,那么我们就在.main
和.footer
的标签直接使用ngIf
对其进行隐藏。
接下来,我们来实现在顶部的输入框输入文字,按下回车键,新增一条待办记录。
我们使用input
元素的keydown
事件。前面我们介绍过事件过滤,这里我们就是用这种技术:
... <input #content class="new-todo" placeholder="What needs to be done?" autofocus (keydown.enter)="addTodo(content.value)"> ...
我们修改HeaderComponent
的模板,在input
元素增加一个事件监听。keydown.enter
的回调函数是addTodo()
。这个函数需要在HeaderComponent
类中定义:
export class HeaderComponent implements OnInit { constructor( private readonly todoService: TodoService ) { } ngOnInit(): void { } addTodo(content: string): void { this.todoService.todoList.push({ id: this.todoService.todoList.length, content: content, completed: false, editing: false }); } }
很明显,addTodo()
函数需要向之前我们定义的Todo
数组增加元素。那么,我们就需要将TodoService
注入到HeaderComponent
类,然后在addTodo()
函数中,使用push()
函数,向todoList
增加一个新的Todo
对象。由于Todo
是一个接口,所以我们只要让普通的 JSON 对象满足接口定义的属性,就是一个合法的Todo
类型对象。在这里,我们将新建的Todo
的id
属性定义为数组长度;在新增时,completed
和editing
属性都应该是false
。注意这里addTodo()
函数的参数content: string
,其实参值来自于input
元素。这一点,在模板中可以看到:我们给input
元素一个变量引用,然后使用了其value
属性,即用户输入的值。这个值正是新的Todo
对象的content
属性值。
修改完之后,运行项目,在input
输入之后点击回车,可以看到之前隐藏的#main
和#footer
立即出现,意味着todoList
的长度已经大于 0。那么,下面的问题就是,ul.todo-list
的内容应该按照todoList
去渲染,而不是之前的临时数据。
要显示一个数组,就应该使用ngFor
去循环这个数组:
... <ul class="todo-list"> <!-- These are here just to show the structure of the list items --> <!-- List items should get the class `editing` when editing and `completed` when marked as completed --> <li *ngFor="let todo of todoList"> <div class="view"> <input class="toggle" type="checkbox"> <label>{{ todo.content }}</label> <button class="destroy"></button> </div> </li> </ul> ...
我们将TodoListComponent
的模板修改一下:li
元素使用ngFor
循环整个todoList
数组,label
标签内容则是每个todo
的content
属性值。
这样,我们的待办列表基本功能就实现了。
附件中是本章的完整项目代码:https://files.devbean.net/code/todomvc-app.zip