Category Archives: Angular

Angular Tutorial 7章

コンポーネント、サービスの連携(P4233)

src/app/app.module.ts

import { NgModule, Provider } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { BookService }  from './book.service'; // サービスを登録

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  providers:    [ BookService ], // サービスを登録
  /*
  providers: [
    <Provider> { provide: BookService, useClass: BookService, multi: false }
  ],
  */
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
//import { Component, Inject, OnInit } from '@angular/core';

import { Book } from './book';
import { BookService } from './book.service'; // サービスを登録

@Component({
  selector: 'my-app',
  template: `
    <table class="table">
      <tr>
      <th>ISBNコード</th><th>書名</th><th>価格</th><th>出版社</th>
      </tr>
      <tr *ngFor="let b of books">
        <td>{{b.isbn}}</td>
        <td>{{b.title}}</td>
        <td>{{b.price}}円</td>
        <td>{{b.publisher}}</td>
      </tr>
    </table>
  `,
})
export class AppComponent implements OnInit {
  books: Book[];

  constructor(private bookservice: BookService) {} // BookServiceをインスタンス化
  //constructor(@Inject(BookService)private bookservice: BookService) {}

  ngOnInit() {
    this.books = this.bookservice.getBooks(); // bookserviceのメソッドを実行
  }
}

src/app/book.service.ts

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

import { Book } from './book';

@Injectable() // Ingectableデコレーターを付与するとコンポーネントに対してサービスを引き渡せる
export class BookService {
  getBooks(): Book[] {
    return [
      {
        isbn: '978-4-7741-8411-1',
        title: '改訂新版JavaScript本格入門',
        price: 2980,
        publisher: '技術評論社',
      },
      {
        isbn: '978-4-7980-4853-6',
        title: 'はじめてのAndroidアプリ開発 第2版',
        price: 3200,
        publisher: '秀和システム',
      },
    ];
  }
}

src/app/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publisher: string;
}

依存性注入(P4272)

