VeeValidateでバリデーション

①vee-validateをインストール。
$ yarn add vee-validate


②pluginsディレクトリ配下にファイルを作成してインスタンスに登録する。
$ touch app/javascript/plugins/veevalidate.js


javascript/packs/hello_vue.js

import '../plugins/veevalidate'


③veevalidateファイルにバリデーションルールを定義する。
import Vue from 'vue'

import {
  ValidationProvider,
  ValidationObserver,
  extend
} from 'vee-validate'

// よくあるルールはすでに用意されているのでインポートして使う。
import {
  email,
  required
} from 'vee-validate/dist/rules';

Vue.component('ValidationObserver', ValidationObserver)
Vue.component('ValidationProvider', ValidationProvider)

// インポートしたルールを定義。
extend('email', {
  ...email,
  message: '{_field_}の形式で入力してください'
});

extend('required', {
  ...required,
  message: '{_field_}は必須項目です'
});

// もちろん独自ルールも定義できる。
// valueは入力される値。入力される値とは別に引数を取ることもできる。
extend('min', {
  validate(value, { length }) {
    return value.length >= length;
  },
  params: ['length'],
  message: '{_field_}は{length}文字以上で入力してください'
});

extend('max', {
  validate(value, { length }) {
    return value.length <= length;
  },
  params: ['length'],
  message: '{_field_}は{length}文字以下で入力してください'
});

extend('password_confirmed', {
  params: ['target'],
  validate(value, { target }) {
    return value === target;
  },
  message: 'パスワードと一致しません'
});


④定義したルールをフォームで使う。

新規登録時フォーム

<template>
  <div
    id="register-form"
    class="container w-50 text-center"
  >
    <div class="h3 mb-3">
      ユーザー登録
    </div>
    <!-- 該当フォーム全体をValidationObserverで囲む。
          handleSubmitでフォーム送信前にもバリデーションチェックできる。 -->
    <ValidationObserver v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <!-- 各入力欄をValidationProviderで囲む。
              errorsにエラーメッセージを格納し、rulesで定義したルールを使う。-->
        <ValidationProvider
          v-slot="{ errors }"
          rules="required"
        >
          <label for="name">ユーザー名</label>
          <input
            id="name"
            v-model="user.name"
            name="ユーザー名"
            type="text"
            placeholder="username"
            class="form-control"
          >
          <!-- errorsに格納されたエラーメッセージを表示する。 -->
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider
          v-slot="{ errors }"
          rules="required|email"
        >
          <label for="email">メールアドレス</label>
          <input
            id="email"
            v-model="user.email"
            name="メールアドレス"
            type="email"
            placeholder="test@example.com"
            class="form-control"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <!-- @passwordで入力された値を取得できる。 -->
        <ValidationProvider
          v-slot="{ errors }"
          rules="required"
          vid="password"
        >
          <label for="password">パスワード</label>
          <input
            id="password"
            v-model="user.password"
            name="パスワード"
            type="password"
            placeholder="password"
            class="form-control"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <!-- @passwordでパスワード入力欄で入力された値を取得している。 -->
        <ValidationProvider
          v-slot="{ errors }"
          rules="required|min:3|password_confirmed:@password"
        >
          <label for="password_confirmation">パスワード(確認)</label>
          <input
            id="password_confirmation"
            v-model="user.password_confirmation"
            name="パスワード(確認)"
            type="password"
            placeholder="password"
            class="form-control"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <!-- handleSubmitでフォーム送信前にもバリデーションチェックできる。 -->
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(register)"
      >
        登録
      </button>
    </ValidationObserver>
  </div>
</template>

タスク作成フォーム

