前提
Vue.jsは使っているが、コンポーネントを使った実装をまだしていない人、少しし始めた人向けです。
この記事のねらい
コンポーネント使わずとも、Vueインスタンスを作成し、data
オブジェクトに必要なプロパティを追加していって、、1つのJSファイルで問題なく実装できるよね。
コンポーネントする意味って何なの?(´・ω・`)という状態から、この記事を読んで貰った後には、**コンポーネント化したほうがよさそう!**と思ってもらえば幸いです。
コンポーネント化するメリット
メンテナンスしやすい
追加修正が発生した場合に該当箇所が見つけやすい。
必要なファイルのみ修正を行えばよい。
ロジック部分、非ロジックのHTML部分で切り分けができる
複数人で作業する場合、分業しやすい
同時に作業しても修正箇所がかぶらないのでコンフリクトの発生が避けられる。
可読性が高い
Vue.jsを使った実装はコンポーネントを使ったものが一般的と思うので、パターンを掴むと読み解きやすい。
スタイルガイドに沿った実装だと更に読みやすい。
メリットを実感するために実装してみる
とは言ってもふ~んという感じで実際に使ってみないとよくわかりません。
コンポーネントのよいとこを実感するために、簡単なサンプルアプリを2つのアプローチで作っていきます。
コンポーネントを使わない実装と、コンポーネントを使った実装をしてみます。
サンプルは、一般的なWEB上の登録フォームにします。
コンポーネントの有用性を検証するため、登録フォームの内容は選択式の質問でなく、テキスト入力するものを主に使います。
こうゆうの
サンプルアプリの要件
これらが入力出来るフォームを作ります。
項目名 | 入力形式 | 入力例 |
---|---|---|
姓 | ||
名 | ||
フリガナ(セイ) | 全角カタカナ | |
フリガナ(メイ) | 全角カタカナ | |
郵便番号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要素を置いて、入力フォームを用意します。
<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>
現時点でこのような画面が描画されています
Vueインスタンスを作成し、入力値を取得
data
オブジェクトに、各項目名のプロパティを定義します。
今回オブジェクトを用意し、名前に関する情報はuser
、住所に関するデータはaddress
オブジェクトに格納するように定義しました。
$(function () {
new Vue({
el: '#registrations',
data: {
user: {
familyName: '',
firstName: '',
familyNameKana: '',
firstNameKana: ''
},
address: {
zipCode3digit: '',
zipCode4digit: '',
prefecture: '',
city: '',
buildingName: ''
}
}
});
});
それに合わせて、HTML側にv-model
を追加します。これにより、各項目の入力値が参照できるようになりました。
<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で確認するとオブジェクトの中身が確認できました。
各項目のバリデーションを実装する
下記の要件に着手していきます。
- 入力のバリデーションが各項目別に行える
- 各項目は全部必須項目という前提なので、全部の項目が入り、バリデーションをクリアしている場合にのみ登録ボタンが押せるようになる
バリデーション実行の概要図
まず、各項目のバリデーションを行えるようにしていきます。
算出プロパティ computedを使い、入力値が変わる度にバリデーションを実行するようにします。
$(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
を使って制御します。
<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であることを検証する。
$(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のこのあたりのものを使いました。
-
v-model
フォームの要素の入力値を取得する -
v-show
バリデーションのエラーメッセージを表示する -
computed
バリデーションを定義する -
クラスバインディング
登録ボタンにクラスを割り当てる
ここからは
今から実装するコンポーネント使用ver.でも上記の仕組みを使います。
それに加え、Vueインスタンス(親)とコンポーネント(子)の関係が出てきます。
この親子間でデータの通知をするという考え方が全てのベースになってきます。
Vue では、親子のコンポーネントの関係は、props down, events up というように要約することができます。
親は、 プロパティを経由して、データを子に伝え、子はイベントを経由して、親にメッセージを送ります。
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種類作りました。
登録フォーム画面を描画
HTML側
<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インスタンスを作成
$(function () {
new Vue({
el: '#registrations-app'
});
});
コンポーネントを作成
Vue.component('form-app-input-text', {
template: '<div>' +
'<div><input type="text" class="form-control"></div>' +
'<small class="text-danger">入力してください</small>' +
'</div>'
});
Vue.component('input-zip-code', {
template: '<div>' +
'<div><input type="text" class="form-control"></div>' +
'<small class="text-danger">正しい形式で入力してください</small>' +
'</div>'
});
現時点でこのような画面が描画されています
プレースホルダーを表示する
HTMLのinput属性にplaceholder属性(↓)を追加します。
HTML側で、コンポーネントタグのplaceholderプロパティ
にデータをセット。
<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属性に使用する。
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
}
});
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
}
});
項目名と入力値を設定・取得する
各項目のバリデーションを実装していくために、まずは、コンポーネント化している項目の名称と値の設定・取得ができるようにします。
props
のcolumn
プロパティを通じて、HTML側でセットした項目名をコンポーネント側で取得できるようにします。
<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
を追加します。
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と設定)
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で確認すると、項目名と入力値を取得できているのが確認できました。
各項目のバリデーションを実装する
バリデーション実行の概要図
各項目のバリデーションの実装部分はコンポーネントなしver.とほぼ同じですが、全項目の実装部分が違ってきます。
氏名・住所
まずは、バリデーションについて入力値があればOKな実装を行います。
コンポーネントを使用しないver.と同じく、computed
を使い、入力している値が変わる度にバリデーションを実行するようにします。
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();
}
}
});
これで、郵便番号以外の項目は入力があればエラーメッセージは表示されないようになりました。
フリガナ
次にフリガナを実装します。入力値は全角カタカナのみ受け入れます。
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桁のみ受け入れるように実装します。
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オブジェクトを値として与えます。
<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>
$(function () {
new Vue({
el: '#registrations-app',
data: {
user: {
type: Object
}
}
});
});
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;
}
}
});
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オブジェクトに各項目のバリデーション結果を格納できていることが確認できます。
結果を親へ通知し、ボタンを制御する
コンポーネント側では、$emit
を使って親側に通知する処理を追加。引数としてuserオブジェクトを与える。
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;
}
}
});
郵便番号のコンポーネントも同様に通知処理を追加。
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
に結果を格納しています。
$(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
メソッドを実行するように全てのコンポーネントタグに追加しています。
<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>
これで登録ボタンの制御までできるようになりました!
まとめ
比較しながらコンポーネントの実装をすることで、先に上げたような良いことをなんとなくでも実感できたでしょうか。
さらにこの登録フォームに、、
- この項目を削って
- 項目の並び順を変えて
- スタイルをもっとかっこよくして
- 郵便番号をいれたら住所が表示されるようにして
などの修正依頼が来たらどうでしょう。コンポーネント化しておくことで、該当の箇所が見つけやすく、ひとりで頑張らなくても分業しやすそうですね。
後で苦労する前にこれを機にコンポーネント化を進めていきましょう〜。