【Angular&rxjs】rxjs#fromEventとViewChildでコンポーネント間のデータ受け渡しを考えてみる


2019/12/28

執筆中時点で、2020年も残すところあと3日になりました。

今年のやり残しが満載の中のタコ野郎にございますが、この記事を読んで頂いている年中無休なエンジニア様に感謝いたします。

さて今回は、
Angularにおけるコンポーネント間のデータ受け渡しのテクニックで、普段はやらないようなパターンをちょっとだけ解説します。

ネットで
Angular コンポーネント間 データ受け渡しなどのキーワード検索で主に取り上げられている方法は、

            
            1. 親子関係のあるコンポーネント間で、
    @Input/@Outputデコレーターを用いて
    EventEmitter経由でデータをやり取りする方法

2. 親子関係のないコンポーネント間で、
    サービスを利用して、
    データを共有・監視する方法
        
の主に2つに区分できるようです。

これらのやり方は通常のAngularコンポーネントでデータ受け渡しをする際の正攻法になります。

例えば
構造ディレクティブで生じたコンポーネント(もどき)や、コンポーネントの中でDOM要素インスタンスとして生成した後で、動的コンポーネントとのデータ受け渡しはどうすればいいの…?という素朴な疑問に応えるような記事の内容になっています。

親子関係のような従属関係を構築させようというより、頭を身体を切り離した上で神経をつなげてみましょう、というお話です。

なんとも分かりにくい表現ですが、上記項目1と項目2の中間なやり方…のような感じでしょうか。

以下、やりながら実例を挙げて解説していきます。


動作環境

現在の手元にあるPCのコンパイル環境です。

            
            $ ng --version
     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 8.1.3
Node: 10.16.3
OS: linux x64
Angular: 8.1.3
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.801.3
@angular-devkit/build-angular     0.801.3
@angular-devkit/build-optimizer   0.801.3
@angular-devkit/build-webpack     0.801.3
@angular-devkit/core              8.1.3
@angular-devkit/schematics        8.1.3
@ngtools/webpack                  8.1.3
@schematics/angular               8.1.3
@schematics/update                0.801.3
rxjs                              6.4.0
typescript                        3.4.5
webpack                           4.35.2
        
まだ本ブログはAngular8の技術を追いかけておりますが、直近ではv9.0.0のrc版が着々とプレリリースされているようです。

既に最新版のv12などでメジャーアップデートで採用されている
Ivyというレンダリングエンジンに変わっていますが、今回解説するコードの実装がガラッと変わることもありませんのでひとまず安心です。


プロジェクト構造

まず適当なフォルダにangularのプロジェクトをng newして、新規のプロジェクトを作成しましょう。

今回追加で必要なリソースの部分のみに絞らさせていただくと以下のようになります。

            
            tree -I node_modules -L 5
.
├── README.md
├── angular.json
├── browserslist
├── e2e
├── karma.conf.js
├── package.json
├── src
│   ├── app
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── components
│   │   │   ├── my-lazyloaded-compo
│   │   │   │   ├── my-lazyloaded-compo.component.scss
│   │   │   │   └── my-lazyloaded-compo.component.ts
│   │   │   └── my-happy-compo
│   │   │       ├── my-happy-compo.component.scss
│   │   │       └── my-happy-compo.component.ts
│   │   └── factories
│   │       └── lazyloading-compo-factory.service.ts
│   ├── assets
│   ├── environments
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
├── tslint.json
└── yarn.lock
        

上はプロジェクトを新規作成してから、2つのコンポーネント(
my-lazyloaded-compomy-happy-compo)と1つのファクトリーlazyloading-compo-factory(中身はサービス)を以下のようなコマンドで追加しています。

            
            $ yarn ng g c components/myLazyloadedCompo      # 任意の位置で生成可能な動的コンポーネント
$ yarn ng g c components/myHappyCompo           # (通常の)静的コンポーネント
$ yarn ng g s factories/lazyloadingCompoFactory # 動的コンポーネントを生成するファクトリーサービス
        


実装

my-happy-compo

はじめに、以下のような静的なコンポーネントを作成しましょう。まだLazyloadingCompoFactoryの中身は未実装ですが、先出しでメソッドを呼び出しておりますが今は捨て置きください。

            
            import { Component, OnInit, ViewChild, ViewContainerRef, ElementRef } from '@angular/core';
import { LazyloadingCompoFactory } from '../../factories/lazyloading-compo-factory.service.ts';

@Component({
    selector: 'app-my-happy-compo',
    template: `
    <div>
        <p #message_from_children>You're now selecting...:</p>
    </div>
    <ng-container #dynamic></ng-container>
    `,
    styleUrls: ['./my-happy-compo.component.scss']
})
export class MyHappyCompoComponent implements OnInit {

