こんにちは、情報システム部の高野です。
ここのところバタバタしていて全然ブログが書けませんでした。
本当に継続は難しい・・・
ということでブログが書けているってことは少し落ち着いてきたってことです(笑)
まだリリースはしていないのですが、現在携わっているシステムはフロントをAngular*1で作っています。
かなり入力項目が多いシステムなのでカテゴリごとにコンポーネント化しています。
Angularをはじめ最近のWebフロントエンドのフレームワークはコンポーネント化が簡単にできるので便利ですよね!
今回の仕組みは下図のようにコンポーネントを分けています。*2
親コンポーネントは、Angularのルーティングでページ遷移しますので
親コンポーネントAを表示している時に存在する子コンポーネントはaとbだけです。
コンポーネント化は簡単なのですが、コンポーネント間の値の受け渡しが発生すると
気を付けて設計しないとコードがとても読みづらいものになってしまいます。
今回の仕組みで発生したコンポーネント間の受け渡しのパターンは
- 親コンポーネントAから子コンポーネントaのように「親から子」に値を受け渡しするケース
- 子コンポーネントaから子コンポーネントbのように「子から子」に値を受け渡しするケース
- 子コンポーネントaから子コンポーネントcのように「親が違う子」に値を受け渡しするケース
です。
下記で示すサンプルは全てパターン02を実装していますが、
パターン03にも応用可能です。
Serviceクラス経由で値を受け渡す
先に結論から言うとServiceクラスを経由して値を受け渡すのが簡単でシンプルです。
(公式のチュートリアルにも載ってるので一番オーソドックスなやり方なのでしょう)
コードは次のような感じになります。*3
サンプルとしては子コンポーネントaで入力し、子コンポーネントbでそれを表示するようにしています。*4
Serviceクラス
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class ValueSharedService { // 受け渡しを行うプロパティ firstName = ''; lastName = ''; }
子コンポーネントa
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; import { ValueSharedService } from '../../value-shared.service'; @Component({ selector: 'app-child-a', templateUrl: './child-a.component.html', styleUrls: ['./child-a.component.css'] }) export class ChildAComponent implements OnInit { form: FormGroup; constructor(private valueSharedService: ValueSharedService, private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ lastName: this.valueSharedService.lastName, firstName: this.valueSharedService.firstName, }); } onChangeName() { this.valueSharedService.lastName = this.form.get('lastName').value; this.valueSharedService.firstName = this.form.get('firstName').value; } }
<div [formGroup]="form"> <div> <label>姓:<input type="text" formControlName="lastName" (change)="onChangeName()"></label> </div> <div> <label>名:<input type="text" formControlName="firstName" (change)="onChangeName()"></label> </div> </div>
入力に応じてイベントが実行され、Serviceクラスのプロパティを書き換えます。
子コンポーネントb(表示側)
import { Component} from '@angular/core'; import { ValueSharedService } from '../../value-shared.service'; @Component({ selector: 'app-child-b', templateUrl: './child-b.component.html', styleUrls: ['./child-b.component.css'] }) export class ChildBComponent { get fullName() { return `${this.valueSharedService.lastName} ${this.valueSharedService.firstName}`; } constructor(private valueSharedService: ValueSharedService) { } }
<p> {{fullName}} </p>
子コンポーネントbは、Serviceクラスの値を連結して表示するだけです。
Serviceクラス経由で値を受け渡す方法は、直感的でなんら引っかかるところが無いと思います。
まあServiceクラスがシングルトンだということだけ意識してれば良いかと。
非常にシンプルで分かりやすい!
さて、これを別の方法でやってみたいと思います。
@Input()と@Output()を使う
まず親から子にデータを渡す方法としては、@Input()を使います。
子コンポーネントb
import { Component, Input } from '@angular/core'; import { Name } from '../../name'; @Component({ selector: 'app-child-b', templateUrl: './child-b.component.html', styleUrls: ['./child-b.component.css'] }) export class ChildBComponent { @Input() name: Name; get fullName() { return `${this.name.lastName} ${this.name.firstName}`; } }
HTMLは、Service経由の時と変わらないので割愛します。
親コンポーネント(表示側の指定)
@Input()を使う場合は、親側からHTMLで渡したい値を指定するだけです。
<div> <app-child-b [name]="name"></app-child-b> </div>
[name]の部分が子コンポーネントの@Input()を指定した変数名になります。
子コンポーネントa
import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { Name } from '../../name'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'app-child-a', templateUrl: './child-a.component.html', styleUrls: ['./child-a.component.css'] }) export class ChildAComponent implements OnInit { @Output() public nameChanged = new EventEmitter<Name>(); form: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ lastName: '', firstName: '', }); } onChangeName() { this.nameChanged.emit({ lastName: this.form.get('lastName').value, firstName: this.form.get('firstName').value }); } }
HTMLは、Serviceクラス経由の時と変わらないので割愛します。
@Output()は、EventEmitter型になります。
onChangeName関数でこれを呼び出すことにより親コンポーネントで指定したコールバック関数が実行されます。
引数で渡したい値を指定すれば親コンポーネントに値が渡せます。
親コンポーネント(入力側の指定)
<div> <app-child-a (nameChanged)="onChangeName($event)"></app-child-a> </div>
HTMLで@Output()の変数に実行したい関数を渡します。
この方法は、サンプルだとシンプルであまり問題無いように見えますが
子から親に値を渡す方法がコールバック関数の引数経由になるので
処理が複雑になってくるとバグの温床になりかねません。
子コンポーネントの値が変わった時に親コンポーネントでなにかしらの処理を実行したい時には有用ですが
単純に値を渡すだけのために使わない方が良いのではないかと個人的には思います。
それとパターン03に対応するのはとても複雑になります。
@ViewChild()を使う
@ViewChild()を使うと親コンポーネントに子コンポーネントのインスタンスを持つことができるようになります。
子コンポーネントのインスタンスがあるのでpublicメンバーであればやりたい放題になります。
@ViewChildを使ったサンプルコードです。
親コンポーネント
import { Component, OnInit, ViewChild } from '@angular/core'; import { Name } from '../name'; import { ChildAComponent } from './child-a/child-a.component'; import { ChildBComponent } from './child-b/child-b.component'; @Component({ selector: 'app-parent-a', templateUrl: './parent-a.component.html', styleUrls: ['./parent-a.component.css'] }) export class ParentAComponent implements OnInit { @ViewChild(ChildAComponent) childA: ChildAComponent; @ViewChild(ChildBComponent) childB: ChildBComponent; constructor() { } ngOnInit() { this.childA.nameChanged = this.onChangeName; } onChangeName = (name: Name) => { this.childB.name = name; } }
@ViewChildを使って子コンポーネントのインスタンスを持つようにしています。
ngOnInit内で子コンポーネントa に対して関数を渡します。
渡した関数onChangeName 内で子コンポーネントbに対して
子コンポーネントaから渡ってきたnameを渡します。
<div> <app-child-a></app-child-a> </div> <div> <app-child-b></app-child-b> </div>
HTMLでは、子コンポーネントを指定するだけで値の受け渡しは無くなります。
子コンポーネント側は何も工夫が無いので載せる必要も無いのですが一応
子コンポーネントa
import { Component, OnInit } from '@angular/core'; import { Name } from '../../name'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'app-child-a', templateUrl: './child-a.component.html', styleUrls: ['./child-a.component.css'] }) export class ChildAComponent implements OnInit { public nameChanged: (name: Name) => void; form: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ lastName: '', firstName: '', }); } onChangeName() { this.nameChanged({ lastName: this.form.get('lastName').value, firstName: this.form.get('firstName').value }); } }
子コンポーネントb
import { Component, OnInit } from '@angular/core'; import { Name } from '../../name'; @Component({ selector: 'app-child-b', templateUrl: './child-b.component.html', styleUrls: ['./child-b.component.css'] }) export class ChildBComponent { name: Name = { lastName: '', firstName: '' }; get fullName() { return `${this.name.lastName} ${this.name.firstName}`; } constructor() { } }
このやり方では、親コンポーネント以外はAngular特有の処理が無くなるのでシンプルに見えますが
親コンポーネントから子コンポーネントに対して何でもやれてしまうので
きちんとルールを設けないとカオスに陥ります。
それとこの例では問題無いのですがAngularのライフサイクルを意識して作らないと思わぬバグに嵌ることにもなります。
ReactiveForms専用の方法
この方法は今回のプロジェクトでは使用していません。
このブログを書くにあたり色々調べていたら出てきたやり方です。
これをもっと早く知っていればこの方法で実装したのになっていう方法です。
親コンポーネント
import { Component, OnInit } from '@angular/core'; import { Name } from '../name'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'app-parent-a', templateUrl: './parent-a.component.html', styleUrls: ['./parent-a.component.css'] }) export class ParentAComponent implements OnInit { form: FormGroup; get name(): Name { return { lastName: this.form.get('name').get('lastName').value, firstName: this.form.get('name').get('firstName').value, }; } constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({}); } }
ここでは空のFormGroupを作っておきます。
nameプロパティは、FormGroupから取り出した値を返すようになっています。
<div [formGroup]="form"> <app-child-a></app-child-a> </div> <div> <app-child-b [name]="name"></app-child-b> </div>
HTML側にはformGroupの指定を追加しています。
子コンポーネントa
import { Component, OnInit } from '@angular/core'; import { FormGroupDirective, FormGroup, FormControl, ControlContainer } from '@angular/forms'; @Component({ selector: 'app-child-a', templateUrl: './child-a.component.html', styleUrls: ['./child-a.component.css'], viewProviders: [ { provide: ControlContainer, useExisting: FormGroupDirective } ] }) export class ChildAComponent implements OnInit { constructor(private parent: FormGroupDirective) { } ngOnInit() { const form = this.parent.form; form.addControl('name', new FormGroup({ lastName: new FormControl(''), firstName: new FormControl(''), })); } }
constructorでFormGroupDirectiveを受け取ります。
このインスタンスから親のFormGroupが取得できます。
<div formGroupName="name"> <div> <label>姓:<input type="text" formControlName="lastName"></label> </div> <div> <label>名:<input type="text" formControlName="firstName"></label> </div> </div>
まとめ
今回のプロジェクトでAngularのコンポーネント分割と値の受け渡しに関して
良いところも大変なところも大分見えました。
Angularは、コンポーネント間で値を受け渡す方法がいくつもあります。
どれが良いとは一概に言えないので都度判断して最良の方法を選択する必要があります。
なにか他のライブラリを使えばもっと簡潔に分かりやすいコードにできたかもしれません。
その辺りはまた次のプロジェクトで検討していきたいと考えてます。
弊社ではエンジニアを募集しています。
Angular以外にもVue.jsを使ったシステムもあります。
興味がある方は下記からエントリお願いします。
athome-inc.jp