以前「FormのValidationの書き方を簡略化させる」と言う記事を書きましたが、その時、特にValidationエラーのメッセージについては触れませんでした。
今回、Validationエラーも含めた「メッセージ管理」について考えて行こうと思います。
以前の記事をまだ見ていない方は、是非、目を通して頂けると幸いです。
ちなみに、今回の考え方と実装は、Angularに限らず(TypeScriptやJavaScriptを使っているフレームワークであれば)、同じように使えるかな、と思います。
(1)そもそも、何が問題なのか
前回、最終的にテンプレート(View)側は下記のようになりました。[register.component.html]
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()"> <div> <label>お名前</label> <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)" [ngClass]="{'alert-danger' : v.nameInvalid}"> <ng-container *ngIf="v.nameInvalid"> <ng-container *ngIf="v.nameHasErrorRequired"> <p class="error-message">お名前は必ず入力してください。</p> </ng-container> <ng-container *ngIf="v.nameHasErrorMaxLength"> <p class="error-message">お名前は10文字以内で入力してください。</p> </ng-container> </ng-container> </div> <button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button> </form>
これの何が問題になるのでしょうか・・・・?
例えば、必須入力のエラーである「xxxは必ず入力してください。」ですが、内容を「xxxの入力は必須です。」に変えたい、となった場合を考えます。
テンプレート(View)側を変えれば良いのでしょうが、例えば、必須項目が数十個あった場合はどうでしょうか?
そして、この画面だけでなく、他の画面でも必須項目があった場合はどうでしょうか?
なるべく修正箇所は少なく済ませたいですが、どうしても対応する量が多くなってしまいます。
(2)メッセージを1箇所で管理する
まあ、1箇所って言ってしまうと極端ですが、なるべくまとめて管理する、が良いと思います。※例えば、メッセージにも「正常」「エラー」「ワーニング」など種別があると思いますので、それを全て1箇所で管理すると、今度は管理するファイルが肥大化することになるので、種別毎に管理した方が良い・・・とか、ですね。
早速、まとめて管理するファイルを作ろうと思います。
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。
今回は、以下のように作成します。
src/ └ app/ └ shared/ └ message/ └ error-messages.ts
見ての通り、今回は、あくまで「エラー用メッセージ」として管理します。
(3)メッセージ管理処理の仕様について
さて、管理用のファイルを作ったところで、実装はどのようにしましょう。パッと思い付く要求仕様としては、
- 複数のメッセージを定義可能。
- メッセージは、差し込みが可能。
- メッセージ毎にユニークなKeyを持ち、Keyからメッセージを取得可能。
と言うところですかね。
特に今回は「メッセージは、差し込みが可能。」が重要です。
必須項目であれば、「{0}は必ず入力してください。」と定義して、「{0}」の部分を名前だったり、電話番号だったり、郵便番号だったり・・・・と動的に差し込み出来れば、メッセージの定義は1つだけで済みます。
では、要求仕様を満たす実装をします。
[error-messages.ts]
export const errorMessages: { [key: string]: string } = { msg_error_field_required: '{0}は必ず入力してください。', msg_error_field_max: '{0}は{1}文字以内で入力してください。', }; function formatMessage(msg: string, ...args: any[]): string { return msg.replace(/\{(\d+)\}/g, (m, k) => { return args[k]; }); } export function getMessage(messageId: string, ...args: any[]): string { return formatMessage(errorMessages[messageId], ...args); }
ざっくりと説明します。
まず、他の処理からこの「メッセージ管理処理」を使いたい時は、メッセージの Key と、差し込みたい文字列を引数に getMessage を呼ぶことになります。
export function getMessage(messageId: string, ...args: any[]): string { return formatMessage(errorMessages[messageId], ...args); }
getMessage の中で、formatMessage が呼ばれ、ここで、Keyに該当するメッセージを取得しつつ、差し込み文字列を置換している、と言う感じです。
function formatMessage(msg: string, ...args: any[]): string { return msg.replace(/\{(\d+)\}/g, (m, k) => { return args[k]; }); }
メッセージの定義自体は、連想配列として、Key・Valueで定義しています。
export const errorMessages: { [key: string]: string } = { msg_error_field_required: '{0}は必ず入力してください。', msg_error_field_max: '{0}は{1}文字以内で入力してください。', };
(4)メッセージ管理処理を使ってみる
では、実際にメッセージ管理処理を使ってみます。まずは、コンポーネント側の実装です。
[register.component.ts]
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RegisterValidator } from './register.validator'; import { getMessage } from '@app/shared/message/error-messages'; // (中略) export class RegisterComponent implements OnInit { // (中略) formRegister: FormGroup = this.formBuilder.group({ name: ['', [Validators.required, Validators.maxLength(10)]], }); // (中略) message(messageId: string, ...args: any[]): string { return getMessage(messageId, ...args); }
コンポーネント側で「error-messages.ts」のimportを行い、
メッセージ管理処理の「getMessage」を呼び出す「message()」を追加しました。
この「message()」を呼び出すのは、テンプレート側です。
[register.component.html]
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()"> <div> <label>お名前</label> <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)" [ngClass]="{'alert-danger' : v.nameInvalid}"> <ng-container *ngIf="v.nameInvalid"> <ng-container *ngIf="v.nameHasErrorRequired"> <p class="error-message">{{ message('msg_error_field_required', 'お名前') }}</p> </ng-container> <ng-container *ngIf="v.nameHasErrorMaxLength"> <p class="error-message">{{ message('msg_error_field_max', 'お名前', 10)}}</p> </ng-container> </ng-container> </div> <button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button> </form>
元々、メッセージを固定で記載していた部分を以下のように変更しています。
{{ message('msg_error_field_required', 'お名前') }} {{ message('msg_error_field_max', 'お名前', 10)}}
テンプレート側とコンポーネント側で「10」を2回定義しているのが気になりますね。ここは定数宣言して1回の定義で済むようにしましょう。
[register.component.ts]
nameMaxLength = 10; formRegister: FormGroup = this.formBuilder.group({ name: ['', [Validators.required, Validators.maxLength(this.nameMaxLength)]], });
テンプレート側も変えておきます。 [register.component.html]
{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}
これで全ての対応が完了しました。
今後、仮に「xxxは必ず入力してください。」を「xxxの入力は必須です。」に変えたいとなっても、
全項目・全画面確認する必要はなく、「error-messages.ts」を変更すれば、一律変更出来ることになりました。
(5)デメリットも把握しておく
当然ながら、汎用的に使われているメッセージについて、「特定の1箇所だけメッセージだけ変えたい」と言う要望に対しては、対応が難しくなります。まあ、そんな事はあまり無い思いますが、それよりも、実際に弊社が抱えている悩みとしては・・・・・
デザイナーが作ったhtmlをAngular側に反映する時に苦労する。
があります。
この方式は結局のところ、テンプレート側にロジックを記載している事になるのですが、デザイナーとしては、そんなこと知ったこっちゃないので、元のままメッセージ直書きしたhtmlを渡してきます。
画面のレイアウトに変更があった場合など、プログラマーが気を付ける必要がある、と言う事が、デメリットと言えばデメリットなのかもしれません。
Angular
0 件のコメント:
コメントを投稿