JavaScript
vue.js

Vue.jsのpropsカスタムバリデーターを使った堅牢なコンポーネント作成

More than 1 year has passed since last update.

Vue.jsには、それぞれのコンポーネントのpropsにバリデーションルールを設定することができます。

ReactでいうところのpropTypesのような概念ですが、あちらより基本構造がシンプルであり、反面複雑な条件は自分でバリデーションルールを書くスタイルとなっています。

これまでは受け付けるpropsの名称だけを設定することも一般的でしたが、Vue 2.4以降は公式のスタイルガイドでも、propsに型や独自で定義するバリデーターを定義することを推奨することとなりました。

ですが、従来型の記法が利用されていることもまだまだ多く、中にはご存じないかたも多いのではないでしょうか。

この記事では、そんなVue.jsのpropsのバリデーターに焦点をあて、基本的な使い方についてご紹介します。


TL;DR


  • propsでキー名のみを指定するのはやめよう

  • とりあえず型だけでも縛ることができるなら型制限はやっておこう

  • 取得するものが決まっているなら正しくバリデータを設定しよう


対象読者


  • Vue.jsを触ったことがある人


Vue.jsとpropsの基本と問題

はじめにVue.jsのprops記法についておさらいします。

ここでは例として、「いいね」ボタンのコンポーネントとしてLikeButton.vueを定義しているとしましょう。

動作としては


  • 複数のサイズを持ち、 size プロパティで管理をする


  • count プロパティの値に応じてLike数を表示する

  • ボタンがクリックされたらLike数を+1した値を表示する


    • 実際ではAPIの処理などが入り、それらの結果は親コンポーネントがもつが、今回は省略する



としましょう。

最低限書くなら、恐らくこういった形となるでしょう。


LikeButton.vue

<template>

<div>
<button :type="type" class="like-button" :class="buttonClass" @click="handleClick">
+{{count}}likes
</button>
</div>
</template>

<style>
.like-button {
/* 何かしらのスタイル */
}

.like-button.button-small {
padding: 6px 10px;
}

.like-button.button-medium {
padding: 10px 13px;
}

</style>

<script>
export default {
props: ['count', 'size'],
data () {
return {
isClicked: false
}
}
computed: {
nowCount () {
return this.isClicked ? ( this.count + 1 ) : this.count;
},
buttonClass () {
const style = {}
style[`button-${this.size}`] = true
return style
}
},
methods: {
handleClick () {
this.isClicked = !this.isClicked;
}
}
}
</script>


一見特に問題なさそうではありますが、それぞれのpropsにはどのような値でも入ってしまうほか、空の場合というのもこれでは常にチェックが必要となってしまいます。

例えばこの例では、padding設定をそれぞれのボタンのサイズごとのスタイルに設定しているため、例えばここでsizeのプロパティに対して、extra-smalllargeなどと指定されるとpaddingのないものが表示されてしまうということも起こります。

また、countプロパティについては、そもそもが+1される可能性がありますので、数値型でないとNaNとなってしまう恐れがあります。

使うところがピンポイントなコンポーネントですとそれでも大体は把握できますが、「複数のサイズをもつボタン」のような、広く使われることが想定されているコンポーネントの場合、のちの破綻に直接的に関わってきます。

こういったシチュエーションでは、適切にpropsを記述し、それぞれのプロパティの値を保護しましょう。


型定義によるpropsバリデーション

まず、簡易的なものとして、JavaScriptの型によるバリデーションがあります。

あくまでもTypeScriptなどではなくJavaScriptベースの簡易的なものですので、複雑なインターフェースの定義には向きませんが、それでも、付与しておくだけで十分役に立ちます。

書き方としては、これまでのprops記法を、 「配列形式ではなくオブジェクト形式で書く」 ことによって表現できます。

まずはサンプルコードを見てみましょう。


従来の記法

export default {

props: ['count', 'size']
}


より厳密な記法

export default {

props: {
count: Number,
size: String
}
}

このように、keyにプロパティ名を、valueに型を定義してやることによって、その型で縛る事が可能となります。

実際に間違えた場合は、「[Vue warn]: Invalid prop: type check failed for prop "size". Expected Number, got String.」という形で知らせてくれます。

Screen Shot 2017-10-19 at 2.32.50.png

ただ数値だけにしたい。文字列だけにしたい。という場合には、これを活用することですぐに縛ることができ十分に便利でしょう。

また、API Specなどが不定の場合などであっても、Objectが返ってくることが保証される場合はひとまずObjectと置いておくだけでも十分意義はあるでしょう。


カスタムバリデータベースのバリデーション

型の整合性チェックは、特に数値型などでは有効ですが、オブジェクトの内部構造や、文字列の取扱を考えると十分といい難いシチュエーションも多くあります。

そういった場合は、関数ベースのカスタムバリデーションを利用するのが一番良いでしょう。

例えば、このボタンの例で言うと、見る限りはsizeプロパティにはsmallもしくはmedium以外が入ることを考慮されていないようですので、これらのみに制限するコードを書くと良さそうです。

実際のコードをベースとして紹介します。


型ベースのみの記法

export default {

props: {
count: Number,
size: String
}
}


