第一節(jié):初識Angular-CLI
第二節(jié):登錄組件的構(gòu)建
第三節(jié):建立一個待辦事項(xiàng)應(yīng)用
第四節(jié):進(jìn)化!模塊化你的應(yīng)用
第五節(jié):多用戶版本的待辦事項(xiàng)應(yīng)用
第六節(jié):使用第三方樣式庫及模塊優(yōu)化用
第七節(jié):給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應(yīng)用
第八節(jié):查缺補(bǔ)漏大合集(上)
第九節(jié):查缺補(bǔ)漏大合集(下)
作者:王芃 wpcfan@gmail.com
第四節(jié):進(jìn)化!模塊化你的應(yīng)用
一個復(fù)雜組件的分拆
上一節(jié)的末尾我偷懶的甩出了大量代碼,可能你看起來都有點(diǎn)暈了,這就是典型的一個功能經(jīng)過一段時(shí)間的需求累積后,代碼也不可避免的臃腫起來?,F(xiàn)在我們看看怎么分拆一下吧。

我們的應(yīng)用似乎可以分為Header,Main和Footer幾部分。首先我們來建立一個新的Component,鍵入
ng g c todo/todo-footer。然后將src\app\todo\todo.component.html中的<footer>...</footer>段落剪切到src\app\todo\todo-footer\todo-footer.component.html中。
<footer class="footer" *ngIf="todos?.length > 0">
<span class="todo-count">
<strong>{{todos?.length}}</strong> {{todos?.length == 1 ? 'item' : 'items'}} left
</span>
<ul class="filters">
<li><a href="">All</a></li>
<li><a href="">Active</a></li>
<li><a href="">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
觀察上面的代碼,我們看到似乎所有的變量都是todos?.length,這提醒我們其實(shí)對于Footer來說,我們并不需要傳入todos,而只需要給出一個item計(jì)數(shù)即可。那么我們來把所有的todos?.length改成itemCount。
<footer class="footer" *ngIf="itemCount > 0">
<span class="todo-count">
<strong>{{itemCount}}</strong> {{itemCount == 1 ? 'item' : 'items'}} left
</span>
<ul class="filters">
<li><a href="">All</a></li>
<li><a href="">Active</a></li>
<li><a href="">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
這樣的話也就是說如果在src\app\todo\todo.component.html中我們可以用<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>去傳遞todo項(xiàng)目計(jì)數(shù)給Footer即可。所以在src\app\todo\todo.component.html中剛才我們剪切掉代碼的位置加上這句吧。當(dāng)然,如果要讓父組件可以傳遞值給子組件,我們還需要在子組件中聲明一下。@Input()是輸入型綁定的修飾符,用于把數(shù)據(jù)從父組件傳到子組件。
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-todo-footer',
templateUrl: './todo-footer.component.html',
styleUrls: ['./todo-footer.component.css']
})
export class TodoFooterComponent implements OnInit {
//聲明itemCount是可以一個可輸入值(從引用者處)
@Input() itemCount: number;
constructor() { }
ngOnInit() {
}
}
運(yùn)行一下看看效果,應(yīng)該一切正常!
類似的我們建立一個Header組件,鍵入ng g c todo/todo-header,同樣的把下面的代碼從src\app\todo\todo.component.html中剪切到src\app\todo\todo-header\todo-header.component.html中
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
</header>
這段代碼看起來有點(diǎn)麻煩,主要原因是我們好像不但需要給子組件輸入什么,而且希望子組件給父組件要輸出一些東西,比如輸入框的值和按下回車鍵的消息等。當(dāng)然你可能猜到了,Angular2里面有@Input()就相應(yīng)的有@Output()修飾符。
我們希望輸入框的占位文字(沒有輸入的情況下顯示的默認(rèn)文字)是一個輸入型的參數(shù),在回車鍵抬起時(shí)可以發(fā)射一個事件給父組件,同時(shí)我們也希望在輸入框輸入文字時(shí)父組件能夠得到這個字符串。也就是說父組件調(diào)用子組件時(shí)看起來是下面的樣子,相當(dāng)于我們自定義的組件中提供一些事件,父組件調(diào)用時(shí)可以寫自己的事件處理方法,而$event就是子組件發(fā)射的事件對象:
<app-todo-header
placeholder="What do you want"
(onTextChanges)="onTextChanges($event)"
(onEnterUp)="addTodo()" >
</app-todo-header>
但是第三個需求也就是“在輸入框輸入文字時(shí)父組件能夠得到這個字符串”,這個有點(diǎn)問題,如果每輸入一個字符都要回傳給父組件的話,系統(tǒng)會過于頻繁進(jìn)行這種通信,有可能會有性能的問題。那么我們希望可以有一個類似濾波器的東東,它可以過濾掉一定時(shí)間內(nèi)的事件。因此我們定義一個輸入型參數(shù)delay。
<app-todo-header
placeholder="What do you want"
delay="400"
(textChanges)="onTextChanges($event)"
(onEnterUp)="addTodo()" >
</app-todo-header>
現(xiàn)在的標(biāo)簽引用應(yīng)該是上面這個樣子,但我們只是策劃了它看起來是什么樣子,還沒有做呢。我們一起動手看看怎么做吧。
todo-header.component.html的模板中我們調(diào)整了一些變量名和參數(shù)以便讓大家不混淆子組件自己的模板和父組件中引用子組件的模板片段。
//todo-header.component.html
<header class="header">
<h1>Todos</h1>
<input
class="new-todo"
[placeholder]="placeholder"
autofocus=""
[(ngModel)]="inputValue"
(keyup.enter)="enterUp()">
</header>
記住子組件的模板是描述子組件自己長成什么樣子,應(yīng)該有哪些行為,這些東西和父組件沒有任何關(guān)系。比如todo-header.component.html中的placeholder就是HTML標(biāo)簽Input中的一個屬性,和父組件沒有關(guān)聯(lián),如果我們不在todo-header.component.ts中聲明@Input() placeholder,那么子組件就沒有這個屬性,在父組件中也無法設(shè)置這個屬性。父組件中的聲明為@Input()的屬性才會成為子組件對外可見的屬性,我們完全可以把@Input() placeholder聲明為@Input() hintText,這樣的話在引用header組件時(shí),我們就需要這樣寫<app-todo-header hintText="What do you want" ...
現(xiàn)在看一下todo-header.component.ts
import { Component, OnInit, Input, Output, EventEmitter, ElementRef } from '@angular/core';
import {Observable} from 'rxjs/Rx';
import 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@Component({
selector: 'app-todo-header',
templateUrl: './todo-header.component.html',
styleUrls: ['./todo-header.component.css']
})
export class TodoHeaderComponent implements OnInit {
inputValue: string = '';
@Input() placeholder: string = 'What needs to be done?';
@Input() delay: number = 300;
//detect the input value and output this to parent
@Output() textChanges = new EventEmitter<string>();
//detect the enter keyup event and output this to parent
@Output() onEnterUp = new EventEmitter<boolean>();
constructor(private elementRef: ElementRef) {
const event$ = Observable.fromEvent(elementRef.nativeElement, 'keyup')
.map(() => this.inputValue)
.debounceTime(this.delay)
.distinctUntilChanged();
event$.subscribe(input => this.textChanges.emit(input));
}
ngOnInit() {
}
enterUp(){
this.onEnterUp.emit(true);
this.inputValue = '';
}
}
下面我們來分析一下代碼:
placeholder和delay作為2個輸入型變量,這樣<app-todo-header>標(biāo)簽中就可以設(shè)置這兩個屬性了。
接下來我們看到了由@Output修飾的onTextChanges和onEnterUp,這兩個顧名思義是分別處理文本變化和回車鍵抬起事件的,這兩個變量呢都定義成了EventEmitter(事件發(fā)射器)。我們會在子組件的邏輯代碼中以適當(dāng)?shù)臈l件去發(fā)射對應(yīng)事件,而父組件會接收到這些事件。我們這里采用了2中方法來觸發(fā)發(fā)射器
- enterUp:這個是比較常規(guī)的方法,在
todo-header.component.html中我們定義了(keyup.enter)="enterUp()",所以在組件的enterUp方法中,我們直接讓onEnterUp發(fā)射了對應(yīng)事件。 - 構(gòu)造器中使用Rx:這里涉及了很多新知識,首先我們注入了ElementRef,這個是一個Angular中需要謹(jǐn)慎使用的對象,因?yàn)樗梢宰屇阒苯硬僮鱀OM,也就是HTML的元素和事件。同時(shí)我們使用了Rx(響應(yīng)式對象),Rx是一個很復(fù)雜的話題,這里我們不展開了,但我們主要是利用Observable去觀察HTML中的keyup事件,然后在這個事件流中做一個轉(zhuǎn)換把輸入框的值發(fā)射出來(map),應(yīng)用一個時(shí)間的濾波器(debounceTime),然后應(yīng)用一個篩選器(distinctUntilChanged)。這里由于這個事件的發(fā)射條件是依賴于輸入時(shí)的當(dāng)時(shí)條件,我們沒有辦法按前面的以模板事件觸發(fā)做處理。
最后需要在todo.component.ts中加入對header輸出參數(shù)發(fā)射事件的處理
onTextChanges(value) {
this.desc = value;
}
最后由于組件分拆后,我們希望也分拆一下css,這里就直接給代碼了
todo-header.component.css的樣式如下:
h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
todo-footer.component.css的樣式如下
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
當(dāng)然上述代碼要從todo.component.css中刪除,現(xiàn)在的todo.component.css看起來是這個樣子
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '?';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
封裝成獨(dú)立模塊
現(xiàn)在我們的todo目錄下好多文件了,而且我們觀察到這個功能相對很獨(dú)立。這種情況下我們似乎沒有必要將所有的組件都聲明在根模塊AppModule當(dāng)中,因?yàn)轭愃葡褡咏M件沒有被其他地方用到。Angular中提供了一種組織方式,那就是模塊。模塊和根模塊很類似,我們先在todo目錄下建一個文件src\app\todo\todo.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { routing} from './todo.routes'
import { TodoComponent } from './todo.component';
import { TodoFooterComponent } from './todo-footer/todo-footer.component';
import { TodoHeaderComponent } from './todo-header/todo-header.component';
import { TodoService } from './todo.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
HttpModule,
routing
],
declarations: [
TodoComponent,
TodoFooterComponent,
TodoHeaderComponent
],
providers: [
{provide: 'todoService', useClass: TodoService}
]
})
export class TodoModule {}
注意一點(diǎn),我們沒有引入BrowserModule,而是引入了CommonModule。導(dǎo)入 BrowserModule 會讓該模塊公開的所有組件、指令和管道在 AppModule 下的任何組件模板中直接可用,而不需要額外的繁瑣步驟。CommonModule 提供了很多應(yīng)用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 導(dǎo)入了 CommonModule 并且 重新導(dǎo)出 了它。 最終的效果是:只要導(dǎo)入 BrowserModule 就自動獲得了 CommonModule 中的指令。幾乎所有要在瀏覽器中使用的應(yīng)用的 根模塊 ( AppModule )都應(yīng)該從 @angular/platform-browser 中導(dǎo)入 BrowserModule 。在其它任何模塊中都 不要導(dǎo)入 BrowserModule,應(yīng)該改成導(dǎo)入 CommonModule 。 它們需要通用的指令。它們不需要重新初始化全應(yīng)用級的提供商。
由于和根模塊很類似,我們就不展開講了。需要做的事情是把TodoComponent中的TodoService改成用@Inject('todoService')來注入。但是注意一點(diǎn),我們需要模塊自己的路由定義。我們在todo目錄下建立一個todo.routes.ts的文件,和根目錄下的類似。
import { Routes, RouterModule } from '@angular/router';
import { TodoComponent } from './todo.component';
export const routes: Routes = [
{
path: 'todo',
component: TodoComponent
}
];
export const routing = RouterModule.forChild(routes);
這里我們只定義了一個路由就是“todo”,另外一點(diǎn)和根路由不一樣的是export const routing = RouterModule.forChild(routes);,我們用的是forChild而不是forRoot,因?yàn)?code>forRoot只能用于根目錄,所有非根模塊的其他模塊路由都只能用forChild。下面就得更改根路由了,src\app\app.routes.ts看起來是這個樣子:
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
export const routes: Routes = [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
},
{
path: 'todo',
redirectTo: 'todo'
}
];
export const routing = RouterModule.forRoot(routes);
注意到我們?nèi)サ袅薚odoComponent的依賴,而且更改todo路徑定義為redirecTo到todo路徑,但沒有給出組件,這叫做“無組件路由”,也就是說后面的事情是TodoModule負(fù)責(zé)的。
此時(shí)我們就可以去掉AppModule中引用的Todo相關(guān)的組件了。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { TodoModule } from './todo/todo.module';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryTodoDbService } from './todo/todo-data';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';
@NgModule({
declarations: [
AppComponent,
LoginComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryTodoDbService),
routing,
TodoModule
],
providers: [
{provide: 'auth', useClass: AuthService}
],
bootstrap: [AppComponent]
})
export class AppModule { }
而且此時(shí)我們注意到其實(shí)沒有任何一個地方目前還需引用<app-todo></app-todo>了,這就是說我們可以安全地把selector: 'app-todo',從Todo組件中的@Component修飾符中刪除了。
更真實(shí)的web服務(wù)
這里我們不想再使用內(nèi)存Web服務(wù)了,因?yàn)槿绻褂?,我們無法將其封裝在TodoModule中。所以我們使用一個更“真”的web服務(wù):json-server。使用npm install -g json-server安裝json-server。然后在todo目錄下建立todo-data.json
{
"todos": [
{
"id": "f823b191-7799-438d-8d78-fcb1e468fc78",
"desc": "blablabla",
"completed": false
},
{
"id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0",
"desc": "getting up",
"completed": false
},
{
"id": "c1092224-4064-b921-77a9-3fc091fbbd87",
"desc": "you wanna try",
"completed": false
},
{
"id": "e89d582b-1a90-a0f1-be07-623ddb29d55e",
"desc": "have to say good",
"completed": false
}
]
}
在src\app\todo\todo.service.ts中更改
// private api_url = 'api/todos';
private api_url = 'http://localhost:3000/todos';
并將addTodo和getTodos中then語句中的 res.json().data替換成res.json()。在AppModule中刪掉內(nèi)存web服務(wù)相關(guān)的語句。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { TodoModule } from './todo/todo.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';
@NgModule({
declarations: [
AppComponent,
LoginComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
routing,
TodoModule
],
providers: [
{provide: 'auth', useClass: AuthService}
],
bootstrap: [AppComponent]
})
export class AppModule { }
另外打開一個命令窗口,進(jìn)入工程目錄,輸入json-server ./src/app/todo/todo-data.json
欣賞一下成果吧

