LoginSignup
19
15

More than 3 years have passed since last update.

Angularで参照に頼らずに配列やオブジェクトの変更を検知したい! [IterableDiffers]

Last updated at Posted at 2019-03-28

IterableDiffers? KeyValueDiffers?

なんだそれは…という人向けの記事です。

配列やオブジェクトの変更を検知するには

あるコンポーネントのInput()がオブジェクトや配列だった時に、その変更をトリガーにして処理を走らせたい場合、どうしますか?例えばこんな感じ。

AppComponent.ts
export class AppComponent  {
  novels = [
    { author: 'Emily', publisher:'BananaBooks'},
    { author: 'Bob', publisher:'OrangeNews'}
  ];

  addNovel(){
    this.novels.push({ author: 'Lyra', publisher: 'GrapePaper'});
    console.log('addNovel')
  }
}

AppComponentでは、小説本の情報をまとめ、novelsとしてBookComponentに渡します。ボタンを押すと小説を追加することができます。BooksComponentでは受け取った書籍情報にコードを追加した物を表示します。ですが、これではAppComponent.tsでAddNovel()が呼ばれてもnovelsの参照が変わらないため、テンプレートが更新されません。

BooksComponent.ts
export interface BookInfo{
  author:string;
  publisher:string;
}

interface BookDetail extends BookInfo{
  code: number;
}

export class BooksComponent implements OnInit {
  @Input() novels: BookInfo[];
  _novels: BookDetail[];

  ngOnInit() {
    this.init();
  }

  init(){
    this._novels = this.fetchCodes(this.novels);
  }

  fetchCodes(books: BookInfo[]): BookDetail[]{
    // it should be service in real case
    return books.map(book => {
      const randomNum = Math.floor( Math.random() * 101 );
      return Object.assign({code: randomNum}, book);
    });
  }
}
BooksComponent.html
<section>
  <h1>novel</h1>
  <div class="novel" *ngFor="let novel of _novels">
    <div class="author">author: {{novel.author}}</div>
    <div class="publisher">publisher: {{novel.publisher}}</div>
    <div class="code">code:{{novel.code}}</div>
  </div>
</section>

よくみる手法

よく見るのが、参照の変更をトリガーにするという手法です。例えばこんな感じ。

BooksComponent.ts
export class BooksComponent implements OnChanges {
  @Input() novels: BookInfo[];
  _novels: BookDetail[];

  ngOnChanges(sc:SimpleChanges) {
    const novelChange = sc['novels'];
    this.init();
  }

  init(){
    this._novels = this.fetchCodes(this.novels);
  }
}

こうすると、AppComponent側でnovelsの参照を新しくしてあげればngOnChangesが走るので無事init処理を走らせることができます。逆に言えば、BooksComponentを使う側はpushした後に参照を新しくする処理をしなければならないということです。

Appcomponent.ts
addNovel(){
    this.novels.push({ author: 'Lyra', publisher: 'GrapePaper'});
    this.novels = this.novels.concat([]);
}

ちょっとした手間ですが、使う側がみんなその処理をしなければいけないと考えるとそこそこ不便です。できれば使う側は追加の処理をするだけで済むようにしたいですよね。そこで登場するのがIterableDiffersです。

IterableDiffersを使う手法

IterableDiffersはその名の通り、iterableの変更を検知します。調べたら英語の記事で「おそらく最も知名度のないAPI」と言われていただけあって日本語の記事がほとんどヒットしません。でもこれがかなり便利なんです。とにかく使ってみましょう。
あとで比較してわかりやすいように、novelsと同じBookInfo[]のcomicsを追加します。そして、変更を検知してくれるIterableDifferも準備します。

BooksComponent.ts
export class BooksComponent implements OnInit {
  @Input() novels: BookInfo[];
  @Input() comics: BookInfo[];
  _novels: BookDetail[];
  _comics: BookDetail[] = [];
  comicDiffer: IterableDiffer<BookInfo>;

そして、そのIterableDifferを生成するためのIterableDiffersをDIします。

BooksComponent.ts

  constructor(private iterable: IterableDiffers) { }

次に、初期化処理でIterableDifferを生成します。今回は例をわかりやすくするため、参照が〜というのは一旦置いておき、最初のBooksComponentにならいngOnInit()で初期化していきます。

BooksComponent.ts

  ngOnInit() {
    this.comicDiffer = this.iterable.find(this.comics).create();
  }

IterableDiffersはAngularが検知しない変更を検知するためのライフサイクルフックであるngDoCheck()で呼び出します。今回は本を追加するという処理のみを行うため、追加された項目に対してオペレーションができるforEachAddedItemを使います。

BooksComponent.ts
  ngDoCheck(){
    const comicChange = this.comicDiffer.diff(this.comics);
    if(comicChange){
      comicChange.forEachAddedItem(record => {
        this._comics = this._comics.concat(this.fetchCodes([record.item]));
      });
    }
  }

これで無事、BooksComponentを使う側は普通にcomicsプロパティに項目を追加するだけで使えるようになりました。

AppComponent.ts
  addComic(){
    this.comics.push({ author: 'Haru', publisher: 'MaronInc'});
  }

これまでの内容をまとめて簡単なデモを作成しました。
StackBlitz

一瞬ハマったこと

ngDoCheckngOnInitngOnChangesの後に実行されるので、初期化時に入っている情報もforEachAddedItemに入ります。なので、ngDoCheck手前でforEachAddedItemと同じ処理を書いてしまうと二重に実行されてしまいます。

当たり前だけどうっかり見落としそうなこと

IterableDifferはあくまでIterableの変更を検知するものなので、今回のように配列の中身がオブジェクトの場合、配列の要素の中の変更までは検知してくれません。今回のケースで中をチェックしたい場合は、keyValueの変更を検知してくれるKeyValueDiffersを使うと良いでしょう。基本的な使い方は同じなのでぜひ使ってみてください。

19
15
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
15