カスタムバリデーションをベースとする記法

export default {

props: {
count: Number,
size: {
type: String,
validator (val) {
return ['small', 'medium'].includes(val)
}
}
}
}

このように定義することで、カスタムバリデータベースのバリデーションが可能となります。

動作としては、検証したいプロパティのキーに対しての値をObject型としておき、そのなかのvalidatorキーに対応している関数が呼び出されます。

validator()内ではval変数によって検査対象の値を取得できるようになっており、validator()の実行結果がtrueかfalseかによってその検証が妥当かどうかが判断できるようになっています。勿論、trueがバリデーションを通ったという意味となります。

今回の場合、Array.prototype.includesは配列に対して利用して存在するかどうかをBooleanで返してくれますので、そのままreturnする形で良いということとなります。

こういった形でカスタムバリデータを使うことによって、多少複雑なプロパティへの検査も可能となりますので、例えば「このオブジェクトにはこのキーが必須」という条件にも対応できるようになっていますので、これらをできる限り使うと良いでしょう。


必須とデフォルトバリデーション

最後に、必須とデフォルト値についても紹介します。

ここまでsizeとcountのバリデーションを足してきましたが、例えばsizeの場合はデフォルトはmediumで良いのでわざわざ毎回指定したくない。countであれば、Likeカウントであるため必ず数値は渡してほしい。

というモチベーションが出てくるかと思います。Vueのオブジェクト形式で記述するpropsには、それらの制御システムも備わっていますので、有効活用して無駄を減らすと良いでしょう。

例によって、サンプルコードをベースとして説明します。


バリデーションのみのコード

export default {

props: {
count: Number,
size: {
type: String,
validator (val) {
return ['small', 'medium'].includes(val)
}
}
}
}


必須とデフォルト値がついたコード

export default {

props: {
count: {
type: Number,
required: true
},
size: {
type: String,
default: 'meduim',
validator (val) {
return ['small', 'medium'].includes(val)
}
}
}
}

新たにrequiredとdefaultが追加されました。

これらはその名の通り、requiredは必ず要求するプロパティ(違反した場合はバリデーションエラーと同等の扱いとなる)、defaultは、特に指定がなかった場合に使われるデフォルト値となります。

このデフォルト値は、validator()の対象として渡されるため、ここが妥当であれば親コンポーネントから渡ってこない場合でもデフォルト値として問題なく検査に通る形となります。そのため、validator()でプロパティそもそもの必須チェックを行うという段階が来た時点でどこかが間違っているため、できるかぎりこちらを利用しましょう。

このように書くことで、デフォルトのサイズ、必要であるプロパティが明示され、読むがわもどのように利用すべきかが明確となりますので、非常にメンテナンスしやすく、また、間違えた時は警告される破綻しにくいコードとなっているでしょう


Vue.jsのスタンダードなスタイルとしての推奨

最後に、はじめに申し上げましたが、Vue公式スタイルとしても、この記法は推奨されているものとなります。

( https://vuejs.org/v2/style-guide/#Prop-definitions-essential )

既に存在するコード全てにこれを厳密に定義していくのは難しいでしょうが、新しいものでは出来る限り設定する、古いものでも順次設定していくことにより、よりメンテナンスしやすいものが生まれることは間違いありません。

Vue Standardに従って快適なコーディングを進めていきましょう。

Screen Shot 2017-10-19 at 2.52.27.png


おまけ:ホワイトリスト形式のバリデーションルール

この記事でも['small', 'medium'].includes(val)という形式をご紹介しましたが、こういったチェックをvalidatorに書いていくとどうしても大きくなっていきがちです。

また、例えば「オブジェクトでこのキーだけは必須にしたい」というものについても、なかなか細かいところまで指定しだすとキリがないでしょう。そういった場合は、程度にもよりますが以下のようにファイル自体を二つにわけて定義を切り出してしまうことも一つの手かと思います。

./LikeButton.vue

./LikeButton.type.js

そうして、type側には以下を書くとよさそうでしょう。


LikeButton.type.js

export const size = [

'small',
'medium'
];

例えばオブジェクトの例で言うと、ユーザー情報を表示するAccountInfo.vueがあるとすると

./AccountInfo.vue

./AccountInfo.type.js

このような構成とした上で、別途ユーティリティとして別のファイルに以下のような検査用のものを用意しておき


ObjectTypeCheck.js

export function hasKeys(obj, keys) {

return !keys.find((key)=>(!obj.hasOwnProperty(key)))
}

以下のようにTypeとpropsを書くというのも便利でしょう。


AccountInfo.type.js

export const accountKeys = [

'id',
'name',
'bio'
];


AccountInfo.vue

import { hasKeys } from 'path/to/ObjectTypeCheck';

import { accountKeys } from './AccountInfo.type';
export default {
props: {
account: {
validator(val) {
return hasKeys(val, accountKeys);
}
}
}
}

こうしておくことで、明確にrequiredなキーを切り出すことができ、拡張もしやすく、また、変更がひと目で分かるようにもなります。

場合によっては利用を検討することをオススメします。


参考

本記事の内容についての補完は以下にて行うことが可能となっています。