完善Todo應(yīng)用
在結(jié)束本節(jié)前,我們得給Todo應(yīng)用收個尾,還差一些功能沒完成:
- 從架構(gòu)上來講,我們似乎還可以進(jìn)一步構(gòu)建出TodoList和TodoItem兩個組件
- 全選并反轉(zhuǎn)狀態(tài)
- 底部篩選器:All,Active,Completed
- 清理已完成項(xiàng)目
TodoItem和TodoList組件
在命令行窗口鍵入ng g c todo/todo-item,angular-cli會十分聰明的幫你在todo目錄下建好TodoItem組件,并且在TodoModule中聲明。一般來說,如果要生成某個模塊下的組件,輸入ng g c 模塊名稱/組件名稱。 好的,類似的我們再建立一個TodoList控件,ng g c todo/todo-list。我們希望未來的todo.component.html是下面這個樣子的
//todo.component.html
<section class="todoapp">
<app-todo-header
placeholder="What do you want"
(textChanges)="onTextChanges($event)"
(onEnterUp)="addTodo()" >
</app-todo-header>
<app-todo-list
[todos]="todos"
(onRemoveTodo)="removeTodo($event)"
(onToggleTodo)="toggleTodo($event)"
>
</app-todo-list>
<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
</section>
那么TodoItem哪兒去了呢?TodoItem是TodoList的子組件,TodoItem的模板應(yīng)該是todos循環(huán)內(nèi)的一個todo的模板。TodoList的HTML模板看起來應(yīng)該是下面的樣子:
<section class="main" *ngIf="todos?.length > 0">
<input class="toggle-all" type="checkbox">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.completed">
<app-todo-item
[isChecked]="todo.completed"
(onToggleTriggered)="onToggleTriggered(todo)"
(onRemoveTriggered)="onRemoveTriggered(todo)"
[todoDesc]="todo.desc">
</app-todo-item>
</li>
</ul>
</section>
那么我們先從最底層的TodoItem看,這個組件怎么剝離出來?首先來看todo-item.component.html
<div class="view">
<input class="toggle" type="checkbox" (click)="toggle()" [checked]="isChecked">
<label [class.labelcompleted]="isChecked" (click)="toggle()">{{todoDesc}}</label>
<button class="destroy" (click)="remove(); $event.stopPropagation()"></button>
</div>
我們需要確定有哪些輸入型和輸出型參數(shù)
- isChecked:輸入型參數(shù),用來確定是否被選中,由父組件(TodoList)設(shè)置
- todoDesc:輸入型參數(shù),顯示Todo的文本描述,由父組件設(shè)置
- onToggleTriggered:輸出型參數(shù),在用戶點(diǎn)擊checkbox或label時(shí)以事件形式通知父組件。在TodoItem中我們是在處理用戶點(diǎn)擊事件時(shí)在toggle方法中發(fā)射這個事件。
- onRemoveTriggered:輸出型參數(shù),在用戶點(diǎn)擊刪除按鈕時(shí)以事件形式通知父組件。在TodoItem中我們是在處理用戶點(diǎn)擊按鈕事件時(shí)在remove方法中發(fā)射這個事件。
//todo-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-todo-item',
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent{
@Input() isChecked: boolean = false;
@Input() todoDesc: string = '';
@Output() onToggleTriggered = new EventEmitter<boolean>();
@Output() onRemoveTriggered = new EventEmitter<boolean>();
toggle() {
this.onToggleTriggered.emit(true);
}
remove() {
this.onRemoveTriggered.emit(true);
}
}
建立好TodoItem后,我們再來看TodoList,還是從模板看一下
<section class="main" *ngIf="todos?.length > 0">
<input class="toggle-all" type="checkbox">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.completed">
<app-todo-item
[isChecked]="todo.completed"
(onToggleTriggered)="onToggleTriggered(todo)"
(onRemoveTriggered)="onRemoveTriggered(todo)"
[todoDesc]="todo.desc">
</app-todo-item>
</li>
</ul>
</section>
TodoList需要一個輸入型參數(shù)todos,由父組件(TodoComponent)指定,TodoList本身不需要知道這個數(shù)組是怎么來的,它和TodoItem只是負(fù)責(zé)顯示而已。當(dāng)然我們由于在TodoList里面還有TodoITem子組件,而且TodoList本身不會處理這個輸出型參數(shù),所以我們需要把子組件的輸出型參數(shù)再傳遞給TodoComponent進(jìn)行處理。
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo.model';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
_todos: Todo[] = [];
@Input()
set todos(todos:Todo[]){
this._todos = [...todos];
}
get todos() {
return this._todos;
}
@Output() onRemoveTodo = new EventEmitter<Todo>();
@Output() onToggleTodo = new EventEmitter<Todo>();
onRemoveTriggered(todo: Todo) {
this.onRemoveTodo.emit(todo);
}
onToggleTriggered(todo: Todo) {
this.onToggleTodo.emit(todo);
}
}
上面代碼中有一個新東東,就是在todos()方法前我們看到有set和get兩個訪問修飾符。這個是由于我們?nèi)绻裻odos當(dāng)成一個成員變量給出的話,在設(shè)置后如果父組件的todos數(shù)組改變了,子組件并不知道這個變化,從而不能更新子組件本身的內(nèi)容。所以我們把todos做成了方法,而且通過get和set修飾成屬性方法,也就是說從模板中引用的話可以寫成{{todos}}。通過標(biāo)記set todos()為@Input我們可以監(jiān)視父組件的數(shù)據(jù)變化。
現(xiàn)在回過頭來看一下todo.component.html,我們看到(onRemoveTodo)="removeTodo($event)",這句是為了處理子組件(TodoList)的輸出型參數(shù)(onRemoveTodo),而$event其實(shí)就是這個事件反射器攜帶的參數(shù)(這里是todo:Todo)。我們通過這種機(jī)制完成組件間的數(shù)據(jù)交換。
//todo.component.html
<section class="todoapp">
<app-todo-header
placeholder="What do you want"
(textChanges)="onTextChanges($event)"
(onEnterUp)="addTodo()" >
</app-todo-header>
<app-todo-list
[todos]="todos"
(onRemoveTodo)="removeTodo($event)"
(onToggleTodo)="toggleTodo($event)"
>
</app-todo-list>
<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
</section>
講到這里大家可能要問是不是過度設(shè)計(jì)了,這么少的功能用得著這么設(shè)計(jì)嗎?是的,本案例屬于過度設(shè)計(jì),但我們的目的是展示出更多的Angular實(shí)戰(zhàn)方法和特性。
填坑,完成漏掉的功能
現(xiàn)在我們還差幾個功能:全部反轉(zhuǎn)狀態(tài)(ToggleAll),清除全部已完成任務(wù)(Clear Completed)和狀態(tài)篩選器。我們的設(shè)計(jì)方針是邏輯功能放在TodoComponent中,而其他子組件只負(fù)責(zé)表現(xiàn)。這樣的話,我們先來看看邏輯上應(yīng)該怎么完成。
用路由參數(shù)傳遞數(shù)據(jù)
首先看一下過濾器,在Footer中我們有三個過濾器:All,Active和Completed,點(diǎn)擊任何一個過濾器,我們只想顯示過濾后的數(shù)據(jù)。

