在本篇文章中,我们将创建:
用于显示代办项目列表的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.html
1 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.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 31 32 33 34 import { 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的组件驱动开发来实现这一点。
进入工程根目录,执行如下命令
$ 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将会自动加入TodoListHeaderComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { 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 4 Error: 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 的
到 src/app/todo-list-header/todo-list-header.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 24 import { 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Component, Output, EventEmitter } from '@angular/core'; import { Todo } from '../todo'; @Component({ // ... }) export class TodoListHeaderComponent { // ... @Output() add: EventEmitter<Todo> = new EventEmitter(); addTodo() { this.add.emit(this.newTodo); this.newTodo = new Todo(); } }
然后我们能够在模板中按照如下方式捕捉事件:
每当在 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 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 34 35 36 37 38 39 40 41 42 import {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 { // No longer needed, now handled by TodoListHeaderComponent // newTodo: Todo = new Todo(); constructor(private todoDataService: TodoDataService) { } // No longer needed, now handled by TodoListHeaderComponent // addTodo() { // this.todoDataService.addTodo(this.newTodo); // this.newTodo = new Todo(); // } // Add new method to handle event emitted by TodoListHeaderComponent onAddTodo(todo: Todo) { this.todoDataService.addTodo(todo); } toggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } }
创建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 31 import { 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 中处理 修改 AppComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 export 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 30 import { 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
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 17 import { 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。