useClassプロパティとvalueClassプロパティ

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { UseService }  from './use.service';
import { UseComponent }  from './use.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, UseComponent  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <my-use></my-use>
      <my-use></my-use>
      <my-use></my-use>
    </ul>
  `,
})
export class AppComponent { }

src/app/use.component.ts

import { Component } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'my-use',
  providers: [
    { provide: UseService, useClass: UseService } // useComponentが生成される度にuseServiceが生成される
    // { provide: UseService, useValue: new UseService() }, // useComponentが生成
  ],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  current: string;

  constructor(private use: UseService) {
    this.current = use.show();
  }
}

src/app/use.service.ts

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

@Injectable()
export class UseService {
  created: Date;

  constructor() {
    this.created = new Date();
    this.sleep(2000);
  }

  show() {
    return this.created.toLocaleString();
  }

  sleep(delay: number) {
    let tmp1 = new Date();
    while (true) {
      let tmp2 = new Date();
      if (tmp2.getTime() - tmp1.getTime() > delay) {
        return;
      }
    }
  }
}

ngModuleでプロバイダーを生成した場合

ngModuleでプロバイダーを生成した場合、 useClassで、useComponentを3回呼び出しても、serviceオブジェクトは一度しか生成されない。
providerは常にシングルトンのインスタンスを生成する。Componentのprovidersに指定されたproviderはそのコンポーネントの範囲でシングルトンにインスタンスを生成する

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { UseService }  from './use.service';
import { UseComponent }  from './use.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, UseComponent  ],
  providers: [{ provide: UseService, useClass: UseService }], // プロバイダー生成
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

途中省いている

非同期処理をサービスに分離(P8539)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { FormsModule }   from '@angular/forms';
import { JsonpModule }  from "@angular/http"; // JsonpModuleをインポート
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule, JsonpModule ], // JsonpModuleをインポート
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/hatena.service.ts

import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class HatenaService {

  constructor(private jsonp: Jsonp) { }

  requestGet(url: string): Observable<any> {
    let params = new URLSearchParams();
    params.set('url', url);
    params.set('callback', 'JSONP_CALLBACK');

    return this.jsonp.get('http://b.hatena.ne.jp/entry/jsonlite/', { search: params })
      .map(
        response => {
          return response.json() || {};
        }
      )
      .catch(
        error => {
          return Observable.throw(error.statusText);
        }
      );
  }
}

src/app/app.component.ts

import { Component } from '@angular/core';
import { Jsonp, URLSearchParams }  from '@angular/http';
import { HatenaService } from './hatena.service'; // serviceインポート

@Component({
  selector: 'my-app',
  providers: [ HatenaService ],
  template: `
  <form>
    <label for="url">URL:</label>
    <input id="url" name="url" type="url" size="50" [(ngModel)]="url" />
    <button (click)="onclick()">検索</button>
  </form>
  <div>{{count}}件</div>
  <ul>
    <li *ngFor="let comment of comments">{{comment}}</li>
  </ul>
  `,
})
export class AppComponent {
  url = 'http://gihyo.jp/';
  count = 0;
  comments: string[] = [];

  constructor(private hatena:HatenaService) {} // serviceのインポート

  onclick() {
    this.hatena.requestGet(this.url) // serviceで定義したメソッドの実行
      .subscribe(
        data => {
          let result: string[] = [];
          data.bookmarks.forEach(function(value: any) {
            if (value.comment !== '') {
              result.push(value.comment)
            }
          });
          this.comments = result;
          this.count = data.count;
        },
        error => {
          this.count = 0;
          this.comments = [];
          console.log('はてなサービスのアクセスに失敗しました。');
        }
      );
  }
}

Angular Tutorial 6章

コンポーネントを入れ子に配置する(P3036)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { DetailsComponent }  from './details.component'; // DetailsComponentを読み込む

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, DetailsComponent ], // DetailsComponentを読み込む
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component } from '@angular/core';
import { Book } from './book';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  selected: Book;

  books = [
    {
      isbn: '978-4-7741-8411-1',
      title: '改訂新版JavaScript本格入門',
      price: 2980,
      publisher: '技術評論社',
    },
    {
      isbn: '978-4-7980-4853-6',
      title: 'はじめてのAndroidアプリ開発 第2版',
      price: 3200,
      publisher: '秀和システム',
    }
  ];

  onclick(book: Book) {
    this.selected = book;
  }
}

src/app/app.component.html

<div>
  <span *ngFor="let b of books">
    [<a href="#" (click)="onclick(b)">{{b.title}}</a>]
  </span>
</div>
<hr />
<detail-book [item]="selected"></detail-book>

src/app/details.component.ts

import { Component, Input } from '@angular/core'; // Inputモジュール追加
import { Book } from './book';

@Component({
  selector: 'detail-book',
  template: `
    <ul *ngIf="item"> // itemプロパティが存在している時のみ表示
      <li>ISBNコード:{{item.isbn}}</li>
      <li>書名:{{item.title}}</li>
      <li>価格:{{item.price | number}}円</li>
      <li>出版社:{{item.publisher}}</li>
    </ul>
  `,
})
export class DetailsComponent {
  @Input() item: Book;
  //@Input('data') item: Book;
  //@Input('item') item: Book;

 // ゲッター、セッターを使う場合は以下
  //   private _item: Book;
  // 
  //     @Input()
  //     set item(_item: Book) {
  //       this._item = _item;
  //     }
  // 
  //     get item() {
  //         return this._item;
  //     }
}

src/app/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publisher: string;
}

outputデコレーター(P3120)

子コンポーネントで発生したイベントを親コンポーネントに通知できる

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';

import { AppComponent }  from './app.component';
import { EditComponent } from './edit.component'

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, EditComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component } from '@angular/core';
import { Book } from './book';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  selected: Book;

  books: Book[] = [
    {
      isbn: '978-4-7741-8411-1',
      title: '改訂新版JavaScript本格入門',
      price: 2980,
      publisher: '技術評論社',
    },
    {
      isbn: '978-4-7980-4853-6',
      title: 'はじめてのAndroidアプリ開発 第2版',
      price: 3200,
      publisher: '秀和システム',
    }
  ];

  onclick(book: Book) {
    // this.selected = book; // bookではなく、下記のように新しいオブジェクトを生成する。そうしないと、フォームの値を変更するとサブミットしなくても、booksの値が変更されてしまう。
    this.selected = {
      isbn: book.isbn,
      title: book.title,
      price: book.price,
      publisher: book.publisher
    };
  }

  onedited(book: Book) {
    for (let item of this.books) {
      if (item.isbn === book.isbn) {
        item.title = book.title;
        item.price = book.price;
        item.publisher = book.publisher;
      }
    }
    this.selected = null;
  }
}

src/app/app.component.html

<div>
  <span *ngFor="let b of books">
    [<a href="#" (click)="onclick(b)">{{b.title}}</a>]
  </span>
</div>
<hr />
<edit-book [item]="selected" (edited)="onedited($event)"></edit-book> // 編集時のハンドラを設定

// 子コンポーネントにアクセスする場合は、以下のように `edit` コンポーネント参照変数を与えておく
//<edit-book #edit [item]="selected" (edited)="onedited($event)"></edit-book>
//<p>編集中の書籍: {{edit.item?.title}}</p>

src/app/edit.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core'; // input, outputモジュールをインポート
import { Book } from './book';

@Component({
  selector: 'edit-book',
  templateUrl: './edit.component.html',
})
export class EditComponent {
  @Input() item :Book;
  @Output() edited = new EventEmitter<Book>(); // eventの中にbookオブジェクトが入る

  onsubmit() {
    this.edited.emit(this.item);
  }
}

src/app/edit.component.html

<form #myForm="ngForm" (ngSubmit)="onsubmit()" *ngIf="item">
  <div>
    <label for="isbn">ISBNコード:</label><br />
    <span id="isbn">{{item.isbn}}</span>
  </div>
  <div>
    <label for="title">書名:</label><br />
    <input id="title" name="title" size="25"
        type="text" [(ngModel)]="item.title" />
  </div>
  <div>
    <label for="price">価格:</label><br />
    <input id="price" name="price" size="5"
        type="number" [(ngModel)]="item.price" />
  </div>
  <div>
    <label for="publisher">出版社:</label><br />
    <input id="publisher" name="publisher"
        type="text" [(ngModel)]="item.publisher" />
  </div>
  <div>
    <input type="submit" value="編集" />
  </div>
</form>

src/app/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publisher: string;
}

モジュールの分離(P3195)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';

import { CoopModule } from './coop/coop.module' // coopモジュールを読み込む
import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule, CoopModule ], // coopモジュールを読み込む
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

app.componentでは、coopModuleは読み込まなくて良い

src/app/app.component.ts

import { Component } from '@angular/core';
import { Book } from './book';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  selected: Book;

  books: Book[] = [
    {
      isbn: '978-4-7741-8411-1',
      title: '改訂新版JavaScript本格入門',
      price: 2980,
      publisher: '技術評論社',
    },
    {
      isbn: '978-4-7980-4853-6',
      title: 'はじめてのAndroidアプリ開発 第2版',
      price: 3200,
      publisher: '秀和システム',
    }
  ];

  onclick(book: Book) {
    this.selected = {
      isbn: book.isbn,
      title: book.title,
      price: book.price,
      publisher: book.publisher
    };
  }

  onedited(book: Book) {
    for (let item of this.books) {
      if (item.isbn === book.isbn) {
        item.title = book.title;
        item.price = book.price;
        item.publisher = book.publisher;
      }
    }
    this.selected = null;
  }
}

src/app/app.component.html

<div>
  <span *ngFor="let b of books">
    [<a href="#" (click)="onclick(b)">{{b.title}}</a>]
  </span>
</div>
<hr />

// coopModuleのedit-bookを読み込む
<edit-book #edit [item]="selected" (edited)="onedited($event)"></edit-book>
<p>編集中の書籍: {{edit.item?.title}}</p>

src/app/coop/coop.module.ts

import { NgModule }      from '@angular/core';
import { CommonModule } from '@angular/common'; // common module(基本的なディレクティブやパイプなどのモジュール)
import { FormsModule }   from '@angular/forms';

import { EditComponent }  from './edit.component'; // 作成したコンポーネント読み込み

@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ EditComponent ],
  exports:    [ EditComponent ]
})
export class CoopModule { }

src/app/coop/edit.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Book } from '../book';

@Component({
  selector: 'edit-book',
  templateUrl: './edit.component.html',
})
export class EditComponent {
  @Input() item :Book;
  @Output() edited = new EventEmitter<Book>();

  onsubmit() {
    this.edited.emit(this.item);
  }
}

src/app/coop/edit.component.html

<form #myForm="ngForm" (ngSubmit)="onsubmit()" *ngIf="item">
  <div>
    <label for="isbn">ISBNコード:</label><br />
    <span id="isbn">{{item.isbn}}</span>
  </div>
  <div>
    <label for="title">書名:</label><br />
    <input id="title" name="title" size="25"
        type="text" [(ngModel)]="item.title" />
  </div>
  <div>
    <label for="price">価格:</label><br />
    <input id="price" name="price" size="5"
        type="number" [(ngModel)]="item.price" />
  </div>
  <div>
    <label for="publisher">出版社:</label><br />
    <input id="publisher" name="publisher"
        type="text" [(ngModel)]="item.publisher" />
  </div>
  <div>
    <input type="submit" value="編集" />
  </div>
</form>

src/app/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publisher: string;
}

ライフサイクルメソッド(P3290)

src/app/app.component.ts

// import { Component, OnChanges, OnInit, DoCheck,
//   AfterContentInit, AfterContentChecked,
//   AfterViewInit, AfterViewChecked, OnDestroy } from '@angular/core';

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

@Component({
  selector: 'my-app',
  template: `
    <div>
      <label>表示/非表示
        <input type="checkbox" (change)="onchange()" checked />
      </label>
    </div>
    <my-child [time]="current" *ngIf="show"></my-child>
  `,
})
// export class AppComponent implements OnChanges,
//   OnInit, DoCheck, AfterContentInit, AfterContentChecked,
//   AfterViewInit, AfterViewChecked, OnDestroy {
export class AppComponent {
  show = true;
  current = new Date();

  onchange() {
    this.show = !this.show;
    this.current = new Date();
  }

  constructor() {
    console.log('constructor');
   }

  ngOnInit() {
    console.log('ngOnInit');
  }

  ngOnChanges() {
    console.log('ngOnChanges');
  }

  ngDoCheck() {
    console.log('ngDoCheck');
  }

  ngAfterContentInit() {
    console.log('ngAfterContentInit');
  }

  ngAfterContentChecked() {
    console.log('ngAfterContentChecked');
  }

  ngAfterViewInit() {
    console.log('ngAfterViewInit');
  }

  ngAfterViewChecked() {
    console.log('ngAfterViewChecked');
  }

  ngOnDestroy() {
    console.log('ngOnDestroy');
  }
}

src/app/child.component.ts

import { Component, Input, OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges
} from '@angular/core';

@Component({
  selector: 'my-child',
  template: `<div>現在時刻は{{time.toLocaleString()}}</div>`,
})
export class ChildComponent implements OnChanges,
  OnInit, DoCheck, AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked, OnDestroy {
   @Input() time: Date;

   constructor() {
    console.log('[child]constructor');
   }

  ngOnInit() {
    console.log('[child]ngOnInit');
  }

  ngOnChanges() {
    console.log('[child]ngOnChanges');
  }
  /*
  ngOnChanges(changes: SimpleChanges) {
    console.log('[child]ngOnChanges');
    for (let prop in changes) {
      let change = changes[prop];
      console.log(`${prop}:${change.previousValue} => ${change.currentValue}`);
    }
  }
  */

  ngDoCheck() {
    console.log('[child]ngDoCheck');
  }

  ngAfterContentInit() {
    console.log('[child]ngAfterContentInit');
  }

  ngAfterContentChecked() {
    console.log('[child]ngAfterContentChecked');
  }

  ngAfterViewInit() {
    console.log('[child]ngAfterViewInit');
  }

  ngAfterViewChecked() {
    console.log('[child]ngAfterViewChecked');
  }

  ngOnDestroy() {
    console.log('[child]ngOnDestroy');
  }
}

ログ

画面表示時

// 親コンポーネントのチェック
app.component.ts:31 constructor
app.component.ts:35 ngOnInit
app.component.ts:43 ngDoCheck // 状態の変更を検出
app.component.ts:47 ngAfterContentInit // 最初のngDoCheckメソッドの後に一度だけ実行
app.component.ts:51 ngAfterContentChecked // 外部コンテンツの状態をチェックした時

// 子コンポーネントを生成
child.component.ts:16 [child]constructor
child.component.ts:24 [child]ngOnChanges // input経由で入力値が設定された時(親コンポーネントにはない処理)
child.component.ts:20 [child]ngOnInit // 最初のngOnChangesの後に一度だけ実行
child.component.ts:37 [child]ngDoCheck // 状態の変更を検出
child.component.ts:41 [child]ngAfterContentInit // 最初のngDoCheckメソッドの後に一度だけ実行
child.component.ts:45 [child]ngAfterContentChecked // 外部コンテンツの状態をチェックした時

// 子のビュー生成
child.component.ts:49 [child]ngAfterViewInit // ビューを生成した時(一度だけ)
child.component.ts:53 [child]ngAfterViewChecked // ビューが変更された時

// 親のビュー生成
app.component.ts:55 ngAfterViewInit // ビューを生成した時(一度だけ)
app.component.ts:59 ngAfterViewChecked // ビューが変更された時

// 状態の変更を検出して再度、コンテンツの状態を確認して再度ビューを更新
app.component.ts:43 ngDoCheck // 状態の変更を検出
app.component.ts:51 ngAfterContentChecked 外部コンテンツの状態をチェックした時
child.component.ts:37 [child]ngDoCheck // 状態の変更を検出
child.component.ts:45 [child]ngAfterContentChecked 外部コンテンツの状態をチェックした時
child.component.ts:53 [child]ngAfterViewChecked // ビューが変更された時
app.component.ts:59 ngAfterViewChecked // ビューが変更された時

子コンポーネントを削除した時

app.component.ts:43 ngDoCheck // 状態の変更を検出
app.component.ts:51 ngAfterContentChecked // コンポーネントをチェック
child.component.ts:57 [child]ngOnDestroy //  // ngAfterViewCheckedではなくてコンポーネント削除
app.component.ts:59 ngAfterViewChecked

子コンポーネントを再表示した時

// 初回表示と同じ手順で、子コンポーネントが再生成される
app.component.ts:43 ngDoCheck
app.component.ts:51 ngAfterContentChecked
child.component.ts:16 [child]constructor
child.component.ts:24 [child]ngOnChanges
child.component.ts:20 [child]ngOnInit
child.component.ts:37 [child]ngDoCheck
child.component.ts:41 [child]ngAfterContentInit
child.component.ts:45 [child]ngAfterContentChecked
child.component.ts:49 [child]ngAfterViewInit
child.component.ts:53 [child]ngAfterViewChecked
app.component.ts:59 ngAfterViewChecked

ngOnChanges

SimpleChangesオブジェクトは プロパティ名: 変更情報 のハッシュ。

ngOnChanges(changes: SimpleChanges) {
  console.log('[child]ngOnChanges');
  for (let prop in changes) {
    let change = changes[prop];
    console.log(`${prop}:${change.previousValue} => ${change.currentValue}`);
  }
}

ngAfterViewInit(P3361)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';

import { AppComponent }  from './app.component';
import { ChildComponent }  from './child.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, ChildComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component, AfterViewChecked,
  QueryList, ViewChildren } from '@angular/core';

import { ChildComponent } from './child.component';

@Component({
  selector: 'my-app',
  template: `
    <my-child [index]="1"></my-child>
    <my-child [index]="2"></my-child>
    <my-child [index]="3"></my-child>
    <hr />
    完成:{{poems[0]}} {{poems[1]}} {{poems[2]}}
  `,
})
export class AppComponent implements AfterViewChecked {
  // ChiledComponentの読み込み。
  @ViewChildren(ChildComponent) children: QueryList<ChildComponent>;
  poems = ['', '', ''];

  ngAfterViewChecked() {
    console.log('ngAfterViewChecked');
    this.children.forEach((item, index) => {
      if(this.poems[index] !== item.poem) { // item.poemが更新された時のみ入れ替えを行う。この処理を入れておかないと無限ループする。
        // ビューの更新が完了した後に、子ポーネントのプロパティ及びビューを変更するので`setTimeout`を使って非同期処理にする必要がある。(更新処理が次の更新サイクルに持ち込まれるの例外エラーを回避できる)
        setTimeout(() => {
          this.poems[index] = item.poem;
        }, 0);
      }
    });
  }
}

src/app/child.component.ts

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

@Component({
  selector: 'my-child',
  template: `
    <div>
      三行詩{{index}}:<input name="poem" [(ngModel)]="poem" size="20" />
    </div>
  `,
})
export class ChildComponent {
  @Input() index: number;
  poem: string;
}

コンポーネント配下のコンテンツをテンプレートに反映させる(P3416)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { ContentComponent }  from './content.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, ContentComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `<my-content><h2>権兵衛</h2></my-content>`,
})
export class AppComponent {
}

src/app/content.component.ts

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

@Component({
  selector: 'my-content',
  template: `
    <div>こんにちは、<ng-content></ng-content>さん!</div>
  `,
})
export class ContentComponent {
}

結果

<my-content>
 <div>
  "こんにちは、"
   <h2>権兵衛</h2>
   "さん!"
 </div>
</my-content>

複数の要素を指定の領域に配置する(P3440)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { ContentComponent }  from './content.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, ContentComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `
    <my-content>
      <span class="header">あなたもWINGSプロジェクトに参加しませんか?</span>
      <span class="attention">ただいま、メンバー募集中!</span>
      <small>連絡先:webmaster@wings.msn.to</small>
      <p>興味のある方は、WINGSプロジェクト採用担当まで、メールでご連絡ください。</p>
      <small>(担当:山田)</small>
    </my-content>
  `,
})
export class AppComponent {
}

src/app/content.component.ts

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

@Component({
  selector: 'my-content',
  template: `
    <section>
      <header>
        <h3>
          <ng-content select=".header"></ng-content>
        </h3>
      </header>
      <div>
        <h2>
          <ng-content select=".attention"></ng-content>
        </h2>
        <ng-content></ng-content>
      </div>
      <footer>
        <hr />
        <ng-content select="small"></ng-content>
      </footer>
    </section>
  `,
})
export class ContentComponent {
}

外部コンテンツの初期化 / 更新時の処理を実装する

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';

import { AppComponent }  from './app.component';
import { ParentComponent }  from './parent.component';
import { ChildComponent }  from './child.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, ParentComponent, ChildComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `
    <my-parent>
      <my-child></my-child>
    </my-parent>
  `,
})
export class AppComponent { }

src/app/parent.component.ts

import { Component, AfterContentChecked, ContentChild } from '@angular/core';

import { ChildComponent } from './child.component';

@Component({
  selector: 'my-parent',
  template: `
    <ng-content></ng-content> // my-childが表示される
    <hr />
    完成:{{poem}}
  `,
})
export class ParentComponent implements AfterContentChecked {
  @ContentChild(ChildComponent) child: ChildComponent; // childの読み込み(childComponentが複数ある場合は、ContentChildren デコレーターを使う)
  poem = '';

  ngAfterContentChecked() {
    // ビューがまだ確定していない状態なので、setTimeoutメソッドによる非同期処理は不要
    if(this.poem !== this.child.poem) {
      this.poem = this.child.poem;
    }
  }
}

src/app/child.component.ts

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

@Component({
  selector: 'my-child',
  template: `
    <div>
      一行詩:<input name="poem1" [(ngModel)]="poem" size="20" />
    </div>
  `,
})
export class ChildComponent {
   poem: string;
}

コンポーネントスタイルの定義

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `
    <h2>Angularアプリケーションプログラミング</h2>
    <p>こんにちは、{{name}}さん!</p>
  `,
  //styleUrls: ['./app.component.css'],

  styles: [`
    h2 {
      font-size: 150%;
      text-decoration: underline;
      color: #369;
    }

    p {
      background-color: Yellow;
      color: Red;
    }
  `],

/*
  //複数の文字列で指定
  styles: [`
  h2 {
    font-size: 150%;
    text-decoration: underline;
    color: #369;
  }
  `,`
  p {
    background-color: Yellow;
    color: Red;
  }
