Help us understand the problem. What is going on with this article?

[Vue.js 2.x] 非コンポーネントvsコンポーネントでの登録フォームの実装

More than 1 year has passed since last update.

前提

Vue.jsは使っているが、コンポーネントを使った実装をまだしていない人、少しし始めた人向けです。

この記事のねらい

コンポーネント使わずとも、Vueインスタンスを作成し、dataオブジェクトに必要なプロパティを追加していって、、1つのJSファイルで問題なく実装できるよね。
コンポーネントする意味って何なの?(´・ω・`)という状態から、この記事を読んで貰った後には、コンポーネント化したほうがよさそう!と思ってもらえば幸いです。

コンポーネント化するメリット

メンテナンスしやすい

追加修正が発生した場合に該当箇所が見つけやすい
必要なファイルのみ修正を行えばよい。
ロジック部分、非ロジックのHTML部分で切り分けができる

複数人で作業する場合、分業しやすい

同時に作業しても修正箇所がかぶらないのでコンフリクトの発生が避けられる

可読性が高い

Vue.jsを使った実装はコンポーネントを使ったものが一般的と思うので、パターンを掴むと読み解きやすい。
スタイルガイドに沿った実装だと更に読みやすい。

メリットを実感するために実装してみる

とは言ってもふ~んという感じで実際に使ってみないとよくわかりません。
コンポーネントのよいとこを実感するために、簡単なサンプルアプリを2つのアプローチで作っていきます。
コンポーネントを使わない実装と、コンポーネントを使った実装をしてみます。

サンプルは、一般的なWEB上の登録フォームにします。
コンポーネントの有用性を検証するため、登録フォームの内容は選択式の質問でなく、テキスト入力するものを主に使います。

こうゆうの

スクリーンショット 2017-12-11 13.24.49.png

サンプルアプリの要件

これらが入力出来るフォームを作ります。

項目名 入力形式 入力例
フリガナ(セイ) 全角カタカナ
フリガナ(メイ) 全角カタカナ
郵便番号1 3桁の半角数字
郵便番号2 4桁の半角数字
都道府県 例:東京都
住所1 例:中央区新川1-2-3
住所2 例:Xビル5F
  • 入力形式はすべてテキストフィールド
  • このサンプルではデータベースへの登録は行いません
  • 各項目は全部必須項目という前提

Vueに関する処理

  • 入力のバリデーションが各項目別に行える
  • 各項目は全部必須項目という前提なので、全部の項目が入り、バリデーションをクリアしている場合にのみ登録ボタンが押せるようになる

では、実装してきましょう!

サンプルアプリ

こちら

1. コンポーネントを使わずに実装する

環境について

Ruby on Rails5系でベースの環境を用意し、http://localhost:3000/registrations でアクセスできるページを作りました。

JavaScriptファイルの構成

./app/assets/javascripts/
├── application.js
├── cable.js
├── channels
├── registrations
│   ├── form.js # コンポーネントなしver.用のJavaScriptファイル
└── vue.js      # 公式サイトのソースを配置

Viewの構成

./app/views/registrations/
├── _form.html.erb # コンポーネントなしver.用のパーシャル
└── index.html.erb

登録フォーム画面を描画

input要素を置いて、入力フォームを用意します。

app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="田中">
      <input type="text" class="form-control" placeholder="太郎">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="タナカ">
      <input type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="111">
      -
      <input type="text" class="form-control" placeholder="2222">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input type="text" class="form-control" placeholder="東京都">
    <input type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input type="text" class="form-control" placeholder="Xビル5F">
    <span class="text-danger">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>

現時点でこのような画面が描画されています

スクリーンショット 2017-12-06 13.24.25.png

Vueインスタンスを作成し、入力値を取得

dataオブジェクトに、各項目名のプロパティを定義します。
今回オブジェクトを用意し、名前に関する情報はuser、住所に関するデータはaddressオブジェクトに格納するように定義しました。

app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    }
  });
});

それに合わせて、HTML側にv-modelを追加します。これにより、各項目の入力値が参照できるようになりました。

app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input v-model="user.familyName" type="text" class="form-control" placeholder="田中">
      <input v-model="user.firstName" type="text" class="form-control" placeholder="太郎">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input v-model="user.familyNameKana" type="text" class="form-control" placeholder="タナカ">
      <input v-model="user.firstNameKana" type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input v-model="address.zipCode3digit" type="text" class="form-control" placeholder="111">
      -
      <input v-model="address.zipCode4digit" type="text" class="form-control" placeholder="2222">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input v-model="address.prefecture" type="text" class="form-control" placeholder="東京都">
    <input v-model="address.city" type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input v-model="address.buildingName" type="text" class="form-control" placeholder="Xビル5F">
    <span class="text-danger">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>

Vue.jsのdevtoolsで確認するとオブジェクトの中身が確認できました。
スクリーンショット 2017-12-11 13.32.17.png

各項目のバリデーションを実装する

下記の要件に着手していきます。

- 入力のバリデーションが各項目別に行える
- 各項目は全部必須項目という前提なので、全部の項目が入り、バリデーションをクリアしている場合にのみ登録ボタンが押せるようになる

バリデーション実行の概要図

スクリーンショット 2017-12-12 15.12.59.png

まず、各項目のバリデーションを行えるようにしていきます。

算出プロパティ computedを使い、入力値が変わる度にバリデーションを実行するようにします。

app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    },
    computed: {
      validation: function () {
        var user = this.user;
        var address = this.address;
        var kanaPattern = /^[\u30a0-\u30ff]+$/;
        return {
          name: user.familyName.trim() && user.firstName.trim(),
          furigana: kanaPattern.test(user.familyNameKana.trim()) && kanaPattern.test(user.firstNameKana.trim()),
          zipCode: this.validationZipcode,
          address: address.prefecture.trim() && address.city.trim() && address.buildingName.trim()
        };
      },
      validationZipcode: function () {
        var zipCode3 = this.address.zipCode3digit.trim();
        var zipCode4 = this.address.zipCode4digit.trim();
        var p = /^\d+$/;
        return (p.test(zipCode3) && p.test(zipCode4)) && (zipCode3.length === 3 && zipCode4.length === 4);
      }
    }
  });
});

computedの validationでバリデーションを行っています。

項目名 検証内容
姓名 入力値があればよいので、入力された値からtrimメソッドで前後の空白を省いた値を返します。
住所 同上
フリガナ 正規表現の全角カタカナパターンを使い、testメソッドで検証。その結果(true/false)を返します。
郵便番号 バリデーションが少し複雑だったので、validationZipcodeという別のcomputed値にし、半角数字であること&桁数を検証しています。

このvalidationを実行した結果によって、HTML側でエラーメッセージの表示をv-showを使って制御します。

app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input v-model="user.familyName" type="text" class="form-control" placeholder="田中">
      <input v-model="user.firstName" type="text" class="form-control" placeholder="太郎">
      <span class="text-danger" v-show="!validation.name">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input v-model="user.familyNameKana" type="text" class="form-control" placeholder="タナカ">
      <input v-model="user.firstNameKana" type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger" v-show="!validation.furigana">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input v-model="address.zipCode3digit" type="text" class="form-control" placeholder="111">
      -
      <input v-model="address.zipCode4digit" type="text" class="form-control" placeholder="2222">
      <span class="text-danger" v-show="!validation.zipCode">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input v-model="address.prefecture" type="text" class="form-control" placeholder="東京都">
    <input v-model="address.city" type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input v-model="address.buildingName" type="text" class="form-control" placeholder="Xビル5F">
    <span class="text-danger" v-show="!validation.address">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>

これでバリデーションエラーの場合のみメッセージが表示されるようになりました。

全てのバリデーションがOKの場合のみ登録ボタンが押せるようにする

全項目のバリデーション結果を取得し、検証するためにcomputed値isValidを定義する。
JavaScriptのeveryメソッドを使ってvalidationの返り値の全ての値がtrueであることを検証する。

app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    },
    computed: {
      isValid: function () {
        var validation = this.validation;
        return Object.keys(validation).every(function (key) {
          return validation[key];
        });
      },
      validation: function () {
        var user = this.user;
        var address = this.address;
        var kanaPattern = /^[\u30a0-\u30ff]+$/;
        return {
          name: user.familyName.trim() && user.firstName.trim(),
          furigana: kanaPattern.test(user.familyNameKana.trim()) && kanaPattern.test(user.firstNameKana.trim()),
          zipCode: this.validationZipcode,
          address: address.prefecture.trim() && address.city.trim() && address.buildingName.trim()
        };
      },
      validationZipcode: function () {
        var zipCode3 = this.address.zipCode3digit.trim();
        var zipCode4 = this.address.zipCode4digit.trim();
        var p = /^\d+$/;
        return (p.test(zipCode3) && p.test(zipCode4)) && (zipCode3.length === 3 && zipCode4.length === 4);
      }
    }
  });
});

HTML側ではisValidの結果を元に、登録ボタンにclassを付与するかどうかを制御する。Bootstrapのbuttonをdisabledの状態にする

<!-- ボタン部分のみ抜粋 -->

  <button type="button" :disabled="!isValid" class="btn btn-info">Submit</button>

ここまで実装すると、全てのバリデーションをクリアした場合のみ登録ボタンが押下できるようになりました!

ここまでの感想

実装したコードを眺めてみてどうでしょうか。
Vueインスタンスのdataオブジェクトが中々のボリュームになっています!
実際の登録画面を想定すると、項目数がもっと増え、入力形式も多様になるかと思います。
その時を想定すると、、dataの管理だけでしんどそうですね。

また、その際にはcomputedオブジェクトのvalidationのreturn値のキーと値の数も増えます。
そのことを考えると、この実装を実案件でつかうのはキビシそうですね。。(´・ω・`)