這個功能其實(shí)有幾種可以實(shí)現(xiàn)的方式,第一種我們可以按照之前講過的組件間傳遞數(shù)據(jù)的方式設(shè)置一個@Output的事件發(fā)射器來實(shí)現(xiàn)。但本節(jié)中我們采用另一種方式,通過路由傳遞參數(shù)來實(shí)現(xiàn)。Angular2可以給路由添加參數(shù),最簡單的一種方式是比如/todo是我們的TodoComponent處理的路徑,如果希望攜帶一個filter參數(shù)的話,可以在路由定義中寫成
{
path: 'todo/:filter',
component: TodoComponent
}
這個:filter是一個參數(shù)表達(dá)式,也就是說例如todo/ACTIVE就意味著參數(shù)filter='ACTIVE'??瓷先ビ悬c(diǎn)像子路由,但這里我們使用一個組件去處理不同路徑的,所以todo/后面的數(shù)據(jù)就被當(dāng)作路由參數(shù)來對待了。這樣的話就比較簡單了,我們在todo-footer.component.html中把幾個過濾器指向的路徑寫一下,注意這里和需要使用Angular2特有的路由鏈接指令(routerLink)
<ul class="filters">
<li><a routerLink="/todo/ALL">All</a></li>
<li><a routerLink="/todo/ACTIVE">Active</a></li>
<li><a routerLink="/todo/COMPLETED">Completed</a></li>
</ul>
當(dāng)然我們還需要在todo.routes.ts中增加路由參數(shù)到路由數(shù)組中
{
path: 'todo/:filter',
component: TodoComponent
}
根路由定義也需要改寫一下,因?yàn)樵瓉韙odo不帶參數(shù)時(shí),我們直接重定向到todo模塊即可,但現(xiàn)在有參數(shù)的話應(yīng)該重定向到默認(rèn)參數(shù)是“ALL”的路徑;
{
path: 'todo',
redirectTo: 'todo/ALL'
}
現(xiàn)在打開todo.component.ts看看怎么接收這個參數(shù):
- 引入路由對象
import { Router, ActivatedRoute, Params } from '@angular/router'; - 在構(gòu)造中注入
ActivatedRoute和Router
constructor(
@Inject('todoService') private service,
private route: ActivatedRoute,
private router: Router) {}
然后在ngOnInit()中添加下面的代碼,一般的邏輯代碼如果需要在ngOnInit()中調(diào)用。
ngOnInit() {
this.route.params.forEach((params: Params) => {
let filter = params['filter'];
this.filterTodos(filter);
});
}
從this.route.params返回的是一個Observable,里面包含著所傳遞的參數(shù),當(dāng)然我們這個例子很簡單只有一個,就是剛才定義的filter。當(dāng)然我們需要在組件內(nèi)添加對各種filter處理的方法:調(diào)用service中的處理方法后對todos數(shù)組進(jìn)行操作。組件中原有的getTodos方法已經(jīng)沒有用了,刪掉吧。
filterTodos(filter: string): void{
this.service
.filterTodos(filter)
.then(todos => this.todos = [...todos]);
}
最后我們看看在todo.service.ts中我們?nèi)绾螌?shí)現(xiàn)這個方法
// GET /todos?completed=true/false
filterTodos(filter: string): Promise<Todo[]> {
switch(filter){
case 'ACTIVE': return this.http
.get(`${this.api_url}?completed=false`)
.toPromise()
.then(res => res.json() as Todo[])
.catch(this.handleError);
case 'COMPLETED': return this.http
.get(`${this.api_url}?completed=true`)
.toPromise()
.then(res => res.json() as Todo[])
.catch(this.handleError);
default:
return this.getTodos();
}
}
至此大功告成,我們來看看效果吧。現(xiàn)在輸入http://localhost:4200/todo進(jìn)入后觀察瀏覽器地址欄,看到了吧,路徑自動被修改成了http://localhost:4200/todo/ALL,我們的在跟路由中定義的重定向起作用了!