`]
*/
  //encapsulation: ViewEncapsulation.Native
})
export class AppComponent {
  name = 'Angular';
}

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <base href="/">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
    <p>今日もいい天気ですね!</p>
  </body>
</html>

結果

AppComponent(my-app 要素)にのみ、CSSが適応される

コンポーネントスタイルを定義する方法(P3622〜P3766)

面倒そうだったので飛ばす

アニメーション(P3800)

飛ばす

Angular Tutorial 5章

基本的なフォーム(P2602)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule} from '@angular/forms'  // formsModuleを追加
import { AppComponent }  from './app.component';
import { BookComponent }  from './book.component';
import { UserComponent }  from './user.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule ], // formsModuleを追加
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  templateUrl: './app.component.html',
})
export class AppComponent {
  user = {
    mail: 'hoge@example.com',
    passwd: '',
    name: '名無権兵衛',
    memo: 'メモ!',
    code: ''
  };

  show() {
    console.log('メールアドレス:' + this.user.mail);
    console.log('パスワード:' + this.user.passwd);
    console.log('名前(漢字):' + this.user.name);
    console.log('備考:' + this.user.memo);
  }
}

src/app/app.component.html

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
<div>
  <label for="mail">メールアドレス:</label><br />
  <input id="mail" name="mail" type="email"
    [(ngModel)]="user.mail" #mail="ngModel" required email />
  <span *ngIf="mail.errors?.required">メールアドレスは必須です。</span>

  <!--hasErrorメソッドの場合-->
  <!--<span *ngIf="mail.hasError('required')">メールアドレスは必須です。</span>-->

  <span *ngIf="mail.errors?.email">
    メールアドレスを正しい形式で入力してください。</span>
</div>
<div>
  <label for="passwd">パスワード:</label><br />
  <input id="passwd" name="passwd" type="password"
    [(ngModel)]="user.passwd"
    required minlength="6" #passwd="ngModel" />
  <!--<span *ngIf="passwd.errors?.required">
    パスワードは必須です。</span>-->
  <span *ngIf="passwd.errors?.required && passwd.dirty">
    パスワードは必須です。</span>
   <span *ngIf="passwd.errors?.minlength">
    パスワードは6文字以上で入力してください。</span>
</div>
<div>
  <label for="name">名前(漢字):</label><br />
  <input id="name" name="name" type="text" [(ngModel)]="user.name"
    required minlength="3" maxlength="10" #name="ngModel" />
  <span *ngIf="name.errors?.required">
    名前(漢字)は必須です。</span>

  <!--入力項目の単位でエラーの有無をチェック-->
  <!--<span *ngIf="name.invalid">
    名前(漢字)は必須です。</span>-->

  <span *ngIf="name.errors?.minlength">
    名前(漢字)は3文字以上で入力してください。</span>
  <span *ngIf="name.errors?.maxlength">
    名前(漢字)は10文字以内で入力してください。</span>
</div>
<div>
  <label for="code">コード</label><br />
  <input id="code" type="text" name="code" [(ngModel)]="user.code"
    required pattern="\d{6}" #code="ngModel" />
  <span *ngIf="code.errors?.required && passwd.dirty">コードは必須です。</span>
  <span *ngIf="code.errors?.pattern">数字6桁で入力してください</span>
</div>
<div>
  <label for="memo">備考:</label><br />
  <textarea id="memo" name="memo" rows="5" cols="30"
    [(ngModel)]="user.memo" maxlength="10" #memo="ngModel"></textarea>
  <span *ngIf="memo.errors?.maxlength">
    備考は10文字以内で入力してください。</span>
  <span *ngIf="memo.dirty">memoを変更しましたね。</span>
</div>
<div>
  <!-- <input type="submit" value="送信"
    [disabled]="myForm.invalid" /> -->

  <!--サブミット済みかどうかを判定-->
  <input type="submit" value="送信"
    [disabled]="myForm.invalid || myForm.submitted" />

    <!--pristine/dirtyプロパティを利用したリセットボタン-->
    <!--<input type="reset" value="リセット" [disabled]="myForm.pristine" />-->
    <!--<input type="reset" value="リセット" [disabled]="!myForm.dirty" />-->
</div>
</form>
<pre>{{myForm.value | json}}</pre>

src/app/app.component.css

input.ng-dirty.ng-invalid { background-color: red; }

ラジオボタン(P2763)

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items: Item[] = [
    {id: 1, name: "item1"},
    {id: 2, name: "item2"},
  ];
}

class Item {
  id: number;
  name: string;
}

src/app/app.component.html

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
  <ng-container *ngFor="let item of items; index as i">
    <label>
      <input type="radio" name="animal"
        [(ngModel)]="selected"
        [value]="item.name" [checked]="selected == item.name"
        (change)="show(i)">{{item.name}}
    </label><br />
  </ng-container>
</form>

チェックボックス(P2775)

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  datas: Data[] = [
    {item: {id: 1, name: "item1"}, selected: false},
    {item: {id: 2, name: "item2"}, selected: false}
  ]
}

class Item {
  id: number;
  name: string;
}

class Data {
  item: Item
  selected: boolean;
}

src/app/app.component.html

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
  <ng-container *ngFor="let data of datas; index as i">
    <label>
      <input type="checkbox" name="animal{{i}}"
        [(ngModel)]="datas[i].selected"
        [value]="data.item.name"
        (change)="show()">{{data.item.name}}
    </label><br />
  </ng-container>
</form>

選択ボックス(P2783)

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  selected = 'hamster';
  data = [
    { label: '犬', value: 'dog', disabled: false },
    { label: '猫', value: 'cat', disabled: false },
    { label: 'ハムスター', value: 'hamster', disabled: false },
    { label: '金魚', value: 'fish', disabled: true },
    { label: '亀', value: 'turtle', disabled: false }
  ];
  show() {
    console.log('選択した値' + this.selected);
  }
}

src/app/app.component.html

<form #myForm="ngForm">
  <select name="animal" [(ngModel)]="selected" (change)="show()">

  <!--リストボックスの生成-->
  <!--<select name="animal" size="3"
    [(ngModel)]="selected" (change)="show()" multiple>-->

    <option value="">ペットを選択してください</option>
    <option *ngFor="let item of data"
      [value]="item.value" [disabled]="item.disabled"
      [selected]="item.value === selected">{{item.label}}</option>
  </select>
</form>

選択ボックスをグループ化(P2796)

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  selected = '';
  data = {
    '哺乳類': [
        { label: '犬', value: 'dog', disabled: false },
        { label: '猫', value: 'cat', disabled: false },
        { label: 'ハムスター', value: 'hamster', disabled: false },
      ],
    '魚類': [
        { label: '金魚', value: 'fish', disabled: true },
        { label: '鯉', value: 'carp', disabled: false },
        { label: '熱帯魚', value: 'tropical fish', disabled: false },
      ],
    '爬虫類': [
        { label: '亀', value: 'turtle', disabled: false },
        { label: 'トカゲ', value: 'lizard', disabled: false },
        { label: 'ヘビ', value: 'snake', disabled: false }
      ]
  };

  keys(obj: Object) {
    return Object.keys(obj);
  }

  show() {
    console.log('現在値:' + this.selected);
  }
}

src/app/app.component.html

<form #myForm="ngForm">
  <select name="animal" [(ngModel)]="selected" (change)="show()">
    <option value="">ペットを選択してください</option>
    <optgroup *ngFor="let group of keys(data)" label={{group}}> // 定義したkeysメソッドを実行
      <option *ngFor="let item of data[group]"
        [value]="item.value" [disabled]="item.disabled"
        [selected]="item.value === selected">{{item.label}}
      </option>
    </optgroup>
  </select>
</form>

文字数カウント(P2844)

src/app/app.component.ts

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

@Component({
  selector: "my-app",
  template: `
    <form>
      <textarea cols="70" rows="5" name="tweet"
        [(ngModel)]="tweet" (input)="setcolor()"></textarea>
      <div [ngStyle]="myStyle">{{count}}</div>
    </form>
  `
})
export class AppComponent {
  max = 10;
  tweet= '';
  count = this.max;
  myStyle = { color: '#00f', fontWeight: 'normal' };

  setcolor() {
    this.count = this.max - this.tweet.length;
    if (this.count > 5) {
      this.myStyle = { color: '#00f', fontWeight: 'normal' }
    } else if (this.count > 0) {
      this.myStyle = { color: '#f0f', fontWeight: 'normal'  };
    } else {
      this.myStyle = { color: '#f00', fontWeight: 'bold' };
    }
  }
}

テキストボックスの内容を ; で区切る(P2854)

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

@Component({
  selector: "my-app",
  template: `
    <form>
    <label for="mail">メールアドレス:</label>
    <textarea id="mail" name="mail" type="text"
      [ngModel]="emails.join(';')"
      (ngModelChange)="emails=$event.split(';')">
    </textarea>
    </form>
    <ul>
      <li *ngFor="let email of emails">{{email}}</li>
    </ul>
  `
})
export class AppComponent {
  emails: String[] = [];
}

ファイルアップロード(P2889)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from "@angular/http";

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule, HttpModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.module.ts

import { Component } from '@angular/core';
import { Http, Headers, RequestOptions } from "@angular/http";

@Component({
  selector: "my-app",
  template: `
    <form>
      <input id="upfile" name="upfile"  type="file" #fl
        accept="image/*" (change)="upload(fl.files)" />
    </form>
  `
})
export class AppComponent {
  constructor(private http:Http) {}

  upload(list: any) {
    if (list.length <= 0) { return; }

    let f = list[0];
    let data = new FormData();
    data.append('upfile', f, f.name);

    this.http.post('app/upload.php', data)
      .subscribe(
        data => console.log(data),
        error => console.log(error)
      );
  }
}

モデル駆動型のフォーム(P2906)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule }   from '@angular/forms'; // 追加

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule, ReactiveFormsModule ], // ReactiveFormsModule を追加
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

formControl

入力要素の初期値とバリデーションを管理

formControlGroup

formControlをまとめる

src/app/app.component.ts

import { Component } from "@angular/core";
import { FormGroup, FormControl,
         FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: "my-app",
  templateUrl: './app.component.html' 
})
export class AppComponent {
  // formControleオブジェクト作成
  mail = new FormControl('hoge@example.com', [
    Validators.required,
    Validators.email
  ]);
  passwd = new FormControl('',[
    Validators.required,
    Validators.minLength(6)
  ]);
  name = new FormControl('名無権兵衛',[
    Validators.required,
    Validators.minLength(3),
    Validators.maxLength(10)
  ]);

  // 初期値と状態を指定
  /*
  name = new FormControl(
  {
    value: '名無権兵衛',
    disabled: true
  },
  [
    Validators.required,
    Validators.minLength(3),
    Validators.maxLength(10)
  ]);
  */

  memo  = new FormControl('メモ',[
    Validators.maxLength(10)
  ]);

  // builderを使ってformControlをグループ化
  myForm = this.builder.group({
    mail: this.mail,
    passwd: this.passwd,
    name: this.name,
    memo: this.memo
  });

  constructor(private builder: FormBuilder) { } // FormBuilderをbuilerに注入してる

  show() {
    console.log('メールアドレス:' + this.mail.value);
    console.log('パスワード:' + this.passwd.value);
    console.log('名前(漢字):' + this.name.value);
    console.log('備考:' + this.memo.value);
    console.log('すべて:');
    console.log(this.myForm.value);
  }
}

src/app/app.component.html

<form [formGroup]="myForm" (ngSubmit)="show()">
<div>
  <label for="mail">メールアドレス:</label><br />
  <input id="mail" name="mail" type="email"
    [formControl]="mail" />
  <span *ngIf="mail.errors?.required">メールアドレスは必須です。
  </span>
  <span *ngIf="mail.errors?.email">
    メールアドレスを正しい形式で入力してください。</span>
</div>
<div>
  <label for="passwd">パスワード:</label><br />
  <input id="passwd" name="passwd" type="password"
    [formControl]="passwd" />
  <span *ngIf="passwd.errors?.required">
    パスワードは必須です。</span>
   <span *ngIf="passwd.errors?.minlength">
    パスワードは6文字以上で入力してください。</span>
</div>
<div>
  <label for="name">名前(漢字):</label><br />
  <input id="name" name="name" type="text" [formControl]="name" />
  <span *ngIf="name.errors?.required">
    名前(漢字)は必須です。</span>
  <span *ngIf="name.errors?.minlength">
    名前(漢字)は3文字以上で入力してください。</span>
  <span *ngIf="name.errors?.maxlength">
    名前(漢字)は10文字以内で入力してください。</span>
</div>
<div>
  <label for="memo">備考:</label><br />
  <textarea id="memo" name="memo" rows="5" cols="30"
    [formControl]="memo"></textarea>
  <span *ngIf="memo.errors?.maxlength">
    備考は10文字以内で入力してください。</span>
</div>
<div>
  <input type="submit" value="送信" 
    [disabled]="myForm.invalid" />
</div>
</form>

Angular Tutorial 4章

パイプ

i18nPlural(P2046)

数字を文字列に変換する

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  favs0: string[] = [];
  favs1 = ['yohei','miyamoto'];
  favs2 = ['yohei','miyamoto', 'test'];
  messages = {
    '=0':'「いいね!」されていません',
    '=1':'1人だけ「いいね!」と言ってくれています',
    'other':'#人が「いいね!」と言ってくれています'
  }
}

src/app/app.component.html

<div>
  <ul>
    <li>{{favs0.length | i18nPlural: messages}}</li>
    <li>{{favs1.length | i18nPlural: messages}}</li>
    <li>{{favs2.length | i18nPlural: messages}}</li>
  </ul>
</div>

表示結果

「いいね!」されていません
2人が「いいね!」と言ってくれています
3人が「いいね!」と言ってくれています

i18nSelect(P2075)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  users: User[] = [
    {name: 'taro', sex: 'male'},
    {name: 'hanako', sex: 'female'},
    {name: 'totoro', sex: '???'},
  ]
  messages = {
    'male': '彼',
    'female': '彼女',
    'other': '彼/彼女',
  }
}

class User {
  name: string;
  sex: string;
}

src/app/app.component.html

<div>
  <ul>
    <li *ngFor="let u of users">
      {{u.sex | i18nSelect:messages}}は{{u.name}}です。
    </li>
  </ul>
</div>

出力結果

彼はtaroです。
彼女はhanakoです。
彼/彼女はtotoroです。

ngIfディレクティブ(P2131)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  show = false
}

src/app/app.component.html

// falseの場合、htmlのツリーからも破棄される
<div *ngIf="show">
  show!
</div>

HTMLのツリーからは、破棄せずその上で非表示にしたい場合は以下

displayスタイルプロパティを使う場合

<div [style.display]="show ? 'inline':'none'">
  hello
</div>

クラスバインディングを使う場合

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  none = false;
}

src/app/app.component.css

.none{
  display: none;
}

src/app/app.component.html

<div [class.none]="none">
  hello
</div>

ngifディレクティブ[else] (P2180)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  show = true;
}

src/app/app.component.html

<div *ngIf="show; then trueContent; else falseContent">
  この部分は無視される
</div>

<ng-template #trueContent>
  <p>true</p>
</ng-template>
<ng-template #falseContent>
  <p>false</p>
</ng-template>

ngSwitch(P2202)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  season = '';
}

src/app/app.component.html

<form>
  <select name="season" [(ngModel)]="season">
    <option value="one">one</option>
    <option value="two">two</option>
  </select>
</form>

<div [ngSwitch]="season">
  <span *ngSwitchCase="''">何も選択してないですね。</span>
  <span *ngSwitchCase="'one'">oneを選択しましたね。</span>
  <span *ngSwitchDefault>one以外を選択しましたね。</span>
</div>

ngForとngContainer(P2295)

quickstart/src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  users: User[] = [
    {name: 'yohei', sex: 'male'},
    {name: "miyamoto", sex: 'female'},
  ]
}

src/app/app.component.html

// divで囲む場合
<div *ngFor="let u of users">
  <p>{{u.name}}</p>
  <p>{{u.sex}}</p>
</div>

// ng-containerを使う場合(プログラムの都合でdivが発生しないようにできる)
<ng-container *ngFor="let u of users">
  <p>{{u.name}}</p>
  <p>{{u.sex}}</p>
</ng-container>

出力結果

// divで囲む場合
<div>
 <p>yohei</p>
 <p>male</p>
</div>
<div>
 <p>male</p>
 <p>female</p>
</div>

// ng-containerを使う場合(divで囲まれない)
<p>yohei</p>
<p>male</p>
<p>male</p>
<p>female</p>

ngFor トラッキング(P2316)

トラッキングしない場合

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  users: User[] = [
    {name: 'yohei', gender: 'male'},
    {name: "miyamoto", gender: 'female'},
  ]
  onClick() {
    this.users = [
      {name: 'yohei', gender: 'male'},
      {name: 'miyamoto', gender: 'female'},
      {name: 'yohei', gender: 'male'},
    ]
  }
}

class User {
  name: string;
  gender: string;
}

src/app/app.component.html

<ng-container *ngFor="let u of users">
  <p>{{u.name}}</p>
  <p>{{u.sex}}</p>
</ng-container>
<input type="button" (click)="onClick()" value="更新">

結果

usersの全レコードが更新されてしまう

トラッキングする場合

更新前との差分のみを追加することができる

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  users: User[] = [
    {id: 1, name: 'yohei', gender: 'male'},
    {id: 2, name: "miyamoto", gender: 'female'},
  ]
  onClick() {
    this.users = [
      {id: 1, name: 'yohei', gender: 'male'},
      {id: 2, name: "miyamoto", gender: 'female'},
      {id: 3, name: "tarou", gender: 'female'},
    ]
  }
  // ユニークになるキーを戻す
  trackFn(index: any, user: any){
    return user.id;
  }
}
class User {
  id: number;
  name: string;
  gender: string;
}

src/app/app.component.html

<ng-container *ngFor="let u of users; trackBy: trackFn">
  <p>{{u.name}}</p>
  <p>{{u.gender}}</p>
</ng-container>
<input type="button" (click)="onClick()" value="更新">

sliceでのページング(P2350)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  start = 0;
  len = 3;
  users: User[] = [
    {id: 1, name: 'yohei1', gender: 'male'},
    {id: 2, name: "yohei2", gender: 'female'},
    {id: 3, name: "yohei3", gender: 'female'},
    {id: 4, name: "yohei4", gender: 'female'},
    {id: 5, name: "yohei5", gender: 'female'},
  ]
  pager(page: number) {
    this.start = this.len * page;
  }
}
class User {
  id: number;
  name: string;
  gender: string;
}

src/app/app.component.html

<table class="table">
 <tr>
  <th>ISBNコード</th><th>書名</th><th>価格</th><th>出版社</th>
  </tr>
// ngForにsliceを使う
  <tr *ngFor="let u of users | slice: start: start+len">
    <td>{{u.name}}</td>
    <td>{{u.gender}}</td>
  </tr>
</table>
<ul class="pagination">
  <li><a href="#" (click)="pager(0)">1</a></li>
  <li><a href="#" (click)="pager(1)">2</a></li>
  <li><a href="#" (click)="pager(2)">3</a></li>
</ul>

ngStyle(P2355)

1つのスタイルを付与するだけであれば、スタイルバインディグでいい。
https://qiita.com/YoheiMiyamoto/items/29fef991ddcd61dcd285#%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0p1462

複数設定するのであれば、 ngStyle を使う方が良い

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

@Component({
  selector: 'my-app',
  template: `<div [ngStyle]="style">hello</div>`
})
export class AppComponent  {
  style = {
    backgroundColor: '#f00',
    color: '#fff',
    fontWeight: 'bold',
    margin: '15px',
    padding: '15px'
  };
}

ngClass(P2393)

src/app/app.component.css

.back{
  background-color: red;
}

.fore{
  color: blue;
}

.space{
   padding: 15px;
}

個別に着脱

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
    flag = false;
    styles = {
      back: false,
      fore: false,
      space: false,
    }
}

src/app/app.component.html

<form>
  <input type="button" (click)="styles.back=!styles.back" value="back">
  <input type="button" (click)="styles.fore=!styles.fore" value="fore">
  <input type="button" (click)="styles.space=!styles.space" value="space">
</form>

<div [ngClass]="styles">
  hello
</div>

ngClassをまとめて着脱

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
    flag = false;
}

src/app/app.component.html

<form>
  <input type="button" (click)="flag=!flag" value="ON/OFF">
</form>
// 3つのクラスを同時に着脱
<div [ngClass]="{'back fore space':flag}">
  hello
</div>

ngPlural(P2435)

i18nPluralパイプのディレクティブ版
https://qiita.com/YoheiMiyamoto/items/22f73f5127429a427829#i18npluralp2046
quickstart/src/app/app.component.html

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  favs: string[] = [ '山田理央', '鈴木洋平', '腰掛奈美' ];
}

src/app/app.component.html

// ロジックをパイプと違ってHTMLに書く事になる。メッセージを動的に組み立てたい時はパイプより便利。
<div [ngPlural]="favs.length">
  <ng-template ngPluralCase="=0">[いいね!]されていません。
  </ng-template>
  <ng-template ngPluralCase="=1">1人だけ[いいね!]と言ってくれています。
  </ng-template>
  <ng-template ngPluralCase="other">
    {{favs.length}}人が[いいね!]と言っています。
  </ng-template>
</div>

ngTemplateOutlet(P2506)

テンプレートをインポートする

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  users :User[] = [
    {id: 1, name: 'yohei', gender: 'male'},
    {id: 2, name: 'miyamoto', gender: 'male'}
  ];
  usersIndex = 0;
}

class User {
  id: number;
  name: string;
  gender: string;
}
// 事前に用意したテンプレート(myTemp)
<ng-template #myTemp let-name="name" let-gender="gender">
  <div>{{name}}</div>
  <div>{{gender}}</div>
</ng-template>

// usersIndexをプルダウンからセット
<select name="usersIndex" [(ngModel)]="usersIndex">
  <option *ngFor="let u of users; let i = index" [value]="i">{{u.name}}</option>
</select>

// テンプレートの呼び出し
<ng-container *ngTemplateOutlet="myTemp; context: users[usersIndex]">

ngComponentOutlet

コンポーネントを動的にインポートする

src/app/app.module.ts

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

// コンポーネントを読み込む
import { BookComponent }  from './book.component';
import { UserComponent }  from './user.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, BookComponent, UserComponent ],
  entryComponents: [ BookComponent, UserComponent ], // entryComponentsの設定も必要
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/user.component.ts

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

@Component({
  selector: 'my-event',
  template: `
    <div class="user">UserComponent!</div>
  `,
  styleUrls: ['app/app.component.css']
})
export class UserComponent {
}

src/app/book.component.ts

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

@Component({
  selector: 'my-event',
  template: `
    <div class="book">BookComponent!</div>
  `,
  styleUrls: ['app/app.component.css']
})
export class BookComponent {
}

src/app/app.component.ts

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

// コンポーネントを読み込む
import { BookComponent } from './book.component';
import { UserComponent } from './user.component';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  interval: any;
  comps = [ BookComponent, UserComponent ]; // 配列でコンポーネントを読み込む
  current = 0;
  banner: any = BookComponent;
  ngOnInit() {
    this.interval = setInterval(() => {
      this.current = (this.current + 1) % this.comps.length; // 配列をループさせる
      this.banner = this.comps[this.current]; // bannerオブジェクトにコンポーネントを入れる
    }, 3000);
  }
}

src/app/app.component.html

<ng-container *ngComponentOutlet="banner"></ng-container>

Angular Tutorial 3章

Interpolation(補完) (P1162)

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

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}. Hello {{user?.name}}</h1>`,
})
export class AppComponent  {
  name = 'Angular';
  // user: User = {
  //   id: "1",
  //   name: "yohei"
  // }
}