おさらい

ここまではVue.jsのこのあたりのものを使いました。

ここからは

今から実装するコンポーネント使用ver.でも上記の仕組みを使います。
それに加え、Vueインスタンス(親)とコンポーネント(子)の関係が出てきます。
この親子間でデータの通知をするという考え方が全てのベースになってきます。

Vue では、親子のコンポーネントの関係は、props down, events up というように要約することができます。
親は、 プロパティを経由して、データを子に伝え、子はイベントを経由して、親にメッセージを送ります。

props-events.png

公式より引用

2. コンポーネントを使って実装する

ここまで作ったコンポーネントなしver.のファイル郡に追加して、下記のファイルを作成します。

JavaScriptファイルの構成

./app/assets/javascripts/registrations/
├── components
│   ├── formAppInputText.js    # コンポーネント使用ver.のコンポーネント(郵便番号以外の項目用)
│   └── formAppInputZipCode.js # コンポーネント使用ver.のコンポーネント(郵便番号用)
├── form.js
└── formApp.js                  # コンポーネント使用ver.のRootインスタンス

Viewの構成

./app/views/registrations/
├── _form.html.erb
├── _form_app.html.erb # コンポーネント使用ver.のパーシャル
└── index.html.erb

コンポーネントの構成図

以下の各四角の単位でコンポーネント化を行います。
コンポーネントは用途を考えて2種類作りました。
スクリーンショット 2017-12-12 15.39.01.png

