今回達成すること
今回は、前回のコンポーネント設計に従って、会員登録ページ(signup.vue)を完成させます。
最終的にこのようになります。↓
完成イメージ
作業に入る前に
今回の記事は全てNuxt.js上での作業となります。
ターミナルは、Rails内のNuxt.jsプロジェクト(筆者の場合、myapp/frontend)に移動しておいてください。
froutend $ # ここに移動しておく
それでは実装に移りましょう。
現状の会員登録ページ「signup.vue」の確認
現在の「signup.vue」を改善していきます。
Nuxt.jsを起動して"http://localhost:3333/signup"にアクセスしてみましょう。
frontend $ yarn dev
すると、このように表示されたかと思います。
signup.vueの問題点
「signup.vue」には2つの問題点があります。
- ログイン後のヘッダー(loginHeader.vue)コンポーネントが表示されている
- フッターが下に固定されていない。
まず、これらの問題点を修正していきます。
問題の根元、レイアウト「default.vue」を修正する
上記の問題点は、layoutsディレクトリの
ログイン前後でヘッダーを切り変える
「signup.vue」にログイン後のヘッダーが利用されている問題を改善していきます。
この問題は、Vue.jsの動的にコンポーネントを切り替えるis
プロパティを使うことで改善します。
frontend/layouts/default.vue
<template>
<v-app>
<div :is="currentHeader" /> <!-- 追記 -->
<nuxt />
<my-footer v-if="!$store.state.loggedIn" />
</v-app>
</template>
<script>
import myHeader from '~/components/shared/header.vue' // 追記
import loginHeader from '~/components/shared/loginHeader.vue'
import myFooter from '~/components/shared/footer.vue'
export default {
components: {
myHeader, // 追記
loginHeader,
myFooter
},
// 追記
computed: {
currentHeader () {
return this.$store.state.loggedIn ? 'loginHeader' : 'myHeader'
}
}
}
</script>
-
<div :is="currentHeader" />
is
プロパティに渡すものは、コンポーネントファイル名と一致する文字列を渡します。
ページをリロードしてみましょう。
OK!ヘッダーが表示されました。
フッターを下に固定する
これは簡単で、メインコンテンツを<v-content>
タグで囲むことにより改善します。
frontend/layouts/default.vue
<template>
<v-app>
<div :is="currentHeader" />
<v-content> <!-- 追記 -->
<nuxt />
</v-content> <!-- 追記 -->
<my-footer v-if="!$store.state.loggedIn" />
</v-app>
</template>
OK!これで「signup.vue」の2つの問題点は解決しましたね。
入力フォームのコンポーネントファイルを作成する
components内にusersディレクトリを作成し、「emailForm.vue」「passwordForm.vue」の2つのファイルを作成します。
ターミナルで下記コマンドを実行してください。
frontend $ mkdir components/users && touch components/users/{emailForm.vue,passwordForm.vue}
componentsディレクトリの構成はこのようになります。
components
├── README.md
├── shared
│ ├── footer.vue
│ ├── header.vue
│ └── loginHeader.vue
└── users
├── emailForm.vue
└── passwordForm.vue
作成したコンポーネントファイルの仮編集
作成したコンポーネントを仮編集しておきます。
まずは、
frontend/components/users/emailForm.vue
<template>
<div>
emailForm.vue
</div>
</template>
<script>
export default {
}
</script>
次に
frontend/components/users/passwordForm.vue
<template>
<div>
passwordForm.vue
</div>
</template>
<script>
export default {
}
</script>
会員登録ページからフォームコンポーネントを呼び出す
コンポーネントの関係性
- signup.vue => 親コンポーネント
- emailForm.vue・passwordForm.vue => 子コンポーネント
frontend/pages/signup.vue
<template>
<div>
新規会員登録ページ
<email-form />
<password-form />
</div>
</template>
<script>
import emailForm from '~/components/users/emailForm.vue'
import passwordForm from '~/components/users/passwordForm.vue'
export default {
components: {
emailForm,
passwordForm
}
}
</script>
はい、無事コンポーネントが呼ばれましたね。
会員登録ページ(signup.vue)の実装に入る
それでは準備が整いましたので、
frontend/pages/signup.vue
<template>
<v-container>
<v-row
justify="center"
>
<v-card
flat
width="90%"
max-width="368"
color="transparent"
>
<v-card-title
class="justify-center headline"
>
新規会員登録
</v-card-title>
<v-form
ref="form"
v-model="valid"
>
<email-form
:email.sync="email"
/>
<password-form
:password.sync="password"
validation
/>
<v-card-text
class="text-center"
>
<v-btn
:disabled="!valid || loading"
:loading="loading"
color="primary"
@click="signup"
>
登録する(無料)
</v-btn>
</v-card-text>
</v-form>
<v-card-actions>
<v-divider />
</v-card-actions>
<v-card-text
class="text-center"
>
会員登録はお済みですか?
</v-card-text>
<v-card-actions>
<v-btn
to="/login"
class="mx-auto px-4"
>
ログインする
</v-btn>
</v-card-actions>
</v-card>
</v-row>
</v-container>
</template>
<script>
import emailForm from '~/components/users/emailForm.vue'
import passwordForm from '~/components/users/passwordForm.vue'
export default {
components: {
emailForm,
passwordForm
},
data: () => ({
valid: true,
email: '',
password: '',
loading: false
}),
methods: {
signup () {
if (this.$refs.form.validate()) {
this.loading = true
alert(
`email: ${this.email}\n` +
`password: ${this.password}\n` +
'これらをRailsに送信してuserを作成します'
)
setTimeout(() => {
this.loading = false
this.$refs.form.reset() // form clear
}, 2000)
}
}
}
}
</script>
-
v-form
emailForm、passwordFormコンポーネントをこの
v-form
タグで囲みます。これにより、
v-model="valid"
が有効になります。 -
v-model="valid"
v-form
タグ直下にあるv-text-field
のバリデーションにエラーがあるとき、validフラグはfalseになります。バリデーションがOKなときはtrueに変わります。v-text-fieldのバリデーションがエラーの時
v-text-fieldのバリデーションが OKの時
-
:email.sync="email"
親コンポーネントのデータを子コンポーネントで編集して親に返す方法は他にもありますが、今回は
sync
を使った方法を採用します。比較的シンプルに書けておすすめです。syncのより詳しい使い方は下記目次をご覧ください。
-
validation
このプロパティはオリジナルで作成したもので、パスワードコンポーネント内でフラグとして扱います。
デフォルトでfalseの値を持ち、プロパティを渡すとtrueになります。
バリデーションを切り替える際に利用します。
emailForm.vueにフォームを実装する
次は
Vuetifyのv-text-field
を使います。
frontend/components/users/emailForm.vue
<template>
<v-text-field
v-model="setEmail"
:rules="[rules.match]"
label="メールアドレスを入力"
outlined
required
validate-on-blur
/>
</template>
<script>
export default {
props: {
email: {
type: String,
default: ''
}
},
data: () => ({
rules: {
match: value => /.+@.+\..+/.test(value) || ''
}
}),
computed: {
setEmail: {
get () { return this.email },
set (newVal) { return this.$emit('update:email', newVal) }
}
}
}
</script>
-
v-model="setEmail"
親コンポーネントから受け取った値を、そのまま
v-model
に渡すとエラーになります。これは「親子間のデータ通信は単方向のみ可能」というVue.jsのルールがあるからです。
v-model
を使うと、双方向 になるのでエラーになるのですね。対応としてcomputed内で
get()
、set()
を使い値の受け渡しを分離します。get()
=> 親の値を受け取りset()
=> 変更した値を親に送る
図解でもう少し詳しく説明しています。↓
-
:rules="[rules.match]"
rules
はVuetifyのプロパティで、配列の形でバリデーションルールを渡します。 -
validate-on-blur
Vuetifyで用意されているプロパティです。
通常、入力のタイミングでエラーが表示されますが、このフラグによりカーソルを離したタイミングでエラーを表示するようになります。
ユーザーのストレスを軽減することができます。
-
match: value => /.+@.+\..+/.test(value) || ''
メールの書式をチェックするバリデーションです。
test()
は、渡された値が正規表現と一致していればtrueを返すjavascriptのメソッドです。falseの場合はエラーメッセージを返すようになります。
ちなみに、エラーメッセージを表示させたい場合は文字列を渡します。
|| 'エラーがあります'
-
/.+@.+\..+/
Javascriptの正規表現です。
.
=> 改行文字以外のどの 1 文字にもマッチします。+
=> 直前の文字の 1 回以上の繰り返し。{1,} と同じです。 -
this.$emit('update:email', newVal)
$emit
は親に変更した値を送信するための、子コンポーネント側の命令文です。sync
を使った$emit
の基本的な書き方はこのようになります。$emit('update:キー名', 新しい値)
passwordForm.vueにフォームを実装する
frontend/components/users/passwordForm.vue
<template>
<v-text-field
v-model="setPassword"
:rules="validation ? [rules.match] : [rules.required]"
:append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
:type="show ? 'text' : 'password'"
:hint="validation ? hint : undefined"
:counter="validation"
label="パスワードを入力"
outlined
required
validate-on-blur
autocomplete="on"
@click:append="show = !show"
/>
</template>
<script>
export default {
props: {
password: {
type: String,
default: ''
},
validation: {
type: Boolean,
default: false
}
},
data () {
const hint = '8文字以上、半角英数字•ハイフン•アンダーバーが使えます'
return {
show: false,
hint,
rules: {
required: value => !!value || '',
match: value => /^[\w-]{8,72}$/.test(value) || hint
}
}
},
computed: {
setPassword: {
get () { return this.password },
set (newVal) { return this.$emit('update:password', newVal) }
}
}
}
</script>
-
:rules="validation ? [rules.match] : [rules.required]"
フラグによりバリデーションを切り替えます。
もし、
validation
フラグがtrueのときは、[rules.match](厳密な書式チェック)をし、falseの場合は[rules.required](入力必須)のみを実装します。 -
:append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
append-icon
は、フォームの中にiconを表示できるVuetifyのプロパティです。目のマークのiconを指定
-
:type="show ? 'text' : 'password'"
type
を切り替えることによってパスワードの表示、非表示を切り替えることができます。 -
@click:append="show = !show"
append-icon
をクリックした時に起こるイベントを指定します。show
がtrueのときはfalseに、falseのときはtrueにしています。 -
:counter="validation"
フォームに入力された文字数をカウントするVuetifyのプロパティです。
validation
がtrueのときだけ文字数をカウントするようにしています。 -
match: value => /^[\w-]{8,72}$/.test(value) || hint
パスワードの書式をチェックするバリデーションです。
^
=> 先頭文字を指定します。\w
=> アンダーバーを含む英数字にマッチします。-
=> ハイフンにマッチします。[]
=> 囲まれた文字、どれにもマッチします。{8,72}
=> 直前の文字が8文字以上、72文字以下であることを指定しています。$
=> 文末を指定します。まとめ => 「最初から最後まで半角英数字、ハイフン、アンダーバーの文字列で、8文字以上72文字以下」の場合にtrueを返します。
-
autocomplete="on"
chromeに「Input elements should have autocomplete attributes」の警告が出たので付けました。無視しても大丈夫な警告っぽいです。
Input elements should have autocomplete attributes - Stack Overflow
会員登録ボタンを作成する
会員登録ボタンは、ログイン周りのレイアウトが完成した後に、API通信を行う処理を書いていきます。
今は仮でボタン処理を実装します。
frontend/pages/signup.vue
<template>
...
<password-form
:password.sync="password"
validation
/>
<!-- 追記 -->
<v-card-text
class="text-center"
>
<v-btn
:disabled="!valid || loading"
:loading="loading"
large
color="primary"
@click="signup"
>
登録する(無料)
</v-btn>
</v-card-text>
<!-- ここまで -->
</template>
<script>
export default {
...
data: () => ({
loading: false // 追記
}),
// 追記
methods: {
signup () {
if (this.$refs.form.validate()) {
this.loading = true
alert(
`email: ${this.email}\n` +
`password: ${this.password}\n` +
'これらをRailsに送信してuserを作成します'
)
setTimeout(() => {
this.loading = false
this.$refs.form.reset() // form clear
}, 2000)
}
}
}
// ここまで
}
</script>
-
:disabled="!valid || loading"
disabled
はボタンクリックを無効にするプロパティです。valid
がfalse、もしくはloading
がtrueの時に無効になります。 -
loading: false
loading
は、Railsとの通信中を判定するフラグです。2度押し防止のため、通信中はtrueにしてボタン操作を無効にします。
-
this.$refs.form.validate()
v-form
内のバリデーションが全て通ったときにtrueを返します。このメソッドを使うには、
v-form
タグにref="form"
を追加します。 -
this.$refs.form.reset()
v-form
内にあるフォームの値をクリアします。これも上記と同じく、
ref="form"
を追加することにより使えるメソッドです。
会員登録ボタンの実装を確認する
ボタンクリックの有効、無効はこのように切り替わります。
ご自身の実装と見比べて、うまくできているか確認してみましょう。
状況 | ボタン |
---|---|
フォーム未入力時 | 無効 |
バリデーションエラーのとき | 無効 |
バリデーションが通ったとき | 有効 |
Railsに送信中 | 無効 |
完成した会員登録ボタンの動きはこのようになります。
ログインページへのリンクを設置する
登録済みユーザーのために、ログインページに遷移するリンクを設置しましょう。
同じく</v-form>
タグ直下に追記します。
<template>
...
</v-form>
<!-- 追記 -->
<v-card-actions>
<v-divider />
</v-card-actions>
<v-card-text
class="text-center"
>
会員登録はお済みですか?
</v-card-text>
<v-card-actions>
<v-btn
to="/login"
class="mx-auto px-4"
>
ログインする
</v-btn>
</v-card-actions>
<!-- ここまで -->
</template>
「ログインする」ボタンが表示されました。
Vuetifyのスタイルを調整する
今のままだとヒントが出るたびに会員登録ボタンが下に押されカクカクします。
そこでVuetifyのスタイルを調整していきます。
アプリ全体に適用したいので
frontend/assets/css/main.scss
// Vuetify //////////////////////////
// v-text-field
.v-text-field__details {
min-height: 18px !important;
}
これにて完了ー。
本番環境にpushしよう
コミットしましょ。
frontend $ cd ../
myapp $ git add -A
myapp $ git commit -m "finished_signup_layout"
変更が大きくなってきたので、一度ブランチを切ります。
今のブランチ名(login_layouting)は、ここまでの変更内容が分かりづらいので、-m
オプションで名前を変更します。
myapp $ git branch -m home_signup_layouts
myapp $ git branch
* home_signup_layouts
master
OK!マスターブランチに移動してマージしましょう。
myapp $ git checkout master
myapp $ git merge home_signup_layouts
myapp $ git push
myapp $ git push heroku
まとめ
さて、今回の学んだポイントをおさらいして終わりましょう。
-
コンポーネントを動的に切り替えるには、Vue.jsの
is
プロパティを使う -
Vue.js、Nuxt.jsでコンポーネント間でデータの受け渡しを行うには、親 =>
sync
をバインドキーに付ける子 => computed内の
get()
、set()
で値の受信、送信を行う -
パスワードのバリデーションを切り替えたいときは、
validation
プロパティを親から子へ渡す -
登録ボタンはバリデーションが通ったときのみ有効。Railsと通信中は
loading
フラグで無効にする
さて、次回は?
いやー、少しづつ形になってきましたね。
次はログインフォームのレイアウトを構築していきます。
またいらしてください。
終。
(下はコラムです。お時間ありましたらどうぞ。)
【コラム】コンポーネントの双方向データバインディング
コンポーネントの双方向データバインディングとは、親の値を子で編集しリアルタイムで親に値を返す実装のことです。
Vue.jsコンポーネント通信の基本
Vue.jsでは、子コンポーネント内のv-model
で、親コンポーネントの値を変えることはできません。
悪い例
しかし、computed内でget()
、set()
を使い「受ける」と「送る」行為を分離すれば子コンポーネント内でもv-model
が使えます。
カタカナの"コ"の字を描くように通信を行います。
良い例
書き方は2つあります。
1. 基本的な書き方
親コンポーネント
親側で:value
で送信し、@input
で受け取る
$event
には変更された新しい値(newVal)が入っている
<email-form
:value="email"
@input="email = $event"
/>
子コンポーネント
$emit
は'input'に送信する
'input'の文字列は自由。'myInput'とした場合、親側では@myInput
で受け取る
<template>
<v-text-field v-model="setEmail"/>
</template>
<script>
export default {
props: {
email: { type: String, default: '' }
},
computed: {
setEmail: {
get () { return this.email },
set (newVal) { return this.$emit('input', newVal) } // newValを@inputに送る
}
}
}
</script>
2. syncを使った書き方
親コンポーネント
バインドするキーにsync
をつけると送受信が可能になる
<email-form
:email.sync="email"
/>
子コンポーネント
$emit
内で'update:email'として送信する
ここの'email'はキーの名前を指定する
<template>
<v-text-field v-model="setEmail"/>
</template>
<script>
export default {
props: {
email: { type: String, default: '' }
},
computed: {
setEmail: {
get () { return this.email },
set (newVal) { return this.$emit('update:email', newVal) } // 'update:キー名'で送信
}
}
}
</script>
ご覧のとおり、sync
を使った方がシンプルにかけますので、今後も双方向データバインディングが必要な場所はこちらを利用していきます。
なお、sync
は暗黙的に親側の値が更新されます。
子側で受け取った値を加工してからデータに代入するといった処理を行う場合は props
、$emit
を使って実装してください。
参考
(コラム終わり)