1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OSS紹介Advent Calendar 2017

Day 15

validatable-recordでVue.jsのフォームバリデーションをカイゼンしよう

Last updated at Posted at 2017-12-14

まえおき

SPAを作っていると、入力フォームを持つコンポーネントはわりかし頻繁に作る機会がありますよね :joy:

フロントエンドのバリデーションというのは、大抵の場合それっぽいnpmを使ったりして済ませてしまいがちな部分がありますが、実際にはアプリケーション・アーキテクチャにおいてバリデーションというのは必ずしも軽視できないものであることが多いです。特に大規模なアプリケーションのフォームであれば、バリデーションのロジックがビューに絡みついてビューレイヤが肥大化していくことがよくあります。

ということで、今回はVue.jsのコンポーネント実装を例にとって、どのようにしてバリデーション・ロジックをコンポーネントに粗結合な形で実装すればよいかを、validatable-recordというOSSの利用例とともに考えてみます1

実装例

実際に動くコードはこちらのURLから
https://www.webpackbin.com/bins/-L-kLd1mknBzNr67zNxW

HTML

HTMLは、このような構造のフォームを作ります。

Selection_043.png

index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <style>
      [v-cloak] { display: none; }
      .field { margin: 10px 10px; }
      .error { color: red; }
    </style>
  </head>
  <body>
    <div id="app" v-cloak>
      <form @submit.prevent="submit">
        <div class="field error">
          <p v-if="errors">{{ errors[0] }}</p>
        </div>
        <div class="field">
          <input type="text" v-model="form.name" placeholder="Your name" />
        </div>
        <div class="field">
          <input type="number" v-model="form.age" placeholder="Your age" />
        </div>
        <div class="field">
          <button type="submit">Submit</button>
         </div>
      </form>
    </div>
    <script src="main.js"></script>
  </body>
</html>

フォームは@submit.preventとすることで、エンターキーでの送信処理をブロックし、Vueコンポーネントへ移譲します。

JavaScript

JavaScript側ではvalidatable-recordを使ったVueコンポーネントの実装を行っていきます。

validatable-recordとは

今回使うvalidatable-recordは簡単に言うと、immutable.jsのRecordvalidate.jsを足したライブラリで、イミュータブルオブジェクトへの属性の定義とバリデーションルールの設定を行えるものです。

利用例
// ベースとなる属性の構造とバリデーションルールを定義
const ManRecord = ValidatableRecord({
  name: null,
  age: null
}, {
  name: {
    presence: true
  },
  age: {
    presence: {
      message: "is invalid"
    }
  }
});

// 上で定義したRecordを継承するクラスを定義
class Man extends ManRecord {
  ...
}

const man = new Man({
  name: "Justine";
  age: 25
});

man.validate() // == true

const agelessMan = new Man({
  name: "Michael"
});

agelessMan.validate() // == false

agelessMan.getErrors() // == [ "Age is invalid" ]

実際のVueコンポーネントでの使用例

まずは、ValidatableRecordの定義を継承したクラスを作ります。

ここでnameageなどのフォームの属性の定義や、そのバリデーション・ルール、そしてそれらのデータを使った振る舞い(submitなどのようなインスタンスメソッド)を定義します。

フォームクラス
class Login extends ValidatableRecord({
  name: null,
  age: null
}, {
  name: { 
    presence: true,
    length: {
      maximum: 20
    }
  },
  age: { 
    presence: true,
    numericality: {
      greaterThanOrEqualTo: 0
    }
  }
}) {
  submit() {
    //
    // Ex: インスタンスのデータを使ってWebAPIをコールしたりする。
    //
  }
}

コンポーネント自体にはバリデーションのルールなどを具体的に定義せず、単にLoginクラスを使った実装のみにすることで、バリデーションのロジックをコンポーネント(ビューレイヤ)から粗結合にすることができます。