class User {
  id: string;
  name: string;
}

プロパティバインディング (P1232)

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

@Component({
  selector: 'my-app',
  template: `<img [src]="image" />`
})
export class AppComponent  {
  image = 'http://www.wings.msn.to/image/wings.jpg'
}

インナーHTML (P1265)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `<div [innerHTML]="msg"></div>`
})
export class AppComponent  {
  msg = `<h2>hello</h2><br><input type="button" onclick="alert('OK')" value="クリック" />`
}

inputタグは出力されない

出力結果

<div>
  <h2>hello</h2>
</div>

以下のようにsafe処理を行う必要がある

src/app/app.component.ts

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

// モジュール読み込み
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'my-app',
  // safeMsgの読み込み
  template: `<div [innerHTML]="safeMsg"></div>`
})
export class AppComponent  {
  constructor(private sanitizer: DomSanitizer){
    // safeMsg生成
    this.safeMsg = sanitizer.bypassSecurityTrustHtml(this.msg)
  }
  msg = `<h2>hello</h2><br><input type="button" onclick="alert('OK')" value="クリック" />`;
  safeMsg: SafeHtml;
}

属性バインディング(P1358)

プロパティバインディングが使えない場合は、属性バインディングを使う。極力、プロパティバインディングを使うべき。
以下の例ではプロパティバインディングは使えない。
例えば、imgのimg属性は、imgプロパティを持ち、inputのvalueはvalueプロパティを持っているので
プロパティバインディングを使えるが、tdのrowspan属性は、rowspanプロパティを持っていないため。

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  template: `
    <table border="1">
      <tr>
        // 属性バインディング
        <td [attr.rowspan]="len">結合</td>
        <td>1</td>
      </tr>
      <tr><td>2</td></tr>
      <tr><td>3</td></tr>
    </table>
  `
})
export class AppComponent  {
  len = 3;
}