登録フォーム画面を描画

HTML側

app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text></form-app-input-text>
      <form-app-input-text></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text></form-app-input-text>
      <form-app-input-text></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code></input-zip-code>
      -
      <input-zip-code></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text></form-app-input-text>
    <form-app-input-text></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>

JavaScript側

親となるRootインスタンスを作成

app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app'
  });
});

コンポーネントを作成

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" class="form-control"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>'
});
app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" class="form-control"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>'
});

現時点でこのような画面が描画されています

スクリーンショット 2017-12-07 11.10.23.png

プレースホルダーを表示する

HTMLのinput属性にplaceholder属性(↓)を追加します。
スクリーンショット_2017-12-07_11_55_55.png

HTML側で、コンポーネントタグのplaceholderプロパティにデータをセット。

app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :placeholder="'Xビル5F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>

コンポーネント側では、propsオブジェクトにplaceholderプロパティを定義。(HTML側でコンポーネントタグに定義されれいるプロパティと同じ名称にすること)
親側で設定したplaceholderプロパティの文字列データが受け取れるので、そのデータをHTMLのinput要素のplaceholder属性に使用する。

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>',
  props: {
    placeholder: String
  }
});
app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    placeholder: String
  }
});

項目名と入力値を設定・取得する

各項目のバリデーションを実装していくために、まずは、コンポーネント化している項目の名称と値の設定・取得ができるようにします。
propscolumnプロパティを通じて、HTML側でセットした項目名をコンポーネント側で取得できるようにします。

