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)

飛ばす