クラスバインディング(P1414)

元々のline classは打ち消されてしまう。

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

@Component({
  selector: 'my-app',
  template: `<div class="line" [class]="myclass">hello</div>`,
  styles: [`
    .red { color: Red; }
    .line { border: solid 1px #f00;}
  `]
})
export class AppComponent  {
  myclass = "red"
}

クラスバインディング(P1433)

クラスは複数設定することが可能

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

@Component({
  selector: 'my-app',
  template: `<div class="line" [class.red]="red">hello</div>`,
  styles: [`
    .red { color: Red; }
    .line { border: solid 1px #f00;}
  `]
})
export class AppComponent  {
  // trueにするとredクラスが追加される。falseにすると削除される
  red = true;
}

スタイルバインディング(P1462)

<div [style.backgroud-color]="bcolor">test<div>

イベントバインディング(P1598)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  // $eventとしないと、eventをハンドラの中で呼び出せない
  template: `<input type="button" (click)="show($event)" value="現在時刻"/>{{msg}}`,
})
export class AppComponent  {
  msg = "";

  show(e: any){
    this.msg = new Date().toLocaleString();
    console.log(e);
  }
}

イベントのデフォルトをキャンセル(P1657)

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

@Component({
  selector: 'my-app',
  template: `
    <div class="line" [class.red]="red">hello</div>
    <input type="text" (keypress)="mask($event)" value="現在時刻"/>{{msg}}
  `,
})
export class AppComponent  {
  msg = "hello";