    @ViewChild('dynamic', {
        read: ViewContainerRef,
        static: true
    }) vc: ViewContainerRef;

    @ViewChild('message_from_children', { static: true }) message: ElementRef;

    constructor(
        private lcf: LazyloadingCompoFactory
    ) { }

    ngOnInit() {
        this.lcf.setRootViewContainerRef(this.vc);
        for (let i = 0 ; i < 100 ; i++) {
            this.lcf.addDynamicComponent({name: 'Compo' + i, id: i}, this.message);
        }
    }
}
        
さてここでは、さしずめこの静的なコンポーネントを、親コンポーネントならぬ頭コンポーネントと呼ぶことにします。

まずHTMLテンプレートに着目すると、
#dynamic#message_from_childrenの2つのテンプレート参照で、DOM要素を参照しています。

ポイントは、このDOM要素は
@ViewChildデコレーターでコンポーネントのクラスメンバとして定義できます。

1つ目の
#dynamicでテンプレート参照したDOM要素には、<ng-container>の位置でLazyloadingCompoFactoryが動的にコンポーネントを生成・追加するためのものです。

2つ目の
#message_from_childrenが、今回コンポーネント間で受け渡しに用いるための変数です。

これは特に、コンポーネントのテンプレート内のDOM要素である必要はなく、この頭コンポーネントのクラスメンバであれば、生成された動的コンポーネントへの変数参照を、ファクトリーを通じてばら撒くことができます。

今回の例では、頭コンポーネントの
InnerTextの表示が書き換わっているのを確認するだけのために、データ参照を動的コンポーネントへ渡しておりますが、色々と応用が効きそうなテクニックです。

今回は動的コンポーネントを100個も生成しております。

お手元のPCへの負荷が大きかもしれませんので、ほどほどな数に調整ください。


my-lazyloaded-compo

つぎに、動的コンポーネントの中身を作り込みます。

これは
(静的な)子コンポーネントの動的コンポーネント版なのですが、前の頭コンポーネントと比較しまして、ここでは肢体コンポーネントとでも呼んでおきます。

            
            import { Component, Input, AfterViewInit, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Subscription, fromEvent } from 'rxjs';
import { mapTo, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
    selector: 'app-my-lazyloaded-compo',
    template: `
        <div class="box-style" #reactiveBox>
            <p>Hello, {{data.name}}!!</p>
            <p>Lazy-loaded Compo {{data.id}}</p>
        </div>
    `,
    styleUrls: ['./my-child-lazyloaded-compo1.component.scss']
})
export class MyLazyloadedCompoComponent implements AfterViewInit, OnDestroy {
    @Input() data: any;
    @Input() elemInParent: ElementRef;
    @ViewChild('reactiveBox', { static: false }) reactBox: ElementRef;

    private subscription: Subscription;

    constructor() { }

    ngAfterViewInit(): void {
        const terms$ = fromEvent<any>(this.reactBox.nativeElement, 'click').pipe(
            mapTo(this.data),
            debounceTime(1000),
            distinctUntilChanged()
        );
        this.subscription = terms$.subscribe(inner_data => {
            if (this.elemInParent) {
                this.elemInParent.nativeElement.innerHTML = 'You\'re now selecting...:' + JSON.stringify(inner_data);
            }
        });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}
        
スタイルファイルは以下のようにしておきます。

            
            .box-style {
    padding: 40px 16px;
    display: block;
    border: 1px solid #ccc;
    margin-bottom: 16px;
}
        
動的コンポーネントは、用途上app.module.ts内のentryComponentsに登録しなければ利用できません。

自動では追加してくれませんので以下のように追加します。

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

import { MyHappyCompoComponent } from './components/my-happy-compo/my-happy-compo.component';
import { MyChildLazyloadedCompo1Component } from './components/my-lazyloaded-compo/my-lazyloaded-compo.component';

@NgModule({
    declarations: [
        AppComponent,
        MyHappyCompoComponent,
        MyLazyloadedCompoComponent
    ],
    imports: [
        BrowserModule
    ],
    providers: [],
    bootstrap: [AppComponent],
    entryComponents: [
        MyLazyloadedCompoComponent // ✍ 追加
    ]
})
export class AppModule { }
        
ちなみにIvy以降から@ngModuleデコレーター内のentryComponentsへの明示的記述は不要になっています。


EventEmitterの代わりのfromEvent

肢体コンポーネントから頭コンポーネントへのイベントを処理しようとした場合、単純にEventEmitterでもデータ受け渡しが出来そうではあります。

とはいえ、動的コンポーネントを活用するユースケースの殆どは、同じコンポーネントを大量に使い回す必要があったり、複雑なバリエーションをもった異なるコンポーネントを適切に切り替えたい場合であります。

そうでなければ、静的なコンポーネントを確実に実装すべきです。

肢体コンポーネント毎に、安全に動作するコールバック関数のような機能であったり、状態を堅牢かつ柔軟に管理できる
Rxjsの利用を早い段階から考慮しておくと良いと考えます。(今回のようなクリック1回程度の例ではあまりご利益が薄いですが、スクロールみたいな処理で知らずにEventEmitterをやると地獄をみます...)

Rxjsを利用したイベントがどのくらい便利か、というのはココではとても語り尽くせないのですが、今回試しに挙げております。

上記のソースコードの抜粋ですが、

            
            ...
    const terms$ = fromEvent<any>(
        this.reactBox.nativeElement, 'click' // コンポーネントのDOM要素がクリックされたら...
    ).pipe(
        mapTo(this.data),      // クラスメンバのdataを送信
        debounceTime(1000),    // その後1s間は他から着火したイベントは受け付けない
        distinctUntilChanged() // 送信されたdataが既に送信済みなら棄却(dataは一回だけ採用)
    );
...
        
の部分で、割と自前でフルスクラッチすると頭を抱えそうな悩ましいイベントの処理がスマートかつコンパクトに実装できております。

Rxjsに慣れますと、複雑な処理をさせようとすればするほど、Rxjsの力添えなしにはコーディングできなくなるほどの魅力があります。

lazyloadingCompoFactory

最後に動的コンポーネントを製造してくれるファクトリーです。

            
            import { Injectable, ComponentFactoryResolver, ViewContainerRef, ElementRef } from '@angular/core';
import { Subject } from 'rxjs';

import { MyLazyloadedCompoComponent } from '../components/my-lazyloaded-compo/my-lazyloaded-compo.component';

@Injectable({
    providedIn: 'root'
})
export class LazyloadingCompoFactory {
    rootViewContainer: ViewContainerRef;