フォームコンポーネント
new Vue({
  el: '#app',
  
  data: {
    errors: null,
    name: null,
    age: null
  },
  
  methods: {
    submit() {
      // フォームのデータをもとにフォームモデルのインスタンスを作る
      const login = new Login({
        name: this.name,
        age: this.age
      });
      
      // バリデーションを行い、エラーがあればerrorsにセットし、submitせずreturnする
      if (!login.validate()) {
         this.errors = login.getErrors();
         return
      }
      
      // エラーがなければsubmitを実行する
      login.submit();
    }
  }
})

ちなみに、動かしてみるとこのようなかたちになります。

Peek 2017-12-11 15-20.gif

ご覧のように、AgeNameをそれぞれバリデーションし、どちらかが空白の場合にはフォームの上に、対応するエラーの文言が出ていることがわかりますね。

また、この実装の最も重要な点はビューレイヤ(VueコンポーネントやHTML)の中にバリデーションロジックの記述がないというところです。コンポーネントの実装はバリデーションロジックの実装によってFatにならず、またテストの際もバリデーション・ロジックの変更とビューの変更は別のものになり、Loginクラスが単体でテストをすれば良いことになります。

この実装のなにがいいの?

「なんか面倒くさそうだし、わざわざvalidatable-recordなんて使わなくても、コンポーネントの中でfilterを使って入力のタイミングでバリデーションしたり、computedを使った実装のほうがいいんじゃない?」

ふむ、たしかにこんな声も聞こえてきそうですね :thinking:

ビューとバリデーションの関係を考える

今回の実装の最も重要な目的は、validatable-recordを使うことではなく、ビューレイヤから、アプリケーションに固有のルール(ドメイン・ロジック)としてのバリデーションを切り離すことで、変化に強いアプリケーションへとカイゼンする、というものです。

例えば、今回触れたVue.jsは、フレームワークの性質上Vueコンポーネントがデフォルトでfiltercomputedなどの様々なビューに関連する機能を提供しており、便利な反面、スケーラビリティを意識せず実装を進めてしまうと、どうしてもビューレイヤとしてのVueコンポーネントが膨らんでしまいがち2になります。

とりわけ、SPAなどのフロントエンド・アプリケーションは、ビューレイヤというデザイン要件の変更などの影響を強く受けやすい部分と近いため、極端に言うとビューはデータを表示するという責務のみを持ち、できるだけ複雑なif分岐がビューレイヤには現れないように意識をすることが大事です :muscle:

ドメインとバリデーション

Ex) 「選択できる商品は3つまで」
Ex) 「総額3000円になるように選択必須」

たとえば、ECサイトを作っていて現れる上のような条件は、確実にドメイン・ロジックの一部です。なぜなら、これらはそのアプリケーションに固有の条件だからです。そのバリデーションが、あなたのアプリケーションによって行われるビジネスによって生まれるものかどうかを考えると、自ずとそのバリデーションがドメインに属するものかどうか? という点が見えてきます :eye:

バリデーションがドメイン・ロジックの一部に属するものであるということが分かれば、そのバリデーション・ルールがHTMLやVueコンポーネントの中に記述されていてはいるべきではない、ということも分かります。なぜなら、ビューレイヤの変更によって意図しない影響を受ける可能性が生まれるからです。アプリケーションの中核としての「ドメイン」を関係のないレイヤから切り離して守ることで、保守・運用のしやすい変化に強いアプリケーションにすることができます :ok_hand:

実装と設計とのトレードオフ

とはいえ、このような実装をするかしないか、という点は完全にアプリケーションを開発するチームや組織に依存します。フレームワークによっても、その実装パターンは多種多様で、必ずしもバリデーションがビューにおかれるべき責務ではないと必ずしも言いきれはしません。それでも、今回のvalidatable-recordを用いたVueコンポーネントのカイゼンパターンが何らかの手助けになれば幸いです。

  1. 元ネタはこのスライドです https://speakerdeck.com/izumisy/hurontoendobaridesiyon

  2. このような観点では、Reactのようなビューのコンポーネント化のみに特化している「薄い」ビューレイヤのフレームワークには利点があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?