Angular 從 0 到 1 (四)史上最簡單的 Angular 教程

第一節:初識Angular-CLI
第二節:登陸組件的構建
第三節:創建一個待辦事項應用
第四節:進化!模塊化你的應用
第五節:多用戶版本的待辦事項應用
第六節:使用第三方樣式庫及模塊優化用
第七節:給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)javascript

第四節:進化!模塊化你的應用

一個複雜組件的分拆

上一節的末尾我偷懶的甩出了大量代碼,可能你看起來都有點暈了,這就是典型的一個功能通過一段時間的需求累積後,代碼也不可避免的臃腫起來。如今咱們看看怎麼分拆一下吧。
css

image_1b11kjibcelb6upnb21su41dilm.png-59.5kB

咱們的應用彷佛能夠分爲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,這提醒咱們其實對於Footer來講,咱們並不須要傳入todos,而只須要給出一個item計數便可。那麼咱們來把全部的todos?.length改爲itemCounthtml

<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項目計數給Footer便可。因此在src\app\todo\todo.component.html中剛纔咱們剪切掉代碼的位置加上這句吧。固然,若是要讓父組件能夠傳遞值給子組件,咱們還須要在子組件中聲明一下。@Input()是輸入型綁定的修飾符,用於把數據從父組件傳到子組件。java

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() {
  }
}複製代碼

運行一下看看效果,應該一切正常!git

相似的咱們創建一個Header組件,鍵入ng g c todo/todo-header,一樣的把下面的代碼從src\app\todo\todo.component.html中剪切到src\app\todo\todo-header\todo-header.component.htmlgithub

<header class="header">
  <h1>Todos</h1>
  <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
</header>複製代碼

這段代碼看起來有點麻煩,主要緣由是咱們好像不但須要給子組件輸入什麼,並且但願子組件給父組件要輸出一些東西,好比輸入框的值和按下回車鍵的消息等。固然你可能猜到了,Angular2裏面有@Input()就相應的有@Output()修飾符。
咱們但願輸入框的佔位文字(沒有輸入的狀況下顯示的默認文字)是一個輸入型的參數,在回車鍵擡起時能夠發射一個事件給父組件,同時咱們也但願在輸入框輸入文字時父組件可以獲得這個字符串。也就是說父組件調用子組件時看起來是下面的樣子,至關於咱們自定義的組件中提供一些事件,父組件調用時能夠寫本身的事件處理方法,而$event就是子組件發射的事件對象:web

<app-todo-header placeholder="What do you want" (onTextChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" >
</app-todo-header>複製代碼

可是第三個需求也就是「在輸入框輸入文字時父組件可以獲得這個字符串」,這個有點問題,若是每輸入一個字符都要回傳給父組件的話,系統會過於頻繁進行這種通訊,有可能會有性能的問題。那麼咱們但願能夠有一個相似濾波器的東東,它能夠過濾掉必定時間內的事件。所以咱們定義一個輸入型參數delay。npm

<app-todo-header placeholder="What do you want" delay="400" (textChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" >
</app-todo-header>複製代碼

如今的標籤引用應該是上面這個樣子,但咱們只是策劃了它看起來是什麼樣子,尚未作呢。咱們一塊兒動手看看怎麼作吧。
todo-header.component.html的模板中咱們調整了一些變量名和參數以便讓你們不混淆子組件本身的模板和父組件中引用子組件的模板片斷。編程

//todo-header.component.html
<header class="header">
  <h1>Todos</h1>
  <input class="new-todo" [placeholder]="placeholder" autofocus="" [(ngModel)]="inputValue" (keyup.enter)="enterUp()">
</header>複製代碼

記住子組件的模板是描述子組件本身長成什麼樣子,應該有哪些行爲,這些東西和父組件沒有任何關係。好比todo-header.component.html中的placeholder就是HTML標籤Input中的一個屬性,和父組件沒有關聯,若是咱們不在todo-header.component.ts中聲明@Input() placeholder,那麼子組件就沒有這個屬性,在父組件中也沒法設置這個屬性。父組件中的聲明爲@Input()的屬性纔會成爲子組件對外可見的屬性,咱們徹底能夠把@Input() placeholder聲明爲@Input() hintText,這樣的話在引用header組件時,咱們就須要這樣寫<app-todo-header hintText="What do you want" ...
如今看一下todo-header.component.tsjson

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>標籤中就能夠設置這兩個屬性了。
接下來咱們看到了由@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,<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;
    }
}複製代碼

封裝成獨立模塊