    constructor(
        private cfr: ComponentFactoryResolver
    ) { }

    public setRootViewContainerRef(viewContainerRef: ViewContainerRef) {
        this.rootViewContainer = viewContainerRef;
    }

    public addDynamicComponent(data: any, elemInParentComp?: ElementRef) {
        const factory = this.cfr.resolveComponentFactory(MyLazyloadedCompoComponent);
        const component = this.rootViewContainer.createComponent(factory);
        (component.instance as MyLazyloadedCompoComponent).data = data;
        if (elemInParentComp) {
            (component.instance as MyLazyloadedCompoComponent).elemInParent = elemInParentComp;
        }
        this.rootViewContainer.insert(component.hostView);
    }
}
        
これは、頭コンポーネントに、肢体コンポーネントのインスタンスを生成し、クラスメンバの参照をバイパスしているサービスとなります。


ビルド〜動作

それではビルドして動かしてみます。

app.component.htmlのhtml部分の適当なところに<app-my-happy-compo></app-my-happy-compo>を書き換えて、頭コンポーネントを呼び出してみます。

プロジェクトをビルドし、ブラウザで動作確認してみると以下のように動作させることができます。

合同会社タコスキングダム|蛸壺の技術ブログ


まとめ

今回は普段はあんまりやらなそうなコンポーネント間でのデータ受け渡しを解説しました。

今後機会ができたら、本格的なLazy Loadな動的コンポーネントを特集したいと思います。


参考サイト

【Angular】コンポーネント間のデータの受け渡し方法

Angular | サービスを使用してデータをコンポーネント間で共有する


関連書籍

Rsjxに関する邦書はほとんどないので、公式のAPIリファレンスを自分流に紐解いていくしか勉強法がないが現状です。

以下の本はRxJSの章があるので興味があればお手元にどうでしょうか。


Angularの学び方

主要なJavascriptのフレームワークの一角であるAngularは、現在Google主導で開発が進められている玄人フロントエンジニア向けのフレームワークです。

Angularを使えばかなり高度なWebアプリも自由に作成できるのですが、やはり初心者が独学で勉強しようとおもうとかなりの知識が必要でAngularは挫折しやすいと言われています。

特にAngularを使う上で注意が必要なのは半年に一回のペースでプログラムのメジャーバージョンが繰り上がるので、他のフレームワークと比べても格段に技術を追っかけるスピードが早いことも躓く人が多い一因になっているとも思います。

Udemyの動画講座では、基礎からじっくり学べるためより実践的なスキルを身につけることが可能です。

以下の講座ではAngular初心者向けに簡単なプロジェクトの作成まで学ぶことができます。

より実践的にフロントエンジニアとして重宝されるためにはSPA(シングルページアプリケーション)をある程度作成できるスキルが必要になりますので、さらに学びたい方は以下のような講座も用意されています。

他にもAngularプログラミングの習得度に応じた講座も多数用意されているので、必要に応じて試されてはいかがでしょうか。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。