<template>
  <div id="task-create-modal">
    <div
      class="modal"
      @click.self="handleCloseModal"
    >
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-body">
            <ValidationObserver v-slot="{ handleSubmit }">
              <div class="from-group mb-3">
                <ValidationProvider
                  v-slot="{ errors }"
                  rules="required|max:50"
                >
                  <label for="title">タイトル</label>
                  <input
                    id="title"
                    v-model="task.title"
                    name="タイトル"
                    type="text"
                    class="form-control"
                  >
                  <span class="text-danger">{{ errors[0] }}</span>
                </ValidationProvider>
              </div>
              <div class="from-group mb-3">
                <ValidationProvider
                  v-slot="{ errors }"
                  rules="max:500"
                >
                  <label for="description">説明文</label>
                  <textarea
                    id="description"
                    v-model="task.description"
                    name="説明文"
                    class="form-control"
                    rows="5"
                  />
                  <span class="text-danger">{{ errors[0] }}</span>
                </ValidationProvider>
              </div>
              <div class="form-group mb-3">
                <ValidationProvider
                  v-slot="{ errors }"
                  rules="required"
                >
                  <label for="status">ステータス</label>
                  <select
                    id="status"
                    v-model="task.status"
                    name="ステータス"
                    class="form-control"
                  >
                    <option value="todo">
                      TODO
                    </option>
                    <option value="doing">
                      DOING
                    </option>
                    <option value="done">
                      DONE
                    </option>
                  </select>
                  <span class="text-danger">{{ errors[0] }}</span>
                </ValidationProvider>
              </div>
              <div class="d-flex justify-content-between">
                <button
                  type="button"
                  class="btn btn-success"
                  @click="handleSubmit(createTask)"
                >
                  追加
                </button>
                <button
                  type="button"
                  class="btn btn-secondary"
                  @click="handleCloseModal"
                >
                  閉じる
                </button>
              </div>
            </ValidationObserver>
          </div>
        </div>
      </div>
    </div>
    <div class="modal-backdrop show" />
  </div>
</template>



VeeValidate公式:
Basics | VeeValidate
Available Rules | VeeValidate
Handling Forms | VeeValidate

param is missing or the value is empty: user

該当コード

class Api::UsersController < ApplicationController
.
.
.
  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
end


解決方法

class Api::UsersController < ApplicationController
.
.
.
  private

  def user_params
    params.permit(:email, :password, :password_confirmation, :name)
  end
end


参考記事:【Rails】param is missing or the value is empty:について - Qiita

JWTを用いてトークンベースの認証を実装

前回:ユーザー登録機能の実装 - Rails技術ブログ

ruby-jwtをインストール。
gem 'jwt'
$ bundle install


②ルーティングを設定。
Rails.application.routes.draw do
  root to: 'home#index'
  namespace :api, format: 'json' do
    resources :tasks
    resources :sessions
    resources :users
  end
  get '*path', to: 'home#index'
end


/api/sessionsにpostリクエストを送るとトークンを返すようにコントローラーを設定。

controllers/api/sessions_controller.rb

class Api::SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password])

    if user
      token = user.create_tokens

      render json: { token: token }
    else
      head :unauthorized
    end
  end
end


models/concerns/jwt_token.rb

module JwtToken
  extend ActiveSupport::Concern

  def create_tokens
    payload = { user_id: id }
    issue_token(payload.merge(exp: Time.current.to_i + 1.month))
  end

  private

  def issue_token(payload)
    JWT.encode payload, Rails.application.secrets.secret_key_base
  end
end


models/user.rb

class User < ApplicationRecord
  include JwtToken
.
.
.
end


④ログインページを実装し、axiosでapi/sessionsにpostリクエストを送信して返ってきたトークンをローカルストレージに保存し、さらにリクエストヘッダーに含めるようにする。

javascript/pages/login/index.vue

