Angular6 コンポーネントやサービスでのデータのやりとり

Angular6でいくつかプロジェクトを作ったので、 コンポーネントやサービスでのデータのやりとりについて、知見をまとめます。

現状の個人的なベターな書き方ですが、オレオレな部分があるかもしれないです。

目次

  1. 親子関係のコンポーネントでのデータのやりとり
  2. サービスを利用したデータのやりとり

Angularのバージョン

  • Angular: 6.1.7
  • rxjs: 6.3.2
  • typescript: 2.7.2

  • 新規プロジェクトの作成

$ ng new angular-data-example

1. 親子関係のコンポーネント

コンポーネントの作成

コンポーネントbooksにて本の一覧を表示し、選択したbookに対して 子コンポーネントbooks-detailで編集を行うことを考える。

BooksDetailComponentBooksComponent でしか利用しないのであれば、 BooksComponent のフォルダ内に含める。

$ ng g component books
$ ng g component books/books-detail

それぞれのコンポーネント<app-books></app-books>, <app-books-detail></app-books-detail> のように利用できる。

Inputの部分

BooksComponentからBooksDetailComponentに情報を渡す。

  • 基本的には[book]="selectedBook"だけでOK
  • オブジェクトや配列を渡す際は参照を渡すことになるので注意が必要
    • 下記の例では、book.copyで自分自身をディープコピーするメソッドを作成した。

(選択部分 books.component.html)

<div *ngFor="let book of books">
  <button type="button" (click)="selectBook(book)">選択</button>
</div>

books.component.ts

selectBook(book: Book): void {
  // .copy() はディープコピーの自作のメソッド
  this.selectedBook = book.copy();
}

books.component.html

<app-books-detail [book]="selectedBook" (edited)="onEdited($event)"></app-books-detail>

books-detail.component.ts

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

@Input() book: Book;

Outputの部分

命名に関しては、

などに統一すると良いと思う。

(編集部分 books-detail.component.html)

<div *ngIf="book != null">
  <form>
    <input type="text" name="title" id="title" [(ngModel)]="book.title">
    <input type="text" name="content" id="content" [(ngModel)]="book.content">
    <button type="submit" (click)="edit()">変更</button>
  </form>
</div>

books-detail.component.ts

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

@Output() edited = new EventEmitter<Book>();
// ...
edit(): void {
  // .copy() はディープコピーの自作のメソッド
  this.edited.emit(this.book.copy());
}

books.component.html

<app-books-detail [book]="selectedBook" (edited)="onEdited($event)"></app-books-detail>

books.component.ts

onEdited(book: Book): void {
  // ...
}

注意点

簡単な状況ならこの方法で十分だが、複雑なコンポーネントの関係では利用できない。 例えば、孫コンポーネントを利用する程度であっても、上記のことを二回繰り返さなければならず、 見通しの悪いコードになった経験がある。

完全に兄弟関係にあるコンポーネント間のみで利用することが分かっている場合のみ利用するのが良いと思う。

2. サービスを利用したデータのやりとり

特定のデータitemsを複数のコンポーネントで利用することを考える。 コンポーネント間は複雑な関係になっていて、Input, Outputでは難しいとする。

itemsはサービスで一元管理し、変更した内容を都度各コンポーネントに送信・取得できるようにする。

サービスの作成

$ ng g service items

サービスで利用するクラス reactive-data.ts

サービス内部で利用する処理の共通部分を予め自作した。 もっと良い方法もあるかもしれない。

export class ReactiveData<T> {

  private cache: T;
  private subject = new Subject<T>();
  private source = this.subject.asObservable();

  constructor(initialValue: T, subscriptionMethod: () => Observable<T> = null) {
    this.cache = initialValue;

    if (subscriptionMethod != null) {
      subscriptionMethod().subscribe(
        data => this.setData(data)
      );
    }
  }

  public getData(): Observable<T> {
    return this.source.pipe(startWith(this.cache));
  }

  public snapshot(): T {
    return this.cache;
  }

  public setData(data: T): void {
    this.subject.next(data);
    this.cache = data;
  }

}

基本的な例

サービス items.service.ts

// 利用頻度の高いrxjsのオペレーター
// rxjs5以前ではインポートの仕方が異なるので注意
import { Observable, Subject, of, merge, zip } from 'rxjs';
import { map, tap, filter, first, flatMap, startWith } from 'rxjs/operators';

import { ReactiveData } from './reactive-data';
// ...

// 引数は共有したいデータの初期値
private items: ReactiveData<Items> = new ReactiveData<Items>(null);
// ...
public getItemsReactiveData() {
  return this.items;
}

サービスを利用するコンポーネント some-component.ts

ItemsServiceの使い方の例。 作成したいコンポーネントがいくつかあっても、同様に記述すれば良い。

import { ItemsService } from '../items.service';
import { ReactiveData } from '../reactive-data.service';
// ...

items: Items;
itemsReactiveData: ReactiveData<Items>;

constructor(
  private modalsService: ModalsService,
) {
  this.itemsReactiveData = this.itemsService.getItemsReactiveData();
}

ngOnInit() {
  // サービスのitemsの初期値(または呼び出し前に変更があった場合、最後の値)を取得し、
  // その後サービスのitemsの変更があればそれを受け取る
  this.itemsReactiveData.getData().subscribe(
    items => {
      this.items = items;
    }
  );
}

someMethod(items: Items): void {
  // サービスのitemsを更新したい時に使うメソッド
  this.itemsReactiveData.setData(items);
}

otherMethod(): Items {
  // 単にサービスのitemsの現在の値を取得したい場合のメソッド
  return this.itemsReactiveData.snapshot();
}

その他の例

ReactiveData自体が何かのデータをsubscribeする必要がある場合もある。 任意の第二引数にsubscribeの方法を取れるようにした。

() => Observable<T>型として渡しているので、 ReactiveDataインスタンスを作成した時にsubscribeを始める。

例1. APIの内容を取得する

let subscriptionItems: () => Observable<Items> = () => {
  return this.someApiService.getItems().pipe(
    map(data => new Items(data))
  );
}
// ...
let items = new ReactiveData<Items>(null, subscriptionItems);

例2. 現在のURLを取得する

// router: Router
let subscriptonPage: () => Observable<string> = () => {
  return this.router.events.pipe(
    filter(event => event instanceof NavigationEnd)
  );
}
// ...
let page = new ReactiveData<string>(null, subscriptonPage));

などなど

参考