現(xiàn)在,試著點(diǎn)擊其中某個todo更改其完成狀態(tài),然后點(diǎn)擊Active,我們看到不光路徑變了,數(shù)據(jù)也按照我們期待的方式更新了。

批量修改和批量刪除
ToggleAll和ClearCompleted的功能其實(shí)是一個批量修改和批量刪除的過程。
在todo-footer.component.html中增加Clear Completed按鈕的事件處理
<button class="clear-completed" (click)="onClick()">Clear completed</button>
Clear Completed在Footer中,所以我們需要給Footer組件增加一個輸出型參數(shù)onClear和onClick()事件處理方法
//todo-footer.component.ts
...
@Output() onClear = new EventEmitter<boolean>();
onClick(){
this.onClear.emit(true);
}
...
類似的,ToggleAll位于TodoList中,所以在todo-list.component.html中為其增加點(diǎn)擊事件
<input class="toggle-all" type="checkbox" (click)="onToggleAllTriggered()">
在todo-list.component.ts中增加一個輸出型參數(shù)onToggleAll和onToggleAllTriggered的方法
@Output() onToggleAll = new EventEmitter<boolean>();
onToggleAllTriggered() {
this.onToggleAll.emit(true);
}
在父組件模板中添加子組件中剛剛聲明的新屬性,在todo.component.html中為app-todo-list和app-todo-footer添加屬性:
...
<app-todo-list
...
(onToggleAll)="toggleAll()"
>
</app-todo-list>
<app-todo-footer
...
(onClear)="clearCompleted()">
</app-todo-footer>
...
最后在父組件(todo.component.ts)中添加對應(yīng)的處理方法。最直覺的做法是循環(huán)數(shù)組,執(zhí)行已有的toggleTodo(todo: Todo)和removeTodo(todo: Todo)。我們更改一下todo.component.ts,增加下面兩個方法:
toggleAll(){
this.todos.forEach(todo => this.toggleTodo(todo));
}
clearCompleted(){
const todos = this.todos.filter(todo=> todo.completed===true);
todos.forEach(todo => this.removeTodo(todo));
}
先保存一下,點(diǎn)擊一下輸入框左邊的下箭頭圖標(biāo)或者右下角的“Clear Completed”,看看效果