  mask(e: any){
    let k = e.which;
    // aを入力した時のみ入力を禁止
    if (k == 97){
      e.preventDefault();
    }
  }
}

バブルのイベントをキャンセル

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  onclick1(e: any){
    console.log('outer')
  }
  onclick2(e: any){
    console.log('inner')
    // 以下でキャンセルを行わないと、innerをクリックした際にouterも反応してしまう。上位階層にイベントがバブリングしていくのを防ぐ

    e.stopPropagation();
  }
}

src/app/app.component.html

<div id="outer" (click)="onclick1()">outer
  <div id="inner" (click)="onclick2($event)">
    inner
  </div>
</div>

src/app/app.component.css

#outer{
  height: 200px;
  width: 350px;
  margin: 50px;
  background-color: red;
}

#inner{
  height: 50px;
  width: 100px;
  margin: 50px;
  background-color: blue;
}

変数の入力値の取得(P1676)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  msg = ''
  show(txt: string){
    this.msg = txt
  }
}

src/app/app.component.html

<input #txt type="text" (input)="show(txt.value)">
<div>{{msg}}</div>

テンプレート参照変数による入力値の取得(P1710)

src/app/app.component.html

//(change)="0" を入れることで、componentファイル不要で変数をバインディングできる。コンポーネントでの定義は不要。
<input #last type="text" (change)="0">
<input #first type="text" (change)="0">
{{last.value}}{{first.value}}

