これまたすんなり行かなかったので共有しとく
#その1 - Elementフォームのvalidateメソッドを参照したい
フォーム全体のバリデーションを行う方法については、Element-UI公式でCustom validation rulesのサンプルコードを丹念に読めば大体のところが把握できる。
が、TypeScriptでそのまま書こうとするとVSCodeに怒られてしまう。
this.$refs[formName].validate(callback);
Property 'validate' does not exist on type 'Vue | Element | Vue[] | Element[]'.
Property 'validate' does not exist on type 'Vue'.Vetur(2339)
$refs
の定義を確認すると
export interface Vue {
readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };
}
となっているが、実際に$refs
下に登録されているのはElementUIComponent
経由でVue
を継承したElForm
であり、そこに定義されているvalidate
メソッドはVue
からは参照できない。
#その1の解決法 - $refsを宣言し直す
以下、Vue.js公式のTypeScript SupportにあるClass-Style Vue Componentsを前提に進める。
まずElForm
の定義を取り込む。
<script lang="ts">
import { ElForm } from 'element-ui/types/form';
次に、インスタンスプロパティ$refs
について下記のように宣言。
export default class MyComponent extends Vue {
/* data */
public $refs!: { [key: string]: ElForm };
定義を被せる都合上、$refs
プロパティはVue
に合わせてpublic
で宣言する必要がある。TypeScriptでは、public修飾子
(デフォルト)は省略可だが、筆者の趣味で明示的に記述しておいた。
プロパティ名の後ろに付く!
はDefinite Assignment Assertionsというモノ。
本来であれば宣言時もしくはコンストラクタ内で初期化する必要のあるインスタンスプロパティが、コレを付けると「既に代入済みであり初期化不要」という扱いになる。このように記述した場合、当該のプロパティが$data
オブジェクトに取り込まれることもない。
$refs
へ登録されるフォーム名はel-form
タグのref
属性で指定するが、名称が固定であれば以下のように宣言しても構わない。
<el-form ref="myForm" :rules="myRules" :model="myModel">
export default class MyComponent extends Vue {
/* data */
public $refs!: { myForm: ElForm };
以上で、サンプルコードと同じような記述が可能となる。
this.$refs[formName].validate(callback);
/* あるいは */
this.$refs.myForm.validate(callback);
#その2 - バリデーション結果を画面に反映させたい
フォーム全体のバリデーション結果を、リアルタイムで画面上(ボタンの使用可否など)に反映させたい、というような欲求はあるかと思う。
フォームのvalidate
メソッドが、テンプレート内から直に呼び出すだけで結果(boolean)を取得できるような作りなら話は簡単だが、残念ながらそうなってはおらず上手く行かない。
/**
* Validate the whole form
*
* @param callback A callback to tell the validation result
*/
validate (callback: ValidateCallback): void
validate (): Promise<boolean>
コールバックを渡して呼び出せば同期的にバリデーション結果を用いた処理を実行できるが、戻り値はvoidに限定される。
引数無しで呼び出せばバリデーション結果(boolean)を載せたPromiseを返すが、いかんせん非同期。
したがってどちらも使えない。
#その2の解決法 - ライフサイクルフックを使う
テンプレートからメソッドを直接呼び出しても結果が取得できないとなると、残る手段はインスタンスプロパティを介して参照する以外に無い。そのためにはライフサイクルフックを利用することになる。
$data
オブジェクト下のデータがいずれかの入力フィールドに紐づいている場合、そのデータに変更が加わると紐付く入力フィールドで個別のバリデーションが実行される。しかる後に、そのバリデーション結果も踏まえたバーチャルDOMの再描画・書き替えが行われるが、その前後で動作するライフサイクルフックがbeforeUpdate
とupdated
である。今回はbeforeUpdate
を使用するのが適切だろう。
validate
メソッドについては同期・非同期どちらを用いても良いが、前者の方がテストを書くのも楽だし、もし必要なら入力フィールド毎のエラー詳細も利用できるので、今回はそちらを採用する。
まずvalidate
メソッドに渡すコールバックValidateCallback
の型定義を取り込む。
<script lang="ts">
import { ElForm, ValidateCallback } from 'element-ui/types/form';
export interface ValidateCallback {
/**
* The callback to tell the validation result
*
* @param isValid Whether the form is valid
* @param invalidFields fields that fail validation
*/
(isValid: boolean, invalidFields: object): void
}
次に、バリデーションの結果を受け取るインスタンスプロパティを用意する。
初期化については入力フィールドのデフォルト値に合わせて決め打ちでも良いが、遷移で諸々入力済みの状態から開始するような場合はバリデーション未実施の状態を区別するために nullable な型(boolean | null
)で宣言した上null
で初期化する手もある。
export default class MyComponent extends Vue {
/* data */
public $refs!: { [key: string]: ElForm };
private validationResult: boolean = false;
export default class MyComponent extends Vue {
/* data */
public $refs!: { [key: string]: ElForm };
private validationResult: boolean | null = null;
最後に、クラス直下へbeforeUpdate
メソッドを追加し、その中でフォームのvalidate
メソッドを実行する。
引数としては、バリデーションの結果をインスタンスプロパティへとセットするコールバックを用意する。
private beforeUpdate() {
this.updateValidationResult();
}
private updateValidationResult() {
const callback: ValidateCallback = (isValid, invalidFields) => {
this.validationResult = isValid;
};
this.$refs.myForm.validate(callback);
}
以上、分かってしまえば超簡単。
ライフサイクルフックを検証するテストの書き方については別記事で書いた。
【Vue】ライフサイクルフックのテストを書く【TypeScript】