摘要:而且此時我們注意到其實沒有任何一個地方目前還需引用了,這就是說我們可以安全地把從組件中的修飾符中刪除了。
第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
作者:王芃 wpcfan@gmail.com
第四節:進化!模塊化你的應用 一個復雜組件的分拆上一節的末尾我偷懶的甩出了大量代碼,可能你看起來都有點暈了,這就是典型的一個功能經過一段時間的需求累積后,代碼也不可避免的臃腫起來。現在我們看看怎么分拆一下吧。
image_1b11kjibcelb6upnb21su41dilm.png-59.5kB
我們的應用似乎可以分為Header,Main和Footer幾部分。首先我們來建立一個新的Component,鍵入ng g c todo/todo-footer。然后將srcapp odo odo.component.html中的段落剪切到srcapp odo odo-footer odo-footer.component.html中。
觀察上面的代碼,我們看到似乎所有的變量都是todos?.length,這提醒我們其實對于Footer來說,我們并不需要傳入todos,而只需要給出一個item計數即可。那么我們來把所有的todos?.length改成itemCount。
這樣的話也就是說如果在srcapp odo odo.component.html中我們可以用
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() { } }
運行一下看看效果,應該一切正常!
類似的我們建立一個Header組件,鍵入ng g c todo/todo-header,同樣的把下面的代碼從srcapp odo odo.component.html中剪切到srcapp odo odo-header odo-header.component.html中
Todos
這段代碼看起來有點麻煩,主要原因是我們好像不但需要給子組件輸入什么,而且希望子組件給父組件要輸出一些東西,比如輸入框的值和按下回車鍵的消息等。當然你可能猜到了,Angular2里面有@Input()就相應的有@Output()修飾符。
我們希望輸入框的占位文字(沒有輸入的情況下顯示的默認文字)是一個輸入型的參數,在回車鍵抬起時可以發射一個事件給父組件,同時我們也希望在輸入框輸入文字時父組件能夠得到這個字符串。也就是說父組件調用子組件時看起來是下面的樣子,相當于我們自定義的組件中提供一些事件,父組件調用時可以寫自己的事件處理方法,而$event就是子組件發射的事件對象:
但是第三個需求也就是“在輸入框輸入文字時父組件能夠得到這個字符串”,這個有點問題,如果每輸入一個字符都要回傳給父組件的話,系統會過于頻繁進行這種通信,有可能會有性能的問題。那么我們希望可以有一個類似濾波器的東東,它可以過濾掉一定時間內的事件。因此我們定義一個輸入型參數delay。
現在的標簽引用應該是上面這個樣子,但我們只是策劃了它看起來是什么樣子,還沒有做呢。我們一起動手看看怎么做吧。
todo-header.component.html的模板中我們調整了一些變量名和參數以便讓大家不混淆子組件自己的模板和父組件中引用子組件的模板片段。
//todo-header.component.htmlTodos
記住子組件的模板是描述子組件自己長成什么樣子,應該有哪些行為,這些東西和父組件沒有任何關系。比如todo-header.component.html中的placeholder就是HTML標簽Input中的一個屬性,和父組件沒有關聯,如果我們不在todo-header.component.ts中聲明@Input() placeholder,那么子組件就沒有這個屬性,在父組件中也無法設置這個屬性。父組件中的聲明為@Input()的屬性才會成為子組件對外可見的屬性,我們完全可以把@Input() placeholder聲明為@Input() hintText,這樣的話在引用header組件時,我們就需要這樣寫
現在看一下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(); //detect the enter keyup event and output this to parent @Output() onEnterUp = new EventEmitter (); 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個輸入型變量,這樣
接下來我們看到了由@Output修飾的onTextChanges和onEnterUp,這兩個顧名思義是分別處理文本變化和回車鍵抬起事件的,這兩個變量呢都定義成了EventEmitter(事件發射器)。我們會在子組件的邏輯代碼中以適當的條件去發射對應事件,而父組件會接收到這些事件。我們這里采用了2中方法來觸發發射器
enterUp:這個是比較常規的方法,在todo-header.component.html中我們定義了(keyup.enter)="enterUp()",所以在組件的enterUp方法中,我們直接讓onEnterUp發射了對應事件。
構造器中使用Rx:這里涉及了很多新知識,首先我們注入了ElementRef,這個是一個Angular中需要謹慎使用的對象,因為它可以讓你直接操作DOM,也就是HTML的元素和事件。同時我們使用了Rx(響應式對象),Rx是一個很復雜的話題,這里我們不展開了,但我們主要是利用Observable去觀察HTML中的keyup事件,然后在這個事件流中做一個轉換把輸入框的值發射出來(map),應用一個時間的濾波器(debounceTime),然后應用一個篩選器(distinctUntilChanged)。這里由于這個事件的發射條件是依賴于輸入時的當時條件,我們沒有辦法按前面的以模板事件觸發做處理。
最后需要在todo.component.ts中加入對header輸出參數發射事件的處理
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; } }
當然上述代碼要從todo.component.css中刪除,現在的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,"); } .todo-list li .toggle:checked:after { content: url("data:image/svg+xml;utf8,"); } .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; } }封裝成獨立模塊
現在我們的todo目錄下好多文件了,而且我們觀察到這個功能相對很獨立。這種情況下我們似乎沒有必要將所有的組件都聲明在根模塊AppModule當中,因為類似像子組件沒有被其他地方用到。Angular中提供了一種組織方式,那就是模塊。模塊和根模塊很類似,我們先在todo目錄下建一個文件srcapp odo odo.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 {}
注意一點,我們沒有引入BrowserModule,而是引入了CommonModule。導入 BrowserModule 會讓該模塊公開的所有組件、指令和管道在 AppModule 下的任何組件模板中直接可用,而不需要額外的繁瑣步驟。CommonModule 提供了很多應用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 導入了 CommonModule 并且 重新導出 了它。 最終的效果是:只要導入 BrowserModule 就自動獲得了 CommonModule 中的指令。幾乎所有要在瀏覽器中使用的應用的 根模塊 ( AppModule )都應該從 @angular/platform-browser 中導入 BrowserModule 。在其它任何模塊中都 不要導入 BrowserModule,應該改成導入 CommonModule 。 它們需要通用的指令。它們不需要重新初始化全應用級的提供商。
由于和根模塊很類似,我們就不展開講了。需要做的事情是把TodoComponent中的TodoService改成用@Inject("todoService")來注入。但是注意一點,我們需要模塊自己的路由定義。我們在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”,另外一點和根路由不一樣的是export const routing = RouterModule.forChild(routes);,我們用的是forChild而不是forRoot,因為forRoot只能用于根目錄,所有非根模塊的其他模塊路由都只能用forChild。下面就得更改根路由了,srcappapp.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);
注意到我們去掉了TodoComponent的依賴,而且更改todo路徑定義為redirecTo到todo路徑,但沒有給出組件,這叫做“無組件路由”,也就是說后面的事情是TodoModule負責的。
此時我們就可以去掉AppModule中引用的Todo相關的組件了。
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 { }
而且此時我們注意到其實沒有任何一個地方目前還需引用
這里我們不想再使用內存Web服務了,因為如果使用,我們無法將其封裝在TodoModule中。所以我們使用一個更“真”的web服務: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 } ] }
在srcapp odo odo.service.ts中更改
// private api_url = "api/todos"; private api_url = "http://localhost:3000/todos";
并將addTodo和getTodos中then語句中的 res.json().data替換成res.json()。在AppModule中刪掉內存web服務相關的語句。
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 { }
另外打開一個命令窗口,進入工程目錄,輸入json-server ./src/app/todo/todo-data.json
欣賞一下成果吧
image_1b12b5v4onlm16ai1bdn7pu143e9.png-165.7kB
在結束本節前,我們得給Todo應用收個尾,還差一些功能沒完成:
從架構上來講,我們似乎還可以進一步構建出TodoList和TodoItem兩個組件
全選并反轉狀態
底部篩選器:All,Active,Completed
清理已完成項目
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
那么TodoItem哪兒去了呢?TodoItem是TodoList的子組件,TodoItem的模板應該是todos循環內的一個todo的模板。TodoList的HTML模板看起來應該是下面的樣子:
0">
那么我們先從最底層的TodoItem看,這個組件怎么剝離出來?首先來看todo-item.component.html
我們需要確定有哪些輸入型和輸出型參數
isChecked:輸入型參數,用來確定是否被選中,由父組件(TodoList)設置
todoDesc:輸入型參數,顯示Todo的文本描述,由父組件設置
onToggleTriggered:輸出型參數,在用戶點擊checkbox或label時以事件形式通知父組件。在TodoItem中我們是在處理用戶點擊事件時在toggle方法中發射這個事件。
onRemoveTriggered:輸出型參數,在用戶點擊刪除按鈕時以事件形式通知父組件。在TodoItem中我們是在處理用戶點擊按鈕事件時在remove方法中發射這個事件。
//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(); @Output() onRemoveTriggered = new EventEmitter (); toggle() { this.onToggleTriggered.emit(true); } remove() { this.onRemoveTriggered.emit(true); } }
建立好TodoItem后,我們再來看TodoList,還是從模板看一下
0">
TodoList需要一個輸入型參數todos,由父組件(TodoComponent)指定,TodoList本身不需要知道這個數組是怎么來的,它和TodoItem只是負責顯示而已。當然我們由于在TodoList里面還有TodoITem子組件,而且TodoList本身不會處理這個輸出型參數,所以我們需要把子組件的輸出型參數再傳遞給TodoComponent進行處理。
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(); @Output() onToggleTodo = new EventEmitter (); onRemoveTriggered(todo: Todo) { this.onRemoveTodo.emit(todo); } onToggleTriggered(todo: Todo) { this.onToggleTodo.emit(todo); } }
上面代碼中有一個新東東,就是在todos()方法前我們看到有set和get兩個訪問修飾符。這個是由于我們如果把todos當成一個成員變量給出的話,在設置后如果父組件的todos數組改變了,子組件并不知道這個變化,從而不能更新子組件本身的內容。所以我們把todos做成了方法,而且通過get和set修飾成屬性方法,也就是說從模板中引用的話可以寫成{{todos}}。通過標記set todos()為@Input我們可以監視父組件的數據變化。
現在回過頭來看一下todo.component.html,我們看到(onRemoveTodo)="removeTodo($event)",這句是為了處理子組件(TodoList)的輸出型參數(onRemoveTodo),而$event其實就是這個事件反射器攜帶的參數(這里是todo:Todo)。我們通過這種機制完成組件間的數據交換。
//todo.component.html
講到這里大家可能要問是不是過度設計了,這么少的功能用得著這么設計嗎?是的,本案例屬于過度設計,但我們的目的是展示出更多的Angular實戰方法和特性。
填坑,完成漏掉的功能現在我們還差幾個功能:全部反轉狀態(ToggleAll),清除全部已完成任務(Clear Completed)和狀態篩選器。我們的設計方針是邏輯功能放在TodoComponent中,而其他子組件只負責表現。這樣的話,我們先來看看邏輯上應該怎么完成。
用路由參數傳遞數據首先看一下過濾器,在Footer中我們有三個過濾器:All,Active和Completed,點擊任何一個過濾器,我們只想顯示過濾后的數據。
image_1b17mtibdkjn105l1ojl1dgr9il9.png-6.5kB
這個功能其實有幾種可以實現的方式,第一種我們可以按照之前講過的組件間傳遞數據的方式設置一個@Output的事件發射器來實現。但本節中我們采用另一種方式,通過路由傳遞參數來實現。Angular2可以給路由添加參數,最簡單的一種方式是比如/todo是我們的TodoComponent處理的路徑,如果希望攜帶一個filter參數的話,可以在路由定義中寫成
{ path: "todo/:filter", component: TodoComponent }
這個:filter是一個參數表達式,也就是說例如todo/ACTIVE就意味著參數filter="ACTIVE"。看上去有點像子路由,但這里我們使用一個組件去處理不同路徑的,所以todo/后面的數據就被當作路由參數來對待了。這樣的話就比較簡單了,我們在todo-footer.component.html中把幾個過濾器指向的路徑寫一下,注意這里和需要使用Angular2特有的路由鏈接指令(routerLink)
當然我們還需要在todo.routes.ts中增加路由參數到路由數組中
{ path: "todo/:filter", component: TodoComponent }
根路由定義也需要改寫一下,因為原來todo不帶參數時,我們直接重定向到todo模塊即可,但現在有參數的話應該重定向到默認參數是“ALL”的路徑;
{ path: "todo", redirectTo: "todo/ALL" }
現在打開todo.component.ts看看怎么接收這個參數:
引入路由對象 import { Router, ActivatedRoute, Params } from "@angular/router";
在構造中注入ActivatedRoute和Router
constructor( @Inject("todoService") private service, private route: ActivatedRoute, private router: Router) {}
然后在ngOnInit()中添加下面的代碼,一般的邏輯代碼如果需要在ngOnInit()中調用。
ngOnInit() { this.route.params.forEach((params: Params) => { let filter = params["filter"]; this.filterTodos(filter); }); }
從this.route.params返回的是一個Observable,里面包含著所傳遞的參數,當然我們這個例子很簡單只有一個,就是剛才定義的filter。當然我們需要在組件內添加對各種filter處理的方法:調用service中的處理方法后對todos數組進行操作。組件中原有的getTodos方法已經沒有用了,刪掉吧。
filterTodos(filter: string): void{ this.service .filterTodos(filter) .then(todos => this.todos = [...todos]); }
最后我們看看在todo.service.ts中我們如何實現這個方法
// GET /todos?completed=true/false filterTodos(filter: string): Promise{ 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(); } }
至此大功告成,我們來看看效果吧。現在輸入http://localhost:4200/todo進入后觀察瀏覽器地址欄,看到了吧,路徑自動被修改成了http://localhost:4200/todo/ALL,我們的在跟路由中定義的重定向起作用了!
image_1b17o06nv10ob13d6pb1f5613pnm.png-137.8kB
現在,試著點擊其中某個todo更改其完成狀態,然后點擊Active,我們看到不光路徑變了,數據也按照我們期待的方式更新了。
image_1b17o6qjlb31grg1o7edjm1q4l13.png-128kB
ToggleAll和ClearCompleted的功能其實是一個批量修改和批量刪除的過程。
在todo-footer.component.html中增加Clear Completed按鈕的事件處理
Clear Completed在Footer中,所以我們需要給Footer組件增加一個輸出型參數onClear和onClick()事件處理方法
//todo-footer.component.ts ... @Output() onClear = new EventEmitter(); onClick(){ this.onClear.emit(true); } ...
類似的,ToggleAll位于TodoList中,所以在todo-list.component.html中為其增加點擊事件
在todo-list.component.ts中增加一個輸出型參數onToggleAll和onToggleAllTriggered的方法
@Output() onToggleAll = new EventEmitter(); onToggleAllTriggered() { this.onToggleAll.emit(true); }
在父組件模板中添加子組件中剛剛聲明的新屬性,在todo.component.html中為app-todo-list和app-todo-footer添加屬性:
......
最后在父組件(todo.component.ts)中添加對應的處理方法。最直覺的做法是循環數組,執行已有的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)); }
先保存一下,點擊一下輸入框左邊的下箭頭圖標或者右下角的“Clear Completed”,看看效果
image_1b1c8if181tld15hlj531aasi8a9.png-140kB
大功告成!慢著,等一下,哪里好像不太對。讓我們回過頭再看看toggleAll方法和clearCompleted方法。目前的實現方式有個明顯問題,那就是現在的處理方式又變成同步的了(this.todos.forEach()是個同步方法),如果我們的處理邏輯比較復雜的話,現在的實現方式會導致UI沒有響應。但是如果不這么做的話,對于一系列的異步操作我們怎么處理呢?Promise.all(iterable)就是應對這種情況的,它適合把一系列的Promise一起處理,直到所有的Promise都處理完(或者是異常時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{ 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 { 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]); }
現在再去試試效果,應該一切功能正常。當然這個版本其實還是有問題的,本質上還是在循環調用toggleTodo和removeTodo,這樣做會導致多次進行HTTP連接,所以最佳策略應該是請服務器后端同學增加一個批處理的API給我們。但是服務器端的編程不是本教程的范疇,這里就不展開了,大家只需記住如果在生產環境中切記要減少HTTP請求的次數和縮減發送數據包的大小。說到減小HTTP交互數據的大小的話,我們的todo.service.ts中可以對toggleTodo方法做點改造。原來的put方法是將整個todo數據上傳,但其實我們只改動了todo.completed屬性。如果你的web api是符合REST標準的話,我們可以用Http的PATCH方法而不是PUT方法,PATCH方法會只上傳變化的數據。
// 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{ 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); }
最后其實Todo的所有子組件其實都沒有用到ngInit,所以不必實現NgInit接口,可以去掉ngInit方法和相關的接口引用。
本節代碼: https://github.com/wpcfan/awe...
第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81809.html
摘要:官方支持微軟出品,是的超集,是的強類型版本作為首選編程語言,使得開發腳本語言的一些問題可以更早更方便的找到。第一個組件那么我們來為我們的增加一個吧,在命令行窗口輸入。引導過程通過在中引導來啟動應用。它們的核心就是。 第一節:Angular 2.0 從0到1 (一)第二節:Angular 2.0 從0到1 (二)第三節:Angular 2.0 從0到1 (三) 第一章:認識Angular...
摘要:下面我們看看如果使用是什么樣子的,首先我們需要在組件的修飾器中配置,然后在組件的構造函數中使用參數進行依賴注入。 第一節:Angular 2.0 從0到1 (一)第二節:Angular 2.0 從0到1 (二)第三節:Angular 2.0 從0到1 (三) 第二節:用Form表單做一個登錄控件 對于login組件的小改造 在 hello-angularsrcapploginlogin...
摘要:如果該構造函數在我們所期望的中運行,就沒有任何祖先注入器能夠提供的實例,于是注入器會放棄查找。但裝飾器表示找不到該服務也無所謂。用處理導航到子路由的情況。路由器會先按照從最深的子路由由下往上檢查的順序來檢查守護條件。 第一節:Angular 2.0 從0到1 (一)第二節:Angular 2.0 從0到1 (二)第三節:Angular 2.0 從0到1 (三)第四節:Angular 2...
摘要:如何在中使用動畫前端掘金本文講一下中動畫應用的部分。與的快速入門指南推薦前端掘金是非常棒的框架,能夠創建功能強大,動態功能的。自發布以來,已經廣泛應用于開發中。 如何在 Angular 中使用動畫 - 前端 - 掘金本文講一下Angular中動畫應用的部分。 首先,Angular本生不提供動畫機制,需要在項目中加入Angular插件模塊ngAnimate才能完成Angular的動畫機制...
閱讀 3623·2021-11-24 10:22
閱讀 3691·2021-11-22 09:34
閱讀 2495·2021-11-15 11:39
閱讀 1533·2021-10-14 09:42
閱讀 3668·2021-10-08 10:04
閱讀 1561·2019-08-30 15:52
閱讀 851·2019-08-30 13:49
閱讀 3024·2019-08-30 11:21