前回、「機能内の画面構成(コンポーネント構成)設計」について記載したので、今回は、「画面ごとのコンポーネント設計」を記載しようと思います。
それでは、前回の続きとして、『アカウント登録処理』から「登録情報入力画面」について、コンポーネントの設計を説明しようと思います。
(1)前回のおさらい
「アカウント登録処理」として、以下のような画面が必要と想定されます。①登録情報入力画面
②入力情報確認画面
③登録完了画面
画面遷移としては以下になります。
そして、ディレクトリ構成は以下となります。(一部省略)
src/ └ app/ └ account/ └ containers/ ├ account/ ├ account-register │ └ account-register.component.html │ └ account-register.component.spec.ts │ └ account-register.component.ts ├ account-register-confirm │ └ ・・・ └ account-register-complete └ ・・・
図で表すと、以下のようになります。
(2)画面単位でのディレクトリ構成を考える
画面単位で考えた時、「登録情報入力画面」にはどんな機能が含まれるでしょうか?- 画面の表示
- 入力項目のチェック(バリデーション、業務的な関連チェックなど)
- 入力項目を次の画面へ持ち越す為の準備
- 確認画面から戻って来た場合は、入力した値を再現
などなど、色々ありますね。
もっと複雑な機能であれば、「遷移先を入力値によって分岐させる」や、「APIを呼び出し、入力用の補足情報を取得する」・・・・なんて事もあるかもしれません。
このような多種多様な処理を以下の「containers」配下のコンポーネントだけでやるとしたら、コンポーネントが肥大化してしまうと思います。
src/ └ app/ └ account/ └ containers/ └ account-register └ account-register.component.ts ← 肥大化する
そこで、「画面の表示」「入力項目のチェック」など、View(ビュー)側に属する処理については、「presentations」ディレクトリに分けることにします。
と言うことで「presentations」ディレクトリとファイルを追加します。
src/ └ app/ └ account/ └ containers/ └ presentations/ └ register/ └ register.component.html └ register.component.spec.ts └ register.component.ts └ register.validator.ts
※「 register.validator.ts」について
弊社のプロジェクトでは、Validationは出来る限りViewとコンポーネントからは切り離して別ファイルで管理するようにしています。(Validationについては、以前の記事「【Angular】FormのValidationの書き方を簡略化させる。」を参照ください。)
(3)「Container」と「Presentation」の切り分け方
「Container」と「Presentation」の切り分けですが、弊社では以下のように定義付けしています。「Container」として分類するもの
- 画画面の状態保持に関すること(NgRxのStore操作など)
- 画面に入力した値の業務チェック(API呼び出しが必要なもの、など)
- API(バックエンド処理)の呼び出し
- 画面遷移
「Presentation」として分類するもの
- 画面表示、及び表示内容の制御(エラー時など)
- 画面に入力した値のバリデーションチェック
- 画面に入力した値の関連チェック、及び業務チェック
「関連チェック」というのは、例えば「AとBが選択されているときは、Cは必須となる」とか、です。
両方に含まれている「業務チェック」が、判断難しいところかな、と思います。
クライアント内で完結出来るチェックであれば「Presentation」。完結出来ないものは「Container」、という考えで良いと思います。
アカウント登録で言うと、「入力されたメールアドレスが既に使用されているかどうか」のチェックは、クライアント内では完結出来ず、バックエンドの処理(API等)を呼び出す必要があると思いますので、「Container」側に記載する必要があります。
さて、今の状態を図で表すと、以下のようになります。(登録情報入力画面のみ)
(4)「Container」と「Presentation」間のデータ・処理受け渡し方法を考える
ここで、分かりやすいように、「Container」を親、「Presentation」を子として、話を進めようと思います。親と子の間では、データ・処理のやり取りが必要になります。
コンポネート間のデータ・処理のやり取りについては、いくつか方法がありますが、
今回は、「@Input()、@Output()」を使います。
参考サイト(本家):ディレクティブとコンポーネントの親子間でのデータ共有
(5)親子間でやり取りが必要なデータ・処理を考える
方式が決まったところで、親子間で、どんなデータ・処理のやり取りが必要か考えます。登録情報入力画面の内部処理は、以下の流れになると思います。
1. 画面初期表示
※確認画面から戻ってきた場合は、画面の入力値を復元
→親から子へ、復元用のデータを送る(@Input())
2. 画面の各フィールドへの値入力(バリデーション)
3. Submitによって、入力値を引き継ぎつつ、確認画面へ遷移
→入力値を引数に、子から親へ処理を引き継ぐ(@Output())
上記を実現したいのですが、ここで、
- 親子間でのデータ
- 次画面(確認画面)に送るデータ
src/ └ app/ └ account/ └ containers/ └ presentations/ └ models/ └ account.ts
中身は下記のようにします。
// アカウント登録用画面間保持データモデル export class AccountViewSaveModel { mobilePhoneNumber: string; name: string; }とりあえず、電話番号と名前を項目として用意しました。この辺はあくまで例なので適当です。
続いて、親と子のコンポーネントの中身を実装します。
(6)親子間のデータ・処理のやり取りを実装する
まずは親コンポーネントの実装です。[account-register.component.ts]
import { Component, OnInit } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Router } from '@angular/router'; import { AccountViewSaveModel } from '../../models/account'; import * as fromAccount from '../../store/reducers'; import * as AccountActions from '../../store/actions/account.actions'; @Component({ selector: 'koma-account-register', templateUrl: './account-register.component.html', }) export class AccountRegisterComponent implements OnInit { // Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている) // 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの) accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost)); constructor(public store: Store<fromAccount.State>, private router: Router) {} ngOnInit(): void {} onSubmit(formModel: AccountViewSaveModel): void { // 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。 this.store.dispatch( AccountActions.setRegisterPostData({ data: formModel }), ); // 確認画面へ遷移する this.router.navigateByUrl('/account/register-confirm'); } }
唐突に「NgRxのStore」が出てきましたが、ここは一旦スルーしてください。(いずれ、NgRxの実装方法も記事にしようと思います。)
ここでは、画面で入力された値はStore(と言う場所)に格納されている、ぐらいの理解でOKです。
(今回重要なのは、「データ保存方式」ではなく、あくまで「データ受け渡し部分」なので)
細かい説明も実装内のコメントとして記載しましたので、参考にお願いします。
続いて、子コンポーネントの実装です。
[register.component.ts]
import { Component, OnInit, Input, Output, OnDestroy, EventEmitter } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; import { RegisterValidator } from './register.validator'; import { AccountViewSaveModel } from '../../models/account'; import { getMessage } from '@app/shared/message/error-messages'; @Component({ selector: 'koma-register', templateUrl: './register.component.html', }) export class RegisterComponent implements OnInit, OnDestroy { // Input・Outputの定義。 @Input() accountViewSave$: Observable<AccountViewSaveModel>; @Output() formSubmit = new EventEmitter<AccountViewSaveModel>(); mobilePhoneNumberMaxLength = 11; nameMaxLength = 20; registerSubscription: Subscription = new Subscription(); formRegister: FormGroup = this.formBuilder.group({ mobilePhoneNumber: ['',[Validators.required,Validators.maxLength(this.mobilePhoneNumberMaxLength)]], name: ['', [Validators.maxLength(this.nameMaxLength)]], }); constructor(private formBuilder: FormBuilder, public v: RegisterValidator) {} ngOnInit(): void { // バリデーション設定 this.v.formGroup = this.formRegister; this.registerSubscription.add( // 画面初期値設定 this.accountViewSave$.subscribe(value => { if (value) { this.formRegister.controls.mobilePhoneNumber.setValue( value.mobilePhoneNumber, ); this.formRegister.controls.name.setValue(value.name); } }), ); } ngOnDestroy(): void { // サブスクリプション解除 this.registerSubscription.unsubscribe(); } // 画面でSubmitが発生した時の処理 onSubmit(): void { if (this.formRegister.valid) { // バリデーションエラーが発生していない場合 const formModel = { mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value, name: this.formRegister.controls.name.value, } as AccountViewSaveModel; this.formSubmit.emit(formModel); } } message(messageId: string, ...args: any[]): string { return getMessage(messageId, ...args); } }ちょっと記載量が多いのですが、あくまで「親子間のデータやり取り」部分についてのみ説明します。
その他、FormBuilder, FormGroup, Validatorsあたりは、以前の記事などを参照ください。
参照:FormのValidationの書き方を簡略化させる
まずはInput・Outputの定義です。
@Input() accountViewSave$: Observable<AccountViewSaveModel>; @Output() formSubmit = new EventEmitter<AccountViewSaveModel>();Inputは、親からデータが来る為の受け口になるので、分かりやすく同じ変数名にします。
また、型も同じ「Observable」にします。
Outputについては、「処理を引き継ぐ」為のお決まりの「EventEmitter」を使います。
続いて、ngOnInitで、親からのデータをFormに反映する処理をSubscribeします。
// 画面初期値設定 this.accountViewSave$.subscribe(value => { if (value) { this.formRegister.controls.mobilePhoneNumber.setValue( value.mobilePhoneNumber, ); this.formRegister.controls.name.setValue(value.name); } }),
Subscribeする事で、親からのデータが来れば、常にアクティブに画面表示が更新されることになります。
最後が、画面でSubmitが発生した場合です。
// 画面でSubmitが発生した時の処理 onSubmit(): void { if (this.formRegister.valid) { // バリデーションエラーが発生していない場合 const formModel = { mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value, name: this.formRegister.controls.name.value, } as AccountViewSaveModel; this.formSubmit.emit(formModel); } }
バリデーションエラーが発生していない場合、画面で入力された値を Model にセットし、その Model を引数に Emit します。 Emitする事で、親側に処理を引数ごと引き渡すことになります。
続いて、親のテンプレート(View)を実装します。
[account-register.component.html]
<koma-register [accountViewSave$]="accountViewSave$" (formSubmit)="onSubmit($event)" ></koma-register>左辺が子の変数(処理)、右辺が親の変数(処理)となります。
ここで、親子を結び付けています。
最後に、子のテンプレート(View)を実装します。
[register.component.html](form部分のみ抜粋)
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()"> <div class="form-input"> <div> <label>携帯電話番号<span>必須</span></label> <input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力" [ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus> <ng-container *ngIf="v.mobilePhoneNumberInvalid"> <p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p> <p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p> </ng-container> </div> <div> <label>お名前</label> <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(任意)" [ngClass]="{'alert-danger' : v.nameInvalid}"> <ng-container *ngIf="v.nameInvalid"> <p class="error-message">{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}</p> </ng-container> </div> </div> <footer> <button [disabled]="formRegister.invalid" type="submit">確認</button> </footer> </form>こちらに関しては、特に「親子間のデータやり取り」について意識している部分は無いですね。
これで、
1. 画面初期表示
※確認画面から戻ってきた場合は、画面の入力値を復元
→親から子へ、復元用のデータを送る(@Input())
2. 画面の各フィールドへの値入力(バリデーション)
3. Submitによって、入力値を引き継ぎつつ、確認画面へ遷移
→入力値を引数に、子から親へ処理を引き継ぐ(@Output())
が実現出来たことになります。(長かった・・・・・)
最終的に図で表すと下記のようになります。
今回は、コンポーネント設計(と実装)を紹介しました。
もっと複雑な画面になると、設計方法や実装も違ってきますので、それはまた別途記事にしようと思います。
Angular
0 件のコメント:
コメントを投稿