大功告成!慢著,等一下,哪里好像不太對。讓我們回過頭再看看
toggleAll方法和clearCompleted方法。目前的實(shí)現(xiàn)方式有個明顯問題,那就是現(xiàn)在的處理方式又變成同步的了(this.todos.forEach()是個同步方法),如果我們的處理邏輯比較復(fù)雜的話,現(xiàn)在的實(shí)現(xiàn)方式會導(dǎo)致UI沒有響應(yīng)。但是如果不這么做的話,對于一系列的異步操作我們怎么處理呢?Promise.all(iterable)就是應(yīng)對這種情況的,它適合把一系列的Promise一起處理,直到所有的Promise都處理完(或者是異常時(shí)reject),之后也返回一個Promise,里面是所有的返回值。
let p1 = Promise.resolve(3);
let p2 = 1337;
let p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});
但是還有個問題,我們目前的toggleTodo(todo: Todo)和removeTodo(todo: Todo)并不返回Promise,所以也需要小改造一下:
//todo.component.ts片段
toggleTodo(todo: Todo): Promise<void> {
const i = this.todos.indexOf(todo);
return this.service
.toggleTodo(todo)
.then(t => {
this.todos = [
...this.todos.slice(0,i),
t,
...this.todos.slice(i+1)
];
return null;
});
}
removeTodo(todo: Todo): Promise<void> {
const i = this.todos.indexOf(todo);
return this.service
.deleteTodoById(todo.id)
.then(()=> {
this.todos = [
...this.todos.slice(0,i),
...this.todos.slice(i+1)
];
return null;
});
}
toggleAll(){
Promise.all(this.todos.map(todo => this.toggleTodo(todo)));
}
clearCompleted(){
const completed_todos = this.todos.filter(todo => todo.completed === true);
const active_todos = this.todos.filter(todo => todo.completed === false);
Promise.all(completed_todos.map(todo => this.service.deleteTodoById(todo.id)))
.then(() => this.todos = [...active_todos]);
}
現(xiàn)在再去試試效果,應(yīng)該一切功能正常。當(dāng)然這個版本其實(shí)還是有問題的,本質(zhì)上還是在循環(huán)調(diào)用toggleTodo和removeTodo,這樣做會導(dǎo)致多次進(jìn)行HTTP連接,所以最佳策略應(yīng)該是請服務(wù)器后端同學(xué)增加一個批處理的API給我們。但是服務(wù)器端的編程不是本教程的范疇,這里就不展開了,大家只需記住如果在生產(chǎn)環(huán)境中切記要減少HTTP請求的次數(shù)和縮減發(fā)送數(shù)據(jù)包的大小。說到減小HTTP交互數(shù)據(jù)的大小的話,我們的todo.service.ts中可以對toggleTodo方法做點(diǎn)改造。原來的put方法是將整個todo數(shù)據(jù)上傳,但其實(shí)我們只改動了todo.completed屬性。如果你的web api是符合REST標(biāo)準(zhǔn)的話,我們可以用Http的PATCH方法而不是PUT方法,PATCH方法會只上傳變化的數(shù)據(jù)。
// It was PUT /todos/:id before
// But we will use PATCH /todos/:id instead
// Because we don't want to waste the bytes those don't change
toggleTodo(todo: Todo): Promise<Todo> {
const url = `${this.api_url}/${todo.id}`;
let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
return this.http
.patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})
.toPromise()
.then(() => updatedTodo)
.catch(this.handleError);
}
最后其實(shí)Todo的所有子組件其實(shí)都沒有用到ngInit,所以不必實(shí)現(xiàn)NgInit接口,可以去掉ngInit方法和相關(guān)的接口引用。
本節(jié)代碼: https://github.com/wpcfan/awesome-tutorials/tree/chap04/angular2/ng2-tut
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner
紙書出版了,比網(wǎng)上內(nèi)容豐富充實(shí)了,歡迎大家訂購!
京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

第一節(jié):初識Angular-CLI
第二節(jié):登錄組件的構(gòu)建
第三節(jié):建立一個待辦事項(xiàng)應(yīng)用
第四節(jié):進(jìn)化!模塊化你的應(yīng)用
第五節(jié):多用戶版本的待辦事項(xiàng)應(yīng)用
第六節(jié):使用第三方樣式庫及模塊優(yōu)化用
第七節(jié):給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應(yīng)用
第八節(jié):查缺補(bǔ)漏大合集(上)
第九節(jié):查缺補(bǔ)漏大合集(下)