キーイベントのフィルタリング(P1719)

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  msg = ''
  show(s: string){
    this.msg = s;
  }
}

enterkeyを押した時のみハンドラが実行される

src/app/app.component.html

<input #txt type="text" (keyup.enter)="show(txt.value)">
{{msg}}

双方向バインディング(P1763)

プロパティバインディングとイベントバインディングをそれぞれ使う実装

FormsModuleを追加

src/app/app.module.ts

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

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

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  myName = '宮本'
  show(s: string){
    this.myName = s;
  }
}

プロパティバインディングとイベントバインディングをそれぞれ実装

src/app/app.component.html

// [ngModel]="myName" はプロパティバインディング
// (input)="show(txt.value)" はイベントバインディグ
<form>
  <input #txt id="name" type="text" name="name" [ngModel]="myName" (input)="show(txt.value)">
  <div>こんにちは{{myName}}さん</div>
</form>

双方向バインディングを使った実装

以下のようにシンプルに実装できる。イベントバインディングハンドラ show(s: string) を実装する必要もなくなる。データバインディング(プロパティバインディングとイベントバインディング)が双方向に行われる。

<form>
  // 角括弧と括弧で囲む
  <input id="name" type="text" name="name" [(ngModel)]="myName">
  <div>こんにちは{{myName}}さん</div>