app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :column="'city'" :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :placeholder="'Xビル5F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>

また、入力値を取得できるようにinputタグにv-modelを追加します。

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  }
});

こちらも同じくv-modelを追加。(郵便番号専用のコンポーネントなので、v-model名はzipCodeと設定)

app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  }
});

ここまでの実装をdevtoolsで確認すると、項目名と入力値を取得できているのが確認できました。


スクリーンショット_2017-12-08_10_53_45.png

各項目のバリデーションを実装する

バリデーション実行の概要図

各項目のバリデーションの実装部分はコンポーネントなしver.とほぼ同じですが、全項目の実装部分が違ってきます。

主に赤文字、実線矢印がコンポーネント特有の処理になります。
スクリーンショット 2017-12-12 15.24.30.png

氏名・住所

まずは、バリデーションについて入力値があればOKな実装を行います。
コンポーネントを使用しないver.と同じく、computedを使い、入力している値が変わる度にバリデーションを実行するようにします。

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      return this.inputValue.trim();
    }
  }
});

これで、郵便番号以外の項目は入力があればエラーメッセージは表示されないようになりました。

フリガナ

次にフリガナを実装します。入力値は全角カタカナのみ受け入れます。

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      return kanaPattern.test(this.inputValue.trim());
    }
  }
});

フリガナも他項目と共通のコンポーネントを使っているので、フリガナの場合(column値にKanaという文字列が含まれる場合)は、フリガナ用に定義したvalidationKana
computedを実行するようにv-ifディレクティブを使い実装しています。

郵便番号

郵便番号は、半角数字3桁と4桁のみ受け入れるように実装します。

app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationZipCode">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      return (pattern.test(zipCode) && zipCode.length === digit);
    }
  }
});

ここまでで、各項目のコンポーネント単位でのバリデーションの実装はできました!

次の全項目のバリデーション結果を見て登録ボタンの制御を行うには、コンポーネント特有の実装が必要になります。

全てのバリデーションがOKの場合のみ登録ボタンが押せるようにする

各項目のバリデーション結果を参照する

各項目のバリデーション結果を参照するために、オブジェクト形式で返り値を返すようにしていきます。
返り値はこのような連想配列のイメージです。

{
  familyName: "鈴木",    # 入力値をtrim()した値が入る
  firstName: "一郎",
  familyNameKana: true,  # testメソッド実行結果がBoolean型で入る
  familyNameKana: true
} 

結果格納用のオブジェクトuserをVueインスタンス側で作成し、コンポーネント側ではデータが受け取れるようにpropsのプロパティに追加します。
HTML側で、userプロパティにデータをセットします。今回は今までのcolumnなどとは異なり、文字列でなくVueインスタンスで作成したuserオブジェクトを値として与えます。

