Angular 学习之路 18 – Todo App (2)

上一章我们简单地把 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#footerTodoListComponent组件中,因此,我们需要在这个组件注入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类型对象。在这里,我们将新建的Todoid属性定义为数组长度;在新增时,completedediting属性都应该是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标签内容则是每个todocontent属性值。

这样,我们的待办列表基本功能就实现了。

附件中是本章的完整项目代码:https://files.devbean.net/code/todomvc-app.zip

Leave a Reply