<template>
  <div
    id="login-form"
    class="container w-50 text-center"
  >
    <div class="h3 mb-3">
      ログイン
    </div>
    <div class="form-group text-left">
      <label for="email">メールアドレス</label>
      <input
        id="email"
        v-model="user.email"
        name="メールアドレス"
        type="email"
        placeholder="test@example.com"
        class="form-control"
      >
    </div>
    <div class="form-group text-left">
      <label for="password">パスワード</label>
      <input
        id="password"
        v-model="user.password"
        name="パスワード"
        type="password"
        placeholder="password"
        class="form-control"
      >
    </div>
    <button
      type="submit"
      class="btn btn-primary"
      @click="login"
    >
      ログイン
    </button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  name: 'LoginIndex',
  data() {
    return {
      user: {
        email: '',
        password: '',
      }
    }
  },
  methods: {
    ...mapActions('users', [
      "loginUser",
    ]),
    async login() {
      try {
        await this.loginUser(this.user);
        this.$router.push({ name: 'TaskIndex' })
      } catch (error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
</style>


javascript/store/modules/users.js

import axios from '../../plugins/axios'

const state = {
}

const getters =  {
}

const mutations = {
}

const actions = {
  async loginUser({ commit }, user) {
    // ログイン
    const sessionsResponse = await axios.post('sessions', user)
    // 返ってきたトークンをローカルストレージに保存する。
    localStorage.auth_token = sessionsResponse.data.token
    axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`
  },
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


javascript/plugins/axios.js

import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: 'api'
})

// リクエストの際にヘッダーにBearerトークンを含めるようにする。
if (localStorage.auth_token) {
  axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`
}

export default axiosInstance


⑤リクエストヘッダーのトークンからユーザーを割り出してcurrent_userに代入する。

controllers/concerns/api/user_authenticator.rb

module Api::UserAuthenticator
  extend ActiveSupport::Concern

  def current_user
    return @current_user if @current_user
    return unless bearer_token

    // Bearerトークンをデコードしてuser_idを取り出し、
    // それをもとにユーザーを特定して@current_userに代入する。
    // ※変数の後にカンマを付けると配列の最初の要素を代入する。
    payload, = User.decode bearer_token
    @current_user ||= User.find_by(id: payload['user_id'])
  end

  // 認証トークンがない場合は401エラーを返す。
  def authenticate!
    return if current_user

    head :unauthorized
  end

  // これでリクエストヘッダーのBearerトークンを取得できる。
  def bearer_token
    pattern = /^Bearer /
    header = request.headers['Authorization']

    header.gsub(pattern, '') if header&.match(pattern)
  end
end


models/concerns/jwt_token.rb

module JwtToken
  extend ActiveSupport::Concern

  class_methods do    // 追加
    def decode(token)
      JWT.decode token, Rails.application.secrets.secret_key_base
    end
  end

  def create_tokens
    payload = { user_id: id }
    issue_token(payload.merge(exp: Time.current.to_i + 1.month))
  end

  private

  def issue_token(payload)
    JWT.encode payload, Rails.application.secrets.secret_key_base
  end
end


controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Api::UserAuthenticator
  protect_from_forgery with: :null_session
end


/api/users/mecurrent_userを返すAPIを設定する。

routes.rb

Rails.application.routes.draw do
  root to: 'home#index'
  namespace :api, format: 'json' do
    resources :tasks
    resources :sessions
    resources :users do
      collection do
        get 'me'
      end
    end
  end
  get '*path', to: 'home#index'
end


controllers/api/users_controller.rb

class Api::UsersController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    user = User.new(user_params)

    if user.save
      render json: user
    else
      render json: user.errors, status: :bad_request
    end
  end

  def me  # 追加
    render json: current_user
  end

  private

  def user_params
    params.permit(:email, :password, :password_confirmation, :name)
  end
end


⑦ログイン前はヘッダーにユーザー登録ページとログインページのリンクを表示し、ログイン後はログアウトボタンを表示する。

javascript/components/TheHeader.vue

<template>
  <header>
    <nav class="navbar navbar-expand navbar-dark bg-dark justify-content-between">
      <span class="navbar-brand mb-0 h1">タスク管理アプリ</span>
      <ul class="navbar-nav">
        <template v-if="!authUser">
          <li class="nav-item active">
            <router-link
              :to="{ name: 'RegisterIndex' }"
              class="nav-link"
            >
              ユーザー登録
            </router-link>
          </li>
          <li class="nav-item active">
            <router-link
              :to="{ name: 'LoginIndex' }"
              class="nav-link"
            >
              ログイン
            </router-link>
          </li>
        </template>
        <template v-else>
          <li class="nav-item active">
            <router-link
              to="#"
              class="nav-link"
              @click.native="handleLogout"
            >
              ログアウト
            </router-link>
          </li>
        </template>
      </ul>
    </nav>
  </header>
</template>

<script>
import { mapGetters, mapActions } from "vuex"

export default {
  name: "TheHeader",
  computed: {
    ...mapGetters("users", ["authUser"])
  },
  methods: {
    ...mapActions("users", ["logoutUser"]),
    async handleLogout() {
      try {
        await this.logoutUser()
        this.$router.push({name: 'TopIndex'})
      } catch (error) {
        console.log(error)
      }
    }
  }
}
</script>

<style scoped>
</style>


javascript/store/modules/users.js

import axios from '../../plugins/axios'

const state = {
  authUser: null
}

const getters =  {
  authUser: state => state.authUser
}

const mutations = {
  setUser: (state, user) => {
    state.authUser = user
  }
}

const actions = {
  async loginUser({ commit }, user) {
    // ログイン
    const sessionsResponse = await axios.post('sessions', user)
    localStorage.auth_token = sessionsResponse.data.token
    axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`

    // ログインユーザー情報の取得
    const userResponse = await axios.get('users/me')
    commit('setUser', userResponse.data)
  },
  logoutUser({ commit }) {
    // ログアウト
    // ローカルストレージのトークンを削除。
    localStorage.removeItem('auth_token')
   // リクエストヘッダーのBearerトークンも削除。
    axios.defaults.headers.common['Authorization'] = ''
    commit('setUser', null)
  },
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


⑧他人のタスクを編集・削除できないようにする。

models/user.rb

class User < ApplicationRecord
  include JwtToken
  authenticates_with_sorcery!

  has_many :tasks, dependent: :destroy
.
.
.
end


models/task.rb

class Task < ApplicationRecord
  belongs_to :user
.
.
.
end


controllers/tasks_controller.rb

class Api::TasksController < ApplicationController
  before_action :authenticate!
  before_action :set_task, only: %i[show update destroy]
  skip_before_action :verify_authenticity_token

  def index
    @tasks = Task.all
    render json: @tasks
  end

  def create
    @task = current_user.tasks.build(task_params)

    if @task.save
      render json: @task
    else
      render json: @task.errors, status: :bad_request
    end
  end
.
.
.
end


javascript/pages/task/index.vue

<template>
.
.
.
    <transition name="fade">
      <TaskDetailModal
        v-if="isVisibleTaskDetailModal"
        :task="taskDetail"
        :auth-user="authUser"  // 追加
        @close-modal="handleCloseTaskDetailModal"
        @show-edit-modal="handleShowTaskEditModal"
        @delete-task="handleDeleteTask"
      />
    </transition>
.
.    
.
</template>

<script>
import TaskList from './components/TaskList'
import TaskDetailModal from './components/TaskDetailModal'
import TaskCreateModal from './components/TaskCreateModal'
import TaskEditModal from './components/TaskEditModal'
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'TaskIndex',
  components: {
    TaskList,
    TaskDetailModal,
    TaskCreateModal,
    TaskEditModal,
  },
  data() {
    return {
      taskDetail: {},
      isVisibleTaskDetailModal: false,
      isVisibleTaskCreateModal: false,
      isVisibleTaskEditModal: false,
      taskEdit: {},
    }
  },
  computed: {
    ...mapGetters('tasks', ["tasks"]),
    ...mapGetters("users", ["authUser"]),  // 追加
.
.
.


javascript/pages/task/components/TaskDetailModal.vue

<template>
  <div :id="'task-detail-modal-' + task.id">
    <div
      class="modal"
      @click.self="handleCloseModal"
    >
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">
              {{ task.title }}
            </h5>
            <button
              type="button"
              class="close"
              @click="handleCloseModal"
            >
              <span>&times;</span>
            </button>
          </div>
          <div
            v-if="task.description"
            class="modal-body"
          >
            <p>{{ task.description }}</p>
          </div>
          <div class="modal-footer d-flex">
            <template v-if="isAuthUserTask">  // 追加
              <button
                type="button"
                class="btn btn-success"
                @click="handleShowTaskEditModal"
              >
                編集
              </button>
              <button
                type="button"
                class="btn btn-danger"
                @click="handleDeleteTask"
              >
                削除
              </button>
            </template>
            <button
              type="button"
              class="btn btn-secondary"
              @click="handleCloseModal"
            >
              閉じる
            </button>
          </div>
        </div>
      </div>
    </div>
    <div class="modal-backdrop show" />
  </div>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  props: {
    task: {
      type: Object,
      required: true,
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      },
      user_id: {   // 追加
        type: Number,
        required: true
      }
    },
    authUser: {   // 追加
      type: Object,
      required: true,
      id: {
        type: Number,
        required: true
      }
    }
  },
  computed: {
    isAuthUserTask() {  // 追加
      return this.task.user_id === this.authUser.id
    }
  },
.
.
.


⑨タスクページはログイン済みでなければ表示しないようにする。また、ログイン状態を保持するようにする。
import Vue from 'vue';
import Router from 'vue-router';
import store from '../store';

import TopIndex from '../pages/top/index';
import TaskIndex from '../pages/task/index';
import RegisterIndex from '../pages/register/index';
import LoginIndex from '../pages/login/index';

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: TopIndex,
      name: 'TopIndex',
    },
    {
      path: '/tasks',
      component: TaskIndex,
      name: 'TaskIndex',
      meta: { requiredAuth: true },  // 追加
    },
    {
      path: '/register',
      component: RegisterIndex,
      name: 'RegisterIndex',
    },
    {
      path: '/login',
      component: LoginIndex,
      name: 'LoginIndex',
    },
  ]
})

// リロード・ページ遷移時に必ずfetchAuthUserメソッドが発動し、トークンが残っていれば
// ログイン状態を保持して指定のページを表示する。
// メタデータで要ログインが指定されていて、かつトークンが残っていない場合は
// ログインページを表示する。
router.beforeEach((to, from, next) => {
  store.dispatch('users/fetchAuthUser').then((authUser) => {
    if (to.matched.some(record => record.meta.requiredAuth) && !authUser) {
      next({ name: 'LoginIndex' });
    } else {
      next();
    }
  })
});

export default router


javascript/store/modules/users.rb

import axios from '../../plugins/axios'

const state = {
  authUser: null
}

const getters =  {
  authUser: state => state.authUser
}

const mutations = {
  setUser: (state, user) => {
    state.authUser = user
  }
}

const actions = {
  async loginUser({ commit }, user) {
    // ログイン
    const sessionsResponse = await axios.post('sessions', user)
    localStorage.auth_token = sessionsResponse.data.token
    axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`

    // ログインユーザー情報の取得
    const userResponse = await axios.get('users/me')
    commit('setUser', userResponse.data)
  },
  logoutUser({ commit }) {
    // ログアウト
    localStorage.removeItem('auth_token')
    axios.defaults.headers.common['Authorization'] = ''
    commit('setUser', null)
  },
  async fetchAuthUser({ commit, state }) {
    if (!localStorage.auth_token) return null
    if (state.authUser) return state.authUser

    const userResponse = await axios.get('users/me')
      .catch((err) => {
        return null
      })
    if (!userResponse) return null

    const authUser = userResponse.data
    if (authUser) {
      commit('setUser', authUser)
      return authUser
    } else {
      commit('setUser', null)
      return null
    }
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}



参考記事:
JWT・Cookieそれぞれの認証方式のメリデメ比較 - Qiita
JSON Web Token(JWT)を用いたAPIの認証の実装(Rails) - Qiita
【Rails】 API開発で『Can't verify CSRF token authenticity』といわれたときの対応 - Qiita
【Nuxt.js】axiosでheaderにAuthorizationを常につけたい! - Qiita
vue-routerでログインしていない場合はログインページに戻す処理 - Qiita
JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用 - Yahoo! JAPAN Tech Blog
JSON Web Tokens - jwt.io
ruby-on-rails — ベアラートークンをレールのヘッダーに渡す方法は?
JWTとは何か?(ruby-jwtのインストール) - 独学プログラマ
【Rails×JWT】トークン発行とデコードを行うAuthTokenクラスの作成 - 独学プログラマ
Ruby - 多重代入 - ざっくりん雑記
ルートメタフィールド | Vue Router
Rubyのmoduleの使い方とメリットを理解して脱初心者!|TECH PLAY Magazine [テックプレイマガジン]
Railsのclass_methodsがやっていること - Qiita

ユーザー登録機能の実装

①sorceryをインストール。
gem 'sorcery'
$ bundle exec rails g sorcery:install
class SorceryCore < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps null: false
    end

    add_index :users, :email, unique: true
  end
end
$ bundle exec rails db:migrate


APIのルーティングを設定。
Rails.application.routes.draw do
  root to: 'home#index'
  namespace :api, format: 'json' do
    resources :tasks
    resources :users
  end
  get '*path', to: 'home#index'
end


③モデルを設定。
class User < ApplicationRecord
  authenticates_with_sorcery!

  has_many :tasks, dependent: :destroy

  validates :password, length: { minimum: 3 }, 
            if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true,
            if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true,
            if: -> { new_record? || changes[:crypted_password] }

  validates :name, presence: true
  validates :email, uniqueness: true, presence: true
end


④コントローラーを設定。
class Api::UsersController < ApplicationController
  def create
    user = User.new(user_params)

    if user.save
      render json: user
    else
      render json: user.errors, status: :bad_request
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
end


⑤vue-routerに登録画面のルーティングを設定してヘッダーにリンクを付ける。
import Vue from 'vue';
import Router from 'vue-router';
import store from '../store';

import TopIndex from '../pages/top/index';
import TaskIndex from '../pages/task/index';
import RegisterIndex from '../pages/register/index';

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: TopIndex,
      name: 'TopIndex',
    },
    {
      path: '/tasks',
      component: TaskIndex,
      name: 'TaskIndex',
      meta: { requiredAuth: true },
    },
    {
      path: '/register',
      component: RegisterIndex,
      name: 'RegisterIndex',
    },
  ]
})

export default router
<template>
  <header>
    <nav class="navbar navbar-expand navbar-dark bg-dark justify-content-between">
      <span class="navbar-brand mb-0 h1">タスク管理アプリ</span>
      <ul class="navbar-nav">
        <li class="nav-item active">
          <router-link
            :to="{ name: 'RegisterIndex' }"
            class="nav-link"
          >
            ユーザー登録
          </router-link>
        </li>
      </ul>
    </nav>
  </header>
</template>

<script>
export default {
  name: "TheHeader",
}
</script>

<style scoped>
</style>


⑥登録画面を実装。

app/javascript/pages/register/index.vue

<template>
  <div
    id="register-form"
    class="container w-50 text-center"
  >
    <div class="h3 mb-3">
      ユーザー登録
    </div>
    <div class="form-group text-left">
      <label for="name">ユーザー名</label>
      <input
        id="name"
        v-model="user.name"
        name="ユーザー名"
        type="text"
        placeholder="username"
        class="form-control"
      >
    </div>
    <div class="form-group text-left">
      <label for="email">メールアドレス</label>
      <input
        id="email"
        v-model="user.email"
        name="メールアドレス"
        type="email"
        placeholder="test@example.com"
        class="form-control"
      >
    </div>
    <div class="form-group text-left">
      <label for="password">パスワード</label>
      <input
        id="password"
        v-model="user.password"
        name="パスワード"
        type="password"
        placeholder="password"
        class="form-control"
      >
    </div>
    <div class="form-group text-left">
      <label for="password_confirmation">パスワード(確認)</label>
      <input
        id="password_confirmation"
        v-model="user.password_confirmation"
        name="パスワード(確認)"
        type="password"
        placeholder="password"
        class="form-control"
      >
    </div>
    <button
      type="submit"
      class="btn btn-primary"
      @click="register"
    >
      登録
    </button>
  </div>
</template>

<script>
export default {
  name: 'RegisterIndex',
  data() {
    return {
      user: {
        name: '',
        email: '',
        password: '',
        password_confirmation: ''
      }
    }
  },
  methods: {
    register() {
      this.$axios.post('users', this.user)
        .then(res => {
          this.$router.push('/login')
        })
        .catch(err => {
          console.log(err)
        })
    }
  }
}
</script>

<style scoped>
</style>

eslintで構文チェック

eslintをインストール

$ yarn add --dev eslint eslint-plugin-vue


プロジェクトのルートディレクトリに.eslintrc.jsを作成して、以下の設定を追記。

module.exports = {
"extends": [
"plugin:vue/recommended",
]
};


eslintのコマンドをpackage.jsonに登録。

"scripts": {
  "lint": "eslint --ext .js,.vue app/javascript",
  "lint-fix": "eslint --fix --ext .js,.vue app/javascript"
}


構文をチェック。

$ yarn run lint


自動補正。

$ yarn run lint-fix

vuexをnamespacedでモジュールに分割

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import tasks from './modules/tasks'
import users from './modules/users'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    tasks,
    users
  }
})


modules/tasks.js

import axios from '../../plugins/axios'

const state = {

}

const getters =  {

}

const mutations = {

}

const actions = {

}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}


modules/users.js

import axios from '../../plugins/axios'

const state = {

}

const getters =  {

}

const mutations = {

}

const actions = {

}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}



参考記事:
Vuexの構造を超簡単にnamespace(名前空間)分割する - Qiita
モジュール | Vuex

slotで親コンポーネントから子コンポーネントにテンプレートを差し込む

コンポーネント

<template>
  <TaskList>
    <template v-slot:header>
      <div class="h4">TODO</div>
    </template>
  </TaskList>

  <TaskList>
    <template v-slot:header>
      <div class="h4">DOING</div>
    </template>
  </TaskList>

  <TaskList>
    <template v-slot:header>
      <div class="h4">DONE</div>
    </template>
  </TaskList>
</template>

<script>
import TaskList from './components/TaskList'

export default {
  name: 'TaskIndex',
  components: { TaskList }
}
</script>


コンポーネント

<template>
  <div>
    <slot name="header">タスク区分</slot>
  </div>
</template>



参考記事:
Vue.jsのslotの機能を初心者にわかるように解説してみた | フューチャー技術ブログ