SPA開発 8. ログイン周りのレイアウト設計 #04
2020年01月05日に更新

【Nuxt.js】会員登録フォームを構築してサインアップページを完成させる

今回達成すること

今回は、前回のコンポーネント設計に従って、会員登録ページ(signup.vue)を完成させます。

最終的にこのようになります。↓

完成イメージ

2019-12-01 13-59-18

作業に入る前に

今回の記事は全てNuxt.js上での作業となります。

ターミナルは、Rails内のNuxt.jsプロジェクト(筆者の場合、myapp/frontend)に移動しておいてください。

froutend $ # ここに移動しておく

それでは実装に移りましょう。

現状の会員登録ページ「signup.vue」の確認

現在の「signup.vue」を改善していきます。

Nuxt.jsを起動して"http://localhost:3333/signup"にアクセスしてみましょう。

frontend $ yarn dev

すると、このように表示されたかと思います。

2019-11-29 14-30-52

signup.vueの問題点

「signup.vue」には2つの問題点があります。

  1. ログイン後のヘッダー(loginHeader.vue)コンポーネントが表示されている
  2. フッターが下に固定されていない。

まず、これらの問題点を修正していきます。

問題の根元、レイアウト「default.vue」を修正する

上記の問題点は、layoutsディレクトリのdefault.vueを修正することで解決します。

ログイン前後でヘッダーを切り変える

「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>

ページをリロードしてみましょう。

2019-11-29 15-03-02

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つの問題点は解決しましたね。

2019-11-29 16-00-23

入力フォームのコンポーネントファイルを作成する

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

作成したコンポーネントファイルの仮編集

作成したコンポーネントを仮編集しておきます。

まずは、emailForm.vueから。

frontend/components/users/emailForm.vue
<template>
  <div>
    emailForm.vue
  </div>
</template>

<script>
export default {
}
</script>

次にpasswordForm.vueをこのように。

frontend/components/users/passwordForm.vue
<template>
  <div>
    passwordForm.vue
  </div>
</template>

<script>
export default {
}
</script>

会員登録ページからフォームコンポーネントを呼び出す

signup.vueから作成したコンポーネントを呼び出します。

コンポーネントの関係性
  • 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>

はい、無事コンポーネントが呼ばれましたね。

2019-11-29 17-24-01

会員登録ページ(signup.vue)の実装に入る

それでは準備が整いましたので、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のバリデーションがエラーの時

2019-11-30 16-08-19

v-text-fieldのバリデーションが OKの時

2019-11-30 16-10-25

  • validation

    このプロパティはオリジナルで作成したもので、パスワードコンポーネント内でフラグとして扱います。

    デフォルトでfalseの値を持ち、プロパティを渡すとtrueになります。

    バリデーションを切り替える際に利用します。

emailForm.vueにフォームを実装する

次は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()を使い値の受け渡しを分離します。

    1. get() => 親の値を受け取り
    2. set() => 変更した値を親に送る

    図解でもう少し詳しく説明しています。↓

    #【コラム】コンポーネント間の双方向データバインディング

  • :rules="[rules.match]"

    rulesはVuetifyのプロパティで、配列の形でバリデーションルールを渡します。

    Forms - 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にフォームを実装する

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を指定

    2019-12-01 10-29-54

  • :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を返します。

会員登録ボタンを作成する

会員登録ボタンは、ログイン周りのレイアウトが完成した後に、API通信を行う処理を書いていきます。

今は仮でボタン処理を実装します。

signup.vueに移動しましょう。

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"を追加します。

    Forms - Vuetify

  • this.$refs.form.reset()

    v-form内にあるフォームの値をクリアします。

    これも上記と同じく、ref="form"を追加することにより使えるメソッドです。

会員登録ボタンの実装を確認する

ボタンクリックの有効、無効はこのように切り替わります。

ご自身の実装と見比べて、うまくできているか確認してみましょう。

状況 ボタン
フォーム未入力時 無効
バリデーションエラーのとき 無効
バリデーションが通ったとき 有効
Railsに送信中 無効

完成した会員登録ボタンの動きはこのようになります。

signup

ログインページへのリンクを設置する

登録済みユーザーのために、ログインページに遷移するリンクを設置しましょう。

同じくsignup.vue</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>

「ログインする」ボタンが表示されました。

2019-12-01 13-59-18

Vuetifyのスタイルを調整する

今のままだとヒントが出るたびに会員登録ボタンが下に押されカクカクします。

そこでVuetifyのスタイルを調整していきます。

アプリ全体に適用したいのでmain.scssに追記しましょう。

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で、親コンポーネントの値を変えることはできません。

悪い例

2019-11-30 19-24-02

しかし、computed内でget()set()を使い「受ける」と「送る」行為を分離すれば子コンポーネント内でもv-modelが使えます。

カタカナの"コ"の字を描くように通信を行います。

良い例

2019-11-30 19-06-46

書き方は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を使って実装してください。

参考

コンポーネントイベント - カスタムイベント — Vue.js

(コラム終わり)

現在、カテゴリー「Rails apiとNuxt.jsでSPA開発」のデモアプリを構築中です。記事になるまでもう少々のお時間が必要です。ブログの更新が止まって申し訳ありません。デモアプリの進捗状況は こちらの記事 で随時お伝えしてまいります。
スポンサー広告
次の記事はこちらです
SPA開発
今日のTweet
スポンサー広告