はじめに
この記事はVue#2アドベントカレンダーの23日目です🎅
アドベントカレンダー初参加の@yakumomutsukiです。
当初は「vue-cliを使わなくてもできるvueを使った開発」を考えていたのですが、書いているうちにただのWebpackの説明になってしまったので、実務であるあるなお悩みを書いてみようと思いました。
バリデーション + 入力フォーム
フロントエンドの開発において、フォーム周りのバリデーションの設計は比較的悩みどころかなあと思います。zipというプロパティにたいして、watchを使ってバリデーションを張るか、computedを使うか、このあたりは実装者によってさまざまあると思います。
さまざまあるなかでの「私はこう実装した」という一例をご覧ください。
<template>
<div id="app">
<div class="container">
<form>
<div class="form-row">
<div class="form-group col-md-6">
<label for="email">メールアドレス(必須)</label>
<input
id="email"
required
type="email"
class="form-control"
:class="formError.email.className"
name="email"
placeholder="メールアドレスを入力してください"
@change="e => update(e)"
:value="formState.email"
/>
<span>{{ formError.email.errorMessage }}</span>
</div>
<div class="form-group col-md-6">
<label for="password">パスワード(必須)</label>
<input
id="password"
required
type="password"
class="form-control"
:class="formError.password.className"
name="password"
placeholder="パスワード"
@change="e => update(e)"
:value="formState.password"
/>
<span>{{ formError.password.errorMessage }}</span>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="zip">郵便番号(必須)</label>
<input
id="zip"
required
type="text"
class="form-control"
:class="formError.zip.className"
name="zip"
placeholder="1000000"
maxlen="7"
@change="e => update(e)"
:value="formState.zip"
/>
<p>{{ formError.zip.errorMessage }}</p>
</div>
<div class="form-group col-md-4">
<label for="state">都道府県(必須)</label>
<select
id="state"
class="form-control"
:class="formError.state.className"
:value="formState.state"
@change="e => update(e)"
required
>
<option value="" selected>選択してください</option>
<option value="tokyo">東京</option>
<option value="osaka">大阪</option>
</select>
<p>{{ formError.state.errorMessage }}</p>
</div>
<div class="form-group col-md-4">
<label for="city">市区町村(必須)</label>
<input
id="city"
required
type="text"
class="form-control"
:class="formError.city.className"
name="city"
placeholder="千代田区"
@change="e => update(e)"
:value="formState.city"
/>
<p>{{ formError.city.errorMessage }}</p>
</div>
</div>
<div class="form-group">
<label for="address1">住所1(必須)</label>
<input
id="address1"
required
type="text"
class="form-control"
:class="formError.address1.className"
name="address1"
placeholder="秋葉原1-1-1"
@change="e => update(e)"
:value="formState.address1"
/>
<p>{{ formError.address1.errorMessage }}</p>
</div>
<div class="form-group">
<label for="address2">住所2</label>
<input
id="address2"
type="text"
class="form-control"
name="address2"
placeholder="秋葉原ビル1F"
@change="e => update(e)"
:value="formState.address2"
/>
</div>
<button class="btn btn-primary" @click="registration">登録する</button>
</form>
</div>
</div>
</template>
<script>
const errorBase = {
errorMessage: "",
className: ""
};
const patterns = {
password: /^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/,
zip: /^[0-9]{7}$/,
phoneNumber: /^[0-9]{10,11}$/
};
import isEmail from "validator/lib/isEmail";
import isMatch from "validator/lib/matches";
export default {
name: "App",
data() {
return {
formState: {
email: "",
password: "",
zip: "",
state: "",
city: "",
address1: "",
address2: ""
},
formError: {
email: { ...errorBase },
password: { ...errorBase },
zip: { ...errorBase },
state: { ...errorBase },
city: { ...errorBase },
address1: { ...errorBase },
address2: { ...errorBase }
}
};
},
methods: {
/**
* 値が更新されたらupdateメソッドが呼ばれる
* formStateの値を更新したら、バリデーションを実行
* @param e HTMLElement
*/
update(e) {
const { name, value } = e.target;
this.formState[name] = value;
this.formError[name] = { ...errorBase };
this.validate(name);
},
/**
* バリデーションを行います
* 未入力チェックが必要な場合は、第2引数にfalseを指定する
* @param name
* @param allowBlank
*/
validate(name, allowBlank = true) {
const formVal = this.formState[name];
// 未入力チェックをする場合
if (!allowBlank && !formVal) {
this.formError[name] = {
errorMessage: "未入力です",
className: "error"
};
return;
}
// メールアドレス
if (name === "email" && formVal && !isEmail(formVal)) {
this.formError[name] = {
errorMessage: "メールアドレスに誤りがあります",
className: "error"
};
return;
}
// パスワード
if (
name === "password" &&
formVal &&
!isMatch(formVal, patterns.password)
) {
this.formError[name] = {
errorMessage: "パスワードに誤りがあります",
className: "error"
};
return;
}
// 郵便番号
if (name === "zip" && formVal && !isMatch(formVal, patterns.zip)) {
this.formError[name] = {
errorMessage: "郵便番号に誤りがあります",
className: "error"
};
}
},
/**
* 登録処理を行います
* バリデーションで引っかかった場合は、登録処理を行えません
*/
registration() {
for (let [key] of Object.entries(this.formState)) {
// バリデーションを行わないのはcontinueで抜けておく
if (key === "address2") {
continue;
}
// バリデーションを実行
this.validate(key, false);
}
let isValid = true;
for (let [key] of Object.entries(this.formError)) {
// エラーメッセージがない場合にはtrueになる
isValid = !this.formError[key].errorMessage ? isValid : false;
}
if (isValid) {
window.alert("登録に成功しました");
} else {
window.alert("入力不備があります");
}
}
}
};
</script>
<style>
.error {
background-color: #ffecec !important;
}
</style>
お気づきになりましたでしょうか。
このコードはwatchもcomputedも使っておりません。しかし、formStateとfromErrorの各プロパティのキー名を同じにしておくことで、updateメソッドからvalidateメソッドに同じキー名を渡して、バリデーションをかけることができています。
update(e) {
const { name, value } = e.target;
this.formState[name] = value;
this.formError[name] = { ...errorBase };
this.validate(name);
},
引数として受け取ったHTMLElementから、const { name, value } = e.target;
のように分割代入して、nameとvalueを受け取り、formStateに値を設定、fromErrorの初期化して、もう一度バリデーションという流れになっています。
おわりに
書いていて思ったのですが、別にVueじゃなくてJavaScriptのテクニック感集ですね...。v-modelやwatch、computedは使わなくてもバリデーションはできますというお話でした。
明日のアドベントカレンダーは@kacky917さんです!!🎄