</form>

データーバインディング時の入力値の加工(P1826)

ハンドラの定義を行わない実装

<form>
  <input id="name" type="text" name="name" [ngModel]="myName" (ngModelChange)="myName=$event.toUpperCase()"> // 入力値を大文字に変換
  <div>こんにちは{{myName}}さん</div>
</form>

ハンドラ定義を行う実装

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  {
  myName = '宮本'
  // イベントハンドラを実装
  upper(s: string){
    return s.toUpperCase();
  }
}

src/app/app.component.html

<form>
  <input id="name" type="text" name="name" [ngModel]="myName" (ngModelChange)="myName=upper($event)"> // イベントハンドラを実装
  <div>こんにちは{{myName}}さん</div>
</form>

Angular Tutorial 2章

モジュール(P860)

Angularモジュール

以下のようにNgModuleで装飾されたもの。今回の例で言うと AppModule がそれにあたる。

app.module.ts

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Javascriptモジュール

import文 import {name, ...} from module のfromで取得している各ファイルのこと。今回の例でいうと、 app.component.ts などがそれにあたる。

app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Javascriptモジュールファイルの中身は以下。外部で使う場合は 以下のようにexport をつける

app.component.ts

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent  { name = 'Angular'; }

コンポーネント(P875)

メインコンポーネントをカスタマイズしてみた。

app.component.ts

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

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}{{user.name}}</h1>`,
})
export class AppComponent  {
  name = 'Angular';
  user: User = {
    id: "1",
    name: "yohei"
  }
}

class User {
  id: string;
  name: string;
}

パッケージインストール(P974)

パッケージjsonからインストール

npm install
npm install --production

パッケージJSONにないものをインストール。パッケージJSONにも追加する

npm install {package名} --save
npm install {package名} --save-dev

angular IE対応

エラー

IEで以下のエラーが発生。画面が表示されない。

オブジェクトは ‘assign’ プロパティまたはメソッドをサポートしていません。

修正

以下のコメントアウトを外す必要がある

bukatsu_report/angular-app/src/polyfills.ts

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

参考

https://stackoverflow.com/questions/45630084/object-doesnt-support-property-or-method-assign-ie11-while-using-angular4

angular4 package.json

{
  "name": "angular-app",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular-devkit/schematics": "0.0.34",
    "@angular/animations": "^4.4.6",
    "@angular/cdk": "^2.0.0-beta.12",
    "@angular/common": "^4.4.6",
    "@angular/compiler": "^4.2.4",
    "@angular/core": "^4.4.6",
    "@angular/forms": "^4.2.4",
    "@angular/http": "^4.2.4",
    "@angular/material": "^2.0.0-beta.12",
    "@angular/platform-browser": "^4.2.4",
    "@angular/platform-browser-dynamic": "^4.2.4",
    "@angular/router": "^4.2.4",
    "@ngui/sortable": "^0.4.1",
    "angular4-drag-drop": "^1.1.2",
    "core-js": "^2.4.1",
    "hammerjs": "^2.0.8",
    "material-design-icons": "^3.0.1",
    "ng2-dnd": "^4.2.0",
    "rxjs": "^5.4.2",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@angular/cli": "1.4.9",
    "@angular/compiler-cli": "^4.2.4",
    "@angular/language-service": "^4.2.4",
    "@types/jasmine": "~2.5.53",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "~3.2.0",
    "jasmine-core": "~2.6.2",
    "jasmine-spec-reporter": "~4.1.0",
    "karma": "~1.7.0",
    "karma-chrome-launcher": "~2.1.1",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~3.2.0",
    "tslint": "~5.7.0",
    "typescript": "~2.3.3"
  }
}

custom validator

import {Component} from '@angular/core';
import { FormGroup, FormControl,
         FormBuilder, Validators, FormArray, ValidatorFn, AbstractControl } from '@angular/forms';
@Component({
  selector: 'my-app',
  template: `
        <form [formGroup]="myForm" (ngSubmit)="show()">
      <input id="email" name="email" type="email" [formControl]="email" />
      <span *ngIf="email.errors?.required">メールアドレスは必須です。</span>
      <p *ngIf="email.errors?.emailDomain">Email must be on the codecraft.tv domain</p>
    </form>
        `
})
export class AppComponent  {
    email = new FormControl('', [
    Validators.required,
    Validators.pattern("[^ @]*@[^ @]*"),
    emailDomainValidator
  ]);
  myForm = this.builder.group({
    email: this.email,
  });
  constructor(private builder: FormBuilder) { }
}

export function emailDomainValidator(control: FormControl) { 
  let email = control.value; 
  if (email && email.indexOf("@") != -1) { 
    let [_, domain] = email.split("@"); 
    if (domain !== "com") { 
      return {
        emailDomain: {
          parsedDomain: domain
        }
      }
    }
  }
  return null; 
}

angular ngFor form

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
  <ng-container *ngFor="let user of users; let i = index" type="text">
    <input id="name" name="name{{i}}" [(ngModel)]="user.name" #name="ngModel" required>
    <span *ngIf="name.errors?.required">名前は必須です。</span>
  </ng-container>
</form>