在本篇文章中,我们将创建:
- 用于显示代办项目列表的TodoListComponent
- 用于显示单个代办项目的TodoListItemComponent
- 用于创建新的代办项目的TodoListHeaderComponent
- 用于显示共有多少代办项目的TodoListFooterComponent
我们的应用程序架构会像这样:
本文将讨论标有红色边框的项目,以外的部分会在后续文章中说明。
通过这篇文章,我们将学习到:
- 基本Angular组件体系结构
- 如何利用属性绑定向组建传递数据
- 如何利用组件事件监听器监听事件源
- 为什么分解成可重用的组件是一个好的做法
- smart和dumb组件的区别,为什么保持组件dumb(哑)是一个很好的实践。
更新并运行
安装最新版angular命令如下:
npm install -g @angular/cli@latest
如果需要移除过去版本重新安装:
npm uninstall -g @angular/cli angular-cli
npm cache clean --force
npm install -g @angular/cli@latest
*win系统中需要管理者权限执行
已完成 AppComponent
让我们打开已完成的 src/app/app.component.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<section class="todoapp">
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
</header>
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
</section>
类 src/app/app.component.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import { Component } from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [TodoDataService]
})
export class AppComponent {
newTodo: Todo = new Todo();
constructor(private todoDataService: TodoDataService) {
}
addTodo() {
this.todoDataService.addTodo(this.newTodo);
this.newTodo = new Todo();
}
toggleTodoComplete(todo) {
this.todoDataService.toggleTodoComplete(todo);
}
removeTodo(todo) {
this.todoDataService.deleteTodoById(todo.id);
}
get todos() {
return this.todoDataService.getAllTodos();
}
}
虽然我们的AppComponent能够正常的执行,但是并不推荐所有的代码都写在一个组件里面。
向Todo应用程序添加更多的功能会使得AppComponent更加复杂,庞大,难于理解。
因此我们推荐按照功能分割成较小的组件。理想情况下,较小的组件是可配置的,这个样我们就不必在业务逻辑更改时重写它们的代码。
例如,在以后的文章中我们将会更改TodoDataService为REST API,而当我们重构TodoDataService的时候,不必更改任何小的组件。
我门来看AppComponent的template,其底层结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- header that lets us create new todo -->
<header></header>
<!-- list that displays todos -->
<ul class="todo-list">
<!-- list item that displays single todo -->
<li>Todo 1</li>
<!-- list item that displays single todo -->
<li>Todo 2</li>
</ul>
<!-- footer that displays statistics -->
<footer></footer>
我们将其变化为Angular组件结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- TodoListHeaderComponent that lets us create new todo -->
<app-todo-list-header></app-todo-list-header>
<!-- TodoListComponent that displays todos -->
<app-todo-list>
<!-- TodoListItemComponent that displays single todo -->
<app-todo-list-item></app-todo-list-item>
<!-- TodoListItemComponent that displays single todo -->
<app-todo-list-item></app-todo-list-item>
</app-todo-list>
<!-- TodoListFooterComponent that displays statistics -->
<app-todo-list-footer></app-todo-list-footer>
让我们看看如何利用Angular的组件驱动开发来实现这一点。
创建TodoListHeaderComponent
进入工程根目录,执行如下命令
$ ng g component todo-list-header
将会生成如下文件:
CREATE src/app/todo-list-header/todo-list-header.component.html (35 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.spec.ts (686 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.ts (307 bytes)
CREATE src/app/todo-list-header/todo-list-header.component.css (0 bytes)
在AppModoule将会自动加入TodoListHeaderComponent1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component';
@NgModule({
declarations: [
AppComponent,
TodoListHeaderComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
在AppModoule将会自动加入TodoListHeaderComponent组件声明是为了能够在所有view模板里面使用它。Angular CLI 方便的为我们添加了该组件所以我们无需手动添加它。
如果组件没有被声明就在view模板中使用,Angular将会抛出如下错误:1
2
3
4Error: Uncaught (in promise): Error: Template parse errors:
'app-todo-list-header' is not a known element:
1. If 'app-todo-list-header' is an Angular component, then verify that it is part of this module.
2. If 'app-todo-list-header' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.
现在我们生成了所有TodoListHeaderComponent所需文件,我们接下来移动src/app/app.component.html 的1
2
3
4<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
</header>
添加逻辑src/app/todo-list-header/todo-list-header.component.ts:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';
@Component({
selector: 'app-todo-list-header',
templateUrl: './todo-list-header.component.html',
styleUrls: ['./todo-list-header.component.css']
})
export class TodoListHeaderComponent implements OnInit {
newTodo: Todo = new Todo();
@Output()
add: EventEmitter<Todo> = new EventEmitter();
constructor() { }
addTodo() {
this.add.emit(this.newTodo);
this.newTodo = new Todo();
}
ngOnInit() {
}
}
替换注入TodoDataService保存新的todo, 我们出发一个add事件,并且传递一个新的todo参数。
在Angular 模板中以如下方式捕捉事件:
这将在enter键按下的时候触发addTodo()方法。之所以能正常执行,是因为在angular framework 中 keyup.enter 被定义为一个有效的事件。
尽管如此,我们也经常 通过 EventEmitter 和 @Output() 为一个组件定义自己的事件。
1 | import { Component, Output, EventEmitter } from '@angular/core'; |
然后我们能够在模板中按照如下方式捕捉事件:
每当在 TodoListHeaderComponent 中调用 add.emit(value) 的时候,onAddTodo($event)事件将会被执行,参数$event将会被赋值为value。
这将解耦TodoListHeaderComponent 和 TodoDataService ,当创建一个新的to的时候,允许父组件处理想要做的事情。
修改 AppComponent 模板1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<section class="todoapp">
<!-- header is now replaced with app-todo-list-header -->
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
</section>
在 AppComponent 中添加 onAddTodo()
1 | import {Component} from '@angular/core'; |
创建TodoListComponent
首先生成 TodoListComponent:
ng g component todo-list
将会产生如下文件:
create src/app/todo-list/todo-list.component.css
create src/app/todo-list/todo-list.component.html
create src/app/todo-list/todo-list.component.spec.ts
create src/app/todo-list/todo-list.component.ts
并且在AppModule里面自动添加了如下声明1
2
3
4
5
6
7
8
9
10
11// ...
import { TodoListComponent } from './todo-list/todo-list.component';
@NgModule({
declarations: [
// ...
TodoListComponent
],
// ...
})
export class AppModule { }
打开 src/app/app.component.html:1
2
3
4
5
6
7
8
9
10
11<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>
将内容移动到 src/app/todo-list/todo-list.component.html:1
2
3
4
5
6
7
8
9
10<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.complete">
<app-todo-list-item
[todo]="todo"
(toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list-item>
</li>
</ul>
</section>
*TodoListItemComponent 将在后续做成,通过 todo 属性 传递 todo 项目,并且处理在TodoListItemComponent中触发的事件。
打开 src/app/todo-list/todo-list.component.ts 添加如下处理:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Todo } from '../todo';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
@Input()
todos: Todo[];
@Output()
remove: EventEmitter<Todo> = new EventEmitter();
@Output()
toggleComplete: EventEmitter<Todo> = new EventEmitter();
constructor() {
}
onToggleTodoComplete(todo: Todo) {
this.toggleComplete.emit(todo);
}
onRemoveTodo(todo: Todo) {
this.remove.emit(todo);
}
}
我们定义了 onToggleTodoComplete(todo) 和 onRemoveTodo(todo) 来处理view的事件
(toggleComplete)=”onToggleTodoComplete($event)” and (remove)=”onRemoveTodo($event)”
注意我们在view中使用 $event 作为参数名字,在方法定义中使用todo作为参数名字。
为了能够在Angular模板中访问到事件的负载(emitted value),我们必须使用 $event作为参数名。
事件在 TodoListItemComponent 产生,经由 TodoListComponent 最终在 AppComponent 中处理
修改 AppComponent1
2
3
4
5
6
7
8
9
10
11
12
13export class AppComponent {
// rename from toggleTodoComplete
onToggleTodoComplete(todo: Todo) {
this.todoDataService.toggleTodoComplete(todo);
}
// rename from removeTodo
onRemoveTodo(todo: Todo) {
this.todoDataService.deleteTodoById(todo.id);
}
}
创建TodoListItemComponent
利用Angular CLI 生成 TodoListItemComponent
ng g component todo-list-item
将生成如下文件:
create src/app/todo-list-item/todo-list-item.component.css
create src/app/todo-list-item/todo-list-item.component.html
create src/app/todo-list-item/todo-list-item.component.spec.ts
create src/app/todo-list-item/todo-list-item.component.ts
并且在AppModule里面自动添加了如下声明1
2
3
4
5
6
7
8
9
10
11// ...
import { TodoListItemComponent } from './todo-list-item/todo-list-item.component';
@NgModule({
declarations: [
// ...
TodoListItemComponent
],
// ...
})
export class AppModule {
移动原view中的\<li> 到 src/app/todo-list-item.component.html:1
2
3
4
5<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
<label>{{todo.title}}</label>
<button class="destroy" (click)="removeTodo(todo)"></button>
</div>
src/app/todo-list-item/todo-list-item.component.ts:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';
@Component({
selector: 'app-todo-list-item',
templateUrl: './todo-list-item.component.html',
styleUrls: ['./todo-list-item.component.css']
})
export class TodoListItemComponent {
@Input() todo: Todo;
@Output()
remove: EventEmitter<Todo> = new EventEmitter();
@Output()
toggleComplete: EventEmitter<Todo> = new EventEmitter();
constructor() {
}
toggleTodoComplete(todo: Todo) {
this.toggleComplete.emit(todo);
}
removeTodo(todo: Todo) {
this.remove.emit(todo);
}
}
更新AppComponent模板:1
2
3
4
5
6
7
8
9
10
11
12
13<section class="todoapp">
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
<!-- section is now replaced with app-todo-list -->
<app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list>
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
</section>
创建TodoListFooterComponent
生成TodoListFooterComponent
ng g component todo-list-footer
移动 \<footer> 元素 src/app/app.component.html 到 src/app/todo-list-footer/todo-list-footer.component.html:1
2
3<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
修改 src/app/todo-list-footer/todo-list-footer.component.ts:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { Component, Input } from '@angular/core';
import { Todo } from '../todo';
@Component({
selector: 'app-todo-list-footer',
templateUrl: './todo-list-footer.component.html',
styleUrls: ['./todo-list-footer.component.css']
})
export class TodoListFooterComponent {
@Input()
todos: Todo[];
constructor() {
}
}
更新 AppComponent 模板:1
2
3
4
5
6<section class="todoapp">
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
<app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
(remove)="onRemoveTodo($event)"></app-todo-list>
<app-todo-list-footer [todos]="todos"></app-todo-list-footer>
</section>
这样我们就成功的重构了AppComponent,由下列组件构成,
TodoListHeaderComponent
TodoListComponent -> TodoListItemComponent
TodoListFooterComponent
以后的章节,我们会继续重构 TodoService 为 REST API。