前集回顾

上一章里我们讲了如何在angular2下开发一个component(还没做的赶紧去学吧)。我们使用了Unidirectional Data Flow模式书写component,并引入了Immutable思想,这些以前只在React里见到的设计,现在angular2里也有体现,并且在本章中会着重讲解多components的协作。

本章源码:multicomponents

本章使用angular2版本为:2.4.5webpack版本为: 2.2.0

先来看看我们将要完成的效果图:

图片描述

需求分析

(注意动画部分),由上一章的一个component,变成了一个输入component、 一个遍历显示component、 一个总结component。画一个组件树的示意图如下:

图片描述

分析第一部分

  1. 我们将其命名为InputItem

  2. 它由一个input[type="text"]和一个button组成

  3. 当点击button时,需要向上冒泡事件,并组合一个新的CheckableItem随事件发送出去

  4. 清空input[type="text"]

  5. 第3步操作,也可以通过键盘敲击"回车键"完成操作

分析第二个遍历显示部分

参考上一章
关于*ngFor

分析第三个总结部分

  1. 我们将其命名为Counter

  2. 它由一个span组成,显示总结信息

  3. 它接受一个items参数,用来生成总结信息

  4. 总结信息为:显示当前还有多少个isChecked === falseitem

设计use case

还是老套路,先来设计这些新的components的使用场景(这种方式,我们称之为"BDD",不了解的朋友参考以BDD手写依赖注入

重构ts/app.ts

import {Component} from '@angular/core';

import {Item} from './CheckableItem';

@Component({
    selector: 'my-app',
    template: `
    <h1>My First Angular 2 App</h1>
    <!--
        在template里,增加input-item和counter的使用
        input-item里,捕获onItemAdded事件,传递给addItem方法
    -->
    <input-item (onItemAdded)="addItem($event)"></input-item>
    <!--
        使用*ngFor遍历items变量。详情:
        https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ngFor
    -->
    <checkable-item *ngFor="let itemInfo of items; let i = index" [item]="itemInfo" (onItemClicked)="toggle($event, i)">
    </checkable-item>
    <!--
        counter里,传入items
    -->
    <counter [items]="items"></counter>
    `
})
export class AppComponent {
    //声明items为成员变量
    items: Item[] = [];

    //当捕获到onItemAdded事件时,调用该方法,添加新item到items里
    //注:根据Immutable策略,生成新的items
    addItem(item: Item) {
        this.items = [...this.items, item];
    }

    //点击checkable-item时,置反其isChecked属性
    //注:根据Immutable策略,生成新的items
    toggle(item: Item, index: number) {
        this.items = [
            ...this.items.slice(0, index),
            { isChecked: !item.isChecked, txt: item.txt },
            ...this.items.slice(index + 1)
        ];
    }
}

实现InputItem

touch ts/InputItem.ts

向刚创建的ts/InputItem.ts中,添加如下内容:

import {Component, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core';

@Component({
    //这里仍然使用OnPush策略
    changeDetection: ChangeDetectionStrategy.OnPush,
    selector: 'input-item',
    //template里包含一个input[type="text"]和button
    //外面又一个form标签是因为需求中希望回车键也可以触发操作
    template: `
    <form (ngSubmit)="onSubmit()">
        <input type="text" [(ngModel)]="text" name="todo">
        <button type="submit">Add Item</button>
    </form>
    `
})
export class InputItem {
    //双向绑定到input[type="text"]
    text: string;
    //向外部冒泡的事件
    @Output() onItemAdded = new EventEmitter();

    //无论点击button、还是敲击回车键,都处罚添加事件
    //组装一个新的item对象,
    //清空text
    onSubmit() {
        this.onItemAdded.emit({
            isChecked: false,
            txt: this.text
        });
        this.text = '';
    }
}

实现Counter

touch ts/Counter.ts

向刚创建的ts/Counter.ts中,添加如下内容:

import {Component, OnChanges, SimpleChange, Input, ChangeDetectionStrategy} from '@angular/core';

import {Item} from './CheckableItem';

@Component({
    //这里仍然使用OnPush策略
    changeDetection: ChangeDetectionStrategy.OnPush,
    selector: 'counter',
    //template包含一个span
    template: `
    <span>
        We have {{ length }} item{{ postFix }}
    </span>
    `
})
export class Counter implements OnChanges {
    //接受items参数
    @Input() items: Item[];

    postFix: string;
    length: number;

    //每次当参数items的reference发生变化时,触发该方法
    //获取新的length、postFix,重绘组件
    //这里和React中的componentWillUpdate很相似
    ngOnChanges(changes: { [key: string]: SimpleChange }): any {
        let newItems: Item[] = changes['items'].currentValue;
        this.length = newItems.reduce((p, item) => p + (item.isChecked ? 0 : 1), 0);
        this.postFix = this.length > 1 ? 's' : '';
    }
}

修改CheckableItem

import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core';

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    selector: 'checkable-item',
    styles: [`
        .deleted{
            text-decoration: line-through;
        }
    `],
    template: `
    <div>
        <input type="checkbox" [checked]="item.isChecked" (change)="clickItem($event)">
        <label [class.deleted]="item.isChecked">{{ item.txt }}</label>
    </div>
    `
})
export class CheckableItem {
    @Input() item: Item;
    @Output() onItemClicked = new EventEmitter();

    clickItem(e: MouseEvent) {
        e.preventDefault();
        this.onItemClicked.emit(this.item);
    }
}


export interface ToggleItemHandler {
    (item: Item): void;
}

export interface Item {
    isChecked?: boolean;
    txt?: string;
}

组件树的整体编写思路就是Unidirectional Data Flow,所以数据的变更都是Immutable的。如果之前写过React,那对于这种书写方式一定无比熟悉。每次数据的变更,无论是InputItem还是CheckableItem,都将变化冒泡到AppComponent,然后由AppComponent再向下逐级推送各组件是否重绘。

引入声明

打开index.ts,增加新模块声明引入

import 'core-js/es6';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { AppComponent }  from './app';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import {CheckableItem} from './CheckableItem';
import {InputItem} from './InputItem';
import {Counter} from './Counter';

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, CheckableItem, InputItem, Counter ],
  bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

OK,代码写到这里基本就结束了,看看效果吧

npm start

你又看到了伟大的效果:

图片描述

下回预告:使用service


leftstick
27.3k 声望1.5k 粉丝

沙滩一卧两年半,今日浪打我翻身