如今咱們的todo目錄下好多文件了,並且咱們觀察到這個功能相對很獨立。這種狀況下咱們彷佛沒有必要將全部的組件都聲明在根模塊AppModule當中,由於相似像子組件沒有被其餘地方用到。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 {}複製代碼

注意一點,咱們沒有引入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。下面就得更改根路由了,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);複製代碼

注意到咱們去掉了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 { }複製代碼

並且此時咱們注意到其實沒有任何一個地方目前還需引用<app-todo></app-todo>了,這就是說咱們能夠安全地把selector: 'app-todo',從Todo組件中的@Component修飾符中刪除了。

更真實的web服務

這裏咱們不想再使用內存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
    }
  ]
}複製代碼

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中刪掉內存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應用

在結束本節前,咱們得給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
<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的模板應該是todos循環內的一個todo的模板。TodoList的HTML模板看起來應該是下面的樣子:

<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>複製代碼

咱們須要肯定有哪些輸入型和輸出型參數

  • 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<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須要一個輸入型參數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<Todo>();
  @Output() onToggleTodo = new EventEmitter<Todo>();

  onRemoveTriggered(todo: Todo) {
    this.onRemoveTodo.emit(todo);
  }
  onToggleTriggered(todo: Todo) {
    this.onToggleTodo.emit(todo);
  }
}複製代碼

上面代碼中有一個新東東,就是在todos()方法前咱們看到有setget兩個訪問修飾符。這個是因爲咱們若是把todos當成一個成員變量給出的話,在設置後若是父組件的todos數組改變了,子組件並不知道這個變化,從而不能更新子組件自己的內容。因此咱們把todos作成了方法,並且經過get和set修飾成屬性方法,也就是說從模板中引用的話能夠寫成{{todos}}。經過標記set todos()@Input咱們能夠監視父組件的數據變化。

如今回過頭來看一下todo.component.html,咱們看到(onRemoveTodo)="removeTodo($event)",這句是爲了處理子組件(TodoList)的輸出型參數(onRemoveTodo),而$event其實就是這個事件反射器攜帶的參數(這裏是todo:Todo)。咱們經過這種機制完成組件間的數據交換。

//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>複製代碼

講到這裏你們可能要問是否是過分設計了,這麼少的功能用得着這麼設計嗎?是的,本案例屬於過分設計,但咱們的目的是展現出更多的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)

<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>複製代碼

固然咱們還須要在todo.routes.ts中增長路由參數到路由數組中

{
    path: 'todo/:filter',
    component: TodoComponent
  }複製代碼

根路由定義也須要改寫一下,由於原來todo不帶參數時,咱們直接重定向到todo模塊便可,但如今有參數的話應該重定向到默認參數是「ALL」的路徑;

{
    path: 'todo',
    redirectTo: 'todo/ALL'
  }複製代碼

如今打開todo.component.ts看看怎麼接收這個參數:

  1. 引入路由對象 import { Router, ActivatedRoute, Params } from '@angular/router';
  2. 在構造中注入ActivatedRouteRouter
    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<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();
    }
  }複製代碼

至此大功告成,咱們來看看效果吧。如今輸入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按鈕的事件處理

<button class="clear-completed" (click)="onClick()">Clear completed</button>複製代碼

Clear Completed在Footer中,因此咱們須要給Footer組件增長一個輸出型參數onClearonClick()事件處理方法

//todo-footer.component.ts
...
  @Output() onClear = new EventEmitter<boolean>();
  onClick(){
    this.onClear.emit(true);
  }
...複製代碼

相似的,ToggleAll位於TodoList中,因此在todo-list.component.html中爲其增長點擊事件

<input class="toggle-all" type="checkbox" (click)="onToggleAllTriggered()">複製代碼

todo-list.component.ts中增長一個輸出型參數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)中添加對應的處理方法。最直覺的作法是循環數組,執行已有的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<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]);
  }複製代碼

如今再去試試效果,應該一切功能正常。固然這個版本其實仍是有問題的,本質上仍是在循環調用toggleTodoremoveTodo,這樣作會致使屢次進行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<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);
  }複製代碼

最後其實Todo的全部子組件其實都沒有用到ngInit,因此沒必要實現NgInit接口,能夠去掉ngInit方法和相關的接口引用。

本節代碼: github.com/wpcfan/awes…

紙書出版了,比網上內容豐富充實了,歡迎你們訂購!
京東連接:item.m.jd.com/product/120…

Angular從零到一

第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)
第六節:Angular 2.0 從0到1 (六)
第七節:Angular 2.0 從0到1 (七)
第八節:Angular 2.0 從0到1 (八)

相關文章
相關標籤/搜索