app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :user="user" :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :user="user" :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :user="user" :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :user="user" :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :user="user" :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :user="user" :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :user="user" :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :column="'city'" :user="user" :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :user="user" :placeholder="'Xビル5F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>
app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app',
    data: {
      user: {
        type: Object
      }
    }
  });
});
app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      this.user[this.column] = this.inputValue.trim();
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      var result = kanaPattern.test(this.inputValue.trim());
      this.user[this.column] = result;
      return result;
    }
  }
});
app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationZipCode">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      var result = (pattern.test(zipCode) && zipCode.length === digit);
      this.user[this.column] = result;
      return result;
    }
  }
});

ここまでの実装で、userオブジェクトに各項目のバリデーション結果を格納できていることが確認できます。


スクリーンショット 2017-12-08 14.10.36.png

結果を親へ通知し、ボタンを制御する

コンポーネント側では、$emitを使って親側に通知する処理を追加。引数としてuserオブジェクトを与える。

app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      this.user[this.column] = this.inputValue.trim();
      this.$emit('update-status-from-child', this.user);
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      var result = kanaPattern.test(this.inputValue.trim());
      this.user[this.column] = result;
      this.$emit('update-status-from-child', this.user);
      return result;
    }
  }
});

郵便番号のコンポーネントも同様に通知処理を追加。

app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder">' +
    '<span class="text-danger" v-show="!validationZipCode">半角数字・規定の桁数で入力してください</span>' +
    '</div>',
  props: {
    column: String,
    placeholder: String,
    user: Object
  },
  data: function () {
    return {
      zipCode: ''
    };
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      var result = (pattern.test(zipCode) && zipCode.length === digit);
      this.user[this.column] = result;
      this.$emit('update-status-from-child', this.user);
      return result;
    }
  }
});

親側のVueインスタンスでは子からの通知を受けて、実行するメソッドを定義。
このメソッドでは全ての項目がクリアしているかを見て変数statusに結果を格納しています。

app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app',
    data: {
      user: {
        type: Object
      }
    },
    methods: {
      updateStatus: function () {
        var _this = this;
        _this.status = Object.keys(_this.user).every(function (key) {
          return _this.user[key];
        });
      }
    }
  });
});

HTML側は、カスタムイベントupdate-status-from-childが呼ばれたら、updateStatusメソッドを実行するように全てのコンポーネントタグに追加しています。

app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :user="user" :placeholder="'田中'" @update-status-from-child="updateStatus"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :user="user" :placeholder="'太郎'" @update-status-from-child="updateStatus"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :user="user" :placeholder="'タナカ'" @update-status-from-child="updateStatus"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :user="user" :placeholder="'タロウ'" @update-status-from-child="updateStatus"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :user="user" :placeholder="'111'" @update-status-from-child="updateStatus"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :user="user" :placeholder="'2222'" @update-status-from-child="updateStatus"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :user="user" :placeholder="'東京都'" @update-status-from-child="updateStatus"></form-app-input-text>
    <form-app-input-text :column="'city'" :user="user" :placeholder="'中央区新川1-2-3'" @update-status-from-child="updateStatus"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :user="user" :placeholder="'Xビル5F'" @update-status-from-child="updateStatus"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info" :disabled="!status">Submit</button>
</div>

これで登録ボタンの制御までできるようになりました!

まとめ

比較しながらコンポーネントの実装をすることで、先に上げたような良いことをなんとなくでも実感できたでしょうか。

さらにこの登録フォームに、、

  • この項目を削って
  • 項目の並び順を変えて
  • スタイルをもっとかっこよくして
  • 郵便番号をいれたら住所が表示されるようにして

などの修正依頼が来たらどうでしょう。コンポーネント化しておくことで、該当の箇所が見つけやすく、ひとりで頑張らなくても分業しやすそうですね。
後で苦労する前にこれを機にコンポーネント化を進めていきましょう〜。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした