カテゴリー
【Angular&rxjs】rxjs#fromEventとViewChildでコンポーネント間のデータ受け渡しを考えてみる
※ 当ページには【広告/PR】を含む場合があります。
2019/12/28
2022/08/10
今回は、
ネットで
Angular コンポーネント間 データ受け渡し
1. 親子関係のあるコンポーネント間で、
@Input/@Outputデコレーターを用いて
EventEmitter経由でデータをやり取りする方法
2. 親子関係のないコンポーネント間で、
サービスを利用して、
データを共有・監視する方法
の主に2つに区分できるようです。
これらのやり方は通常のAngularコンポーネントでデータ受け渡しをする際の正攻法になります。
例えば
構造ディレクティブ
動的コンポーネント
親子関係のような従属関係を構築させようというより、頭を身体を切り離した上で神経をつなげてみましょう、というお話です。
なんとも分かりにくい表現ですが、上記項目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の技術を追いかけておりますが、直近では
既に最新版の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-compo
my-happy-compo
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
ポイントは、このDOM要素は
@ViewChild
1つ目の
#dynamic
<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
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
<app-my-happy-compo></app-my-happy-compo>
プロジェクトをビルドし、ブラウザで動作確認してみると以下のように動作させることができます。

まとめ
今回は普段はあんまりやらなそうなコンポーネント間でのデータ受け渡しを解説しました。
今後機会ができたら、本格的なLazy Loadな動的コンポーネントを特集したいと思います。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー