会員登録ページは全4回に渡って構築します。
1/4 レイアウトファイルの作成
2/4 名前、メールアドレス、パスワードコンポーネントの作成
4/4 Vuetityのv-text-fieldを使ったバリデーション設定(今ここ)
今回達成すること
Vuetifyを使い、各フォームにバリデーションを設定します。
今回の完成イメージ
この回で会員登録ページが完成しますよー!
途中でつまづいた方
このページ下、#最終的なコード を置いていますので、コピペして次へ進んでください。
nameのバリデーションを追加する
名前の入力フォームには、
- 入力必須
- 30文字まで
2つのバリデーションを設定します。
Vuetifyの<v-text-field>
にバリデーションを設定するには、rules
キーに配列を渡します。
front/components/user/userFormName.vue
<template>
<!-- rulesキー 追加 -->
<v-text-field
v-model="setName"
:rules="rules"
label="ユーザー名を入力"
placeholder="あなたの表示名"
outlined
/>
</template>
<script>
export default {
...
// data () 追加
data () {
const max = 30
return {
max,
rules: [
v => !!v || '',
v => (!!v && max >= v.length) || `${max}文字以内で入力してください`
]
}
},
// ここまで
computed: {
...
</script>
-
rules: []
... バリデーションを格納した配列。1つ目は入力必須、2つ目は文字数制限となります。
-
v => !!v || ''
...v
はvalueの略で、ユーザーが入力した値が入ります。バリデーションの書き方はこんな感じで分解できます。
!!v
... バリデーション成功条件(valueが存在する)||
... そうでない場合''
... バリデーションのエラーメッセージ
ユーザーのストレスになるため、入力必須のバリデーションにはエラーメッセージ無しとしています。
-
v => (!!v && max >= v.length) || '${max}文字以内で入力してください'
...例えば上記の場合は、
!!v
... valueが存在する、&&
... かつmax >= v.length
... 文字数が30文字以下||
... でない場合、- 「30文字以内で入力してください」とメッセージを出す
という内容です。
文字カウンターを追加する
文字数要件がある場合は、「今何文字なのか、何文字まで入力できるのか」をユーザーに明記してあげると親切です。
これは、Vuetifyのcounter
キーに最大許容文字数を渡すことで実現します。
front/components/user/userFormName.vue
<template>
<!-- counterキー 追加 -->
<v-text-field
...
:counter="max"
label="ユーザー名を入力"
placeholder="あなたの表示名"
outlined
/>
</template>
...
コンテナを起動して、http://localhost:8080/signup にアクセスしてください。
名前を入力すると、カウントとエラーメッセージが表示されるようになりました。
nameのバリデーションはこれにて完了です。
emailのバリデーションを追加する
メールアドレスのバリデーションは、
- 入力必須
- メールの書式チェック
を行います。
front/components/user/userFormEmail.vue
<template>
<!-- rulesキー 追加 -->
<v-text-field
v-model="setEmail"
:rules="rules"
label="メールアドレスを入力"
placeholder="your@email.com"
outlined
/>
</template>
<script>
export default {
...
// data () 追加
data () {
return {
rules: [
v => !!v || '',
v => /.+@.+\..+/.test(v) || ''
]
}
},
// ここまで
computed: {
...
</script>
-
v => /.+@.+\..+/.test(v) || ''
... 正規表現でのメールアドレス書式チェック。JavaScriptの
test()
メソッドを使い、正しければtrue
を返します。この正規表現は、「
@
と.
が含まれているか」の簡易チェックとなります。
(コラム)書式チェックにエラーメッセージを表示させない理由
フロントサイドのバリデーションは、リアルタイムで行うためどうしてもユーザーが入力中にエラーメッセージが表示されます。
書式チェックの場合、「@
と.
」が入力されるまでエラーメッセージが表示されます。
筆者的には、「今入力しとるちゅうねん!!」と叫んでしまうほど不快に感じます。
こんなUI設計は極力辞めましょう。
ユーザーはフォームの色でエラーかどうかを十分に判断できるので、このメッセージは不要です。
あ、でもサーバーサイドから返ってくるエラーメッセージは必要ですよ。
これはユーザーが入力を完了した後に返されるエラーなので。
(コラムおわり)
このコンポーネントはログインでも使用する
このメールアドレスコンポーネントは、ログイン時にも使用します。
ログイン時にはプレースホルダーは必要ありません。
そこでnoValidation
というフラグを用意し、プレースホルダーの表示・非表示を切り替えます。
front/components/user/userFormEmail.vue
<template>
<!-- placeholder を書き換え -->
<v-text-field
...
:placeholder="form.placeholder"
outlined
/>
</template>
<script>
export default {
props: {
...
// 追加
noValidation: {
type: Boolean,
default: false
}
},
...
computed: {
...
// 追加
form () {
const placeholder = this.noValidation ? undefined : 'your@email.com'
return { placeholder }
}
}
}
</script>
確認してみよう
試しに<user-form-email>
に、no-validation
キーを追加してみましょう。
front/pages/signup.vue
<template>
...
<!-- no-validation 追加 -->
<user-form-email
no-validation
:email.sync="params.user.email"
/>
...
コンテナを起動して、http://localhost:8080/signup にアクセスしてください。
プレースホルダーが無くなって、ログインフォームっぽくなりましたね。
確認が取れたら、追加したno-validation
は削除しておいてください。
front/pages/signup.vue
<template>
...
<!-- no-validation 削除 -->
<user-form-email
:email.sync="params.user.email"
/>
...
このロジックを使えば、「ログイン時にはバリデーションをしない」という設定も可能です。
しかし今回は、Railsへ無駄なAPIを投げないようにするため、ログイン時にも「入力必須」と「書式チェック」のバリデーションは残します。
passwordのバリデーションを追加する
最後はパスワードフォームですね。
パスワードのバリデーションは、
- 入力必須
- 8文字以上
- 72文字以下
- 半角英数字、ハイフン、アンダーバーのみ使用可能
です。これらは正規表現でチェックしていきます。
ログインフォームにも対応する設定を行う
パスワードコンポーネントもログイン時に使用するので、フラグを用意し、それぞれの要素を切り替えていきます。
会員登録フォーム | ログインフォーム | |
---|---|---|
バリデーション | 8 ~ 72文字の文字数、書式チェック | 入力必須のみ |
文字数カウンター | 必要 | 不要 |
入力のヒント | 8文字以上。半角英数字•ハイフン•アンダーバーが使えます | undefined |
プレースホルダー | 8文字以上 | undefined |
front/components/user/userFormPassword.vue
<template>
<!-- まるっと書き換え -->
<v-text-field
v-model="setPassword"
:rules="form.rules"
:counter="!noValidation"
:hint="form.hint"
label="パスワードを入力"
:placeholder="form.placeholder"
:hide-details="noValidation"
outlined
/>
</template>
<script>
export default {
props: {
...
// 追加
noValidation: {
type: Boolean,
default: false
}
},
computed: {
...
// 追加
form () {
const min = '8文字以上'
const msg = `${min}。半角英数字•ハイフン•アンダーバーが使えます`
const required = v => !!v || ''
const format = v => /^[\w-]{8,72}$/.test(v) || msg
const rules = this.noValidation ? [required] : [format]
const hint = this.noValidation ? undefined : msg
const placeholder = this.noValidation ? undefined : min
return { rules, hint, placeholder }
}
}
}
</script>
:rules
noValidation
が
true(ログイン)
=> 入力必須false(会員登録)
=> 文字数、書式チェック
:counter
入力文字数をカウントします。
noValidation
が
true(ログイン)
=> カウントしないfalse(会員登録)
=> カウントする
:hint
フォームにフォーカスされた時に入力のヒントを表示します。
noValidation
が
true(ログイン)
=> undefinedfalse(会員登録)
=> '8文字以上。半角英数字•ハイフン•アンダーバーが使えます'
:hide-details(ハイド-ディティルズ)
フォーム下に表示されるメッセージの余白が無くなります。
noValidation
が
true(ログイン)
=> 隠すfalse(会員登録)
=> 隠さない
ヒントやエラーメッセージがあっても、このキーがtrue
の場合はメッセージが隠されます。
ログインフォームの場合、表示するエラーメッセージが無いのでtrue
とします。
hide-detailsがtrueの場合、余白が無くなる
passwordの表示・非表示ボタンの設定
Vuetifyを使えばパスワードの表示・非表示が簡単に切り替えられます。
<v-text-field>
に新しいキーを追加しましょう。
front/components/user/userFormPassword.vue
<template>
<!-- 追加 -->
<v-text-field
...
:append-icon="toggle.icon"
:type="toggle.type"
outlined
autocomplete="on"
@click:append="show = !show"
/>
</template>
<script>
export default {
...
// 追加
data () {
return {
show: false
}
},
computed: {
...
// 追加
toggle () {
const icon = this.show ? 'mdi-eye' : 'mdi-eye-off'
const type = this.show ? 'text' : 'password'
return { icon, type }
}
}
}
</script>
:append-icon
フォームの隣に表示するアイコンを指定します。
:type
フォームタイプを指定します。
autocomplete="on"
フォームタイプがパスワードの場合、このキーを追加しないとGoogleが警告を出します。恐らくGoogleのパスワード予測機能を使うために必要なキーだと思います。
@click:append
append-icon
がクリックされた時のイベントを指定します。
これでパスワードフォームが完成しました。
登録ボタンのクリックイベントを追加する
最後の作業です。
今はまだRailsに送信できないので、仮イベントとなります。
...
<!-- ref 追加 -->
<v-form
ref="form"
v-model="isValid"
>
...
<!-- :disabled, :loading, @click 追加 -->
<v-btn
:disabled="!isValid || loading"
:loading="loading"
block
color="myblue"
class="white--text"
@click="signup"
>
登録する
</v-btn>
...
</template>
<script>
export default {
layout: 'beforeLogin',
data () {
return {
...
// loading 追加
loading: false,
params: { user: { name: '', email: '', password: '' } }
}
},
// 追加
methods: {
signup () {
this.loading = true
setTimeout(() => {
this.formReset()
this.loading = false
}, 1500)
},
formReset () {
this.$refs.form.reset()
this.params = { user: { name: '', email: '', password: '' } }
}
}
// ここまで
}
</script>
-
:loading
... 送信時にボタンがくるくるする。 -
this.$refs.form.reset()
... Vuetifyのメソッド。<v-form ref="form">
タグ内のフォームの、全ての入力を削除し、かつバリデーションエラーも削除します。ちなみに、
ref="form"
と$refs.form
の「form」の部分は、一致していないとVuetifyが正しく認識してくれませんのでご注意ください。
ボタンイベントの確認
ボタンイベントを確認してみましょう。
- バリデーションエラー時
- データ送信中
はクリックできなくなっています。
そして、バリデーション成功時にだけボタンが有効になっていますね。
コミットしとく
今回の作業は以上です。
「front」ディレクトリをコミットしておきましょう。
root $ cd front && git commit -am "finishe_signup_page" && cd ..
まとめ
今回は、会員登録の各フォームに、Vuetifyを使ったバリデーション設定を行いました。
これにて会員登録ページの完成です。
お疲れっした!
次回は?
ログインページの構築です。
もう今回で段取り作業は終わっていますので、次は簡単ですよ。
どうぞお楽しみに。
最終的なコード
会員登録ページ
front/pages/signup.vue
<template>
<bef-login-form-card #form-card-content>
<v-form
ref="form"
v-model="isValid"
>
<user-form-name
:name.sync="params.user.name"
/>
<user-form-email
:email.sync="params.user.email"
/>
<user-form-password
:password.sync="params.user.password"
/>
<v-btn
:disabled="!isValid || loading"
:loading="loading"
block
color="myblue"
class="white--text"
@click="signup"
>
登録する
</v-btn>
</v-form>
<v-card-text>
{{ params }}
</v-card-text>
</bef-login-form-card>
</template>
<script>
export default {
layout: 'beforeLogin',
data () {
return {
isValid: false,
loading: false,
params: { user: { name: '', email: '', password: '' } }
}
},
methods: {
signup () {
this.loading = true
setTimeout(() => {
this.formReset()
this.loading = false
}, 1500)
},
formReset () {
this.$refs.form.reset()
this.params = { user: { name: '', email: '', password: '' } }
}
}
}
</script>
nameコンポーネント
front/components/user/userFormName.vue
<template>
<v-text-field
v-model="setName"
:rules="rules"
:counter="max"
label="ユーザー名を入力"
placeholder="あなたの表示名"
outlined
/>
</template>
<script>
export default {
props: {
name: {
type: String,
default: ''
}
},
data () {
const max = 30
return {
max,
rules: [
v => !!v || '',
v => (!!v && max >= v.length) || `${max}文字以内で入力してください`
]
}
},
computed: {
setName: {
get () { return this.name },
set (newVal) { return this.$emit('update:name', newVal) }
}
}
}
</script>
emailコンポーネント
front/components/user/userFormEmail.vue
<template>
<v-text-field
v-model="setEmail"
:rules="rules"
label="メールアドレスを入力"
:placeholder="form.placeholder"
outlined
/>
</template>
<script>
export default {
props: {
email: {
type: String,
default: ''
},
noValidation: {
type: Boolean,
default: false
}
},
data () {
return {
rules: [
v => !!v || '',
v => /.+@.+\..+/.test(v) || ''
]
}
},
computed: {
setEmail: {
get () { return this.email },
set (newVal) { return this.$emit('update:email', newVal) }
},
form () {
const placeholder = this.noValidation ? undefined : 'your@email.com'
return { placeholder }
}
}
}
</script>
passwordコンポーネント
front/components/user/userFormPassword.vue
<template>
<v-text-field
v-model="setPassword"
:rules="form.rules"
:counter="!noValidation"
:hint="form.hint"
label="パスワードを入力"
:placeholder="form.placeholder"
:hide-details="noValidation"
:append-icon="toggle.icon"
:type="toggle.type"
outlined
autocomplete="on"
@click:append="show = !show"
/>
</template>
<script>
export default {
props: {
password: {
type: String,
default: ''
},
noValidation: {
type: Boolean,
default: false
}
},
data () {
return {
show: false
}
},
computed: {
setPassword: {
get () { return this.password },
set (newVal) { return this.$emit('update:password', newVal) }
},
form () {
const min = '8文字以上'
const msg = `${min}。半角英数字•ハイフン•アンダーバーが使えます`
const required = v => !!v || ''
const format = v => /^[\w-]{8,72}$/.test(v) || msg
const rules = this.noValidation ? [required] : [format]
const hint = this.noValidation ? undefined : msg
const placeholder = this.noValidation ? undefined : min
return { rules, hint, placeholder }
},
toggle () {
const icon = this.show ? 'mdi-eye' : 'mdi-eye-off'
const type = this.show ? 'text' : 'password'
return { icon, type }
}
}
}
</script>