Vue.jsで上に戻るボタンを実装する

はじめに

Vue.jsで下にスクロールした際に、上に戻れるボタンを実装する方法について。

上に戻るボタンを追加

まずはreturnTop()というクリックイベントを設定します。

<div class="return_top_button btn btn-secondary" @click="returnTop()">
     <font-awesome-icon icon="chevron-circle-up" />
</div>

続いてreturnTop()メソッドの処理を追加します。

<script>
export default {
  name: 'JokeIndex',
  methods: {
    returnTop() {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      })
  }
</script>

<style scoped>
.return_top_button {
    position: fixed;
    right: 15px;
    bottom: 15px;
  }
</style>

window.scrollToメソッドを使って指定の位置までスクロールするように設定します。

behavior: 'smooth'は、ぬるっとスクロールする指定です。

デフォルトだと、一瞬でスクロールされます。

下にスクロールしたときにボタンを表示させる

続いて、下にスクロールしたときだけ、上に戻るボタンを表示させるように設定します。

<transition name="return_button"> // トランジション設定
  <div class="return_top_button btn btn-secondary" @click="returnTop()" v-show="returnTopActive">
     <font-awesome-icon icon="chevron-circle-up" />
  </div>
</transition>

v-showを使って、returnTopActiveがtrueのときのみ表示されるようにします。

<script>
export default {
  name: 'JokeIndex',
  data() {
    return {
      returnTopActive: false,
      scroll: 0,
    }
  },

  mounted() {
    window.addEventListener('scroll', this.scrollWindow)  // スクロールの際に発火
  },

  methods: {
    returnTop() {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      }),

    scrollWindow() {

      this.scroll = window.scrollY  // 縦方向のスクロール量を取得
      if ( 100 <= this.scroll ) {
        this.returnTopActive = true
      } else {
        this.returnTopActive = false
      }
    },
  }
</script>

<style scoped>
  .return_top_button {
    position: fixed;
    right: 15px;
    bottom: 15px;
  }

// enter/leaveトランジションで滑らかに表示・非表示させる
  .return_button-enter-active, .return_button-leave-active {
    transition: opacity .5s;
  }

  .return_button-enter, .return_button-leave-to {
    opacity: 0;
  }
</style>

完成

Image from Gyazo

yarnを使ってVue.jsにFontAwesomeを導入する

はじめに

yarnを使ってVue.jsにFontAwesomeを導入する方法について

導入方法

まずはyarnを使ってFontAwesomeをインストールします。

$ yarn add @fortawesome/fontawesome-svg-core

$ yarn add @fortawesome/free-solid-svg-icons

$ yarn add @fortawesome/vue-fontawesome

続いて、hello_vue.jsファイルにインストールしたものをインポートします。

import Vue from 'vue'
import App from '../app.vue'
import router from '../router'
import axios from '../plugins/axios'
import store from '../store'
import 'bootstrap/dist/css/bootstrap.css'
import '../plugins/veevalidate'

// ここから
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

library.add(faChevronCircleUp)

Vue.component('font-awesome-icon', FontAwesomeIcon)
// ここまで

Vue.config.productionTip = false;

Vue.prototype.$axios = axios

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)
})
import { faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'

library.add(faChevronCircleUp)

上記の2つの部分には使いたいアイコンを指定します。

複数アイコンを使う場合はカンマ区切りでインポートします。

一括でアイコンをインポートする場合は下記のようにします。

import { fas } from '@fortawesome/free-solid-svg-icons'

library.add(fas)

最後に、該当のページで先程インポートしたアイコンを表示します。

<div class="return_top_button btn btn-secondary" v-show="returnTopActive" @click="returnTop()">
    <font-awesome-icon icon="chevron-circle-up" />  // この部分
</div>

GitHub

github.com

Vue.jsでリアルタイム検索機能をつくる

はじめに

Vue.jsでリアルタイム検索機能をつくる方法について

設定方法

<template>
  <div class="container-fluid">
    <div
      id="search-form"
      class="form-row p-3"
    >
      <div class="form-group col-lg-6 offset-lg-3">
        <label for="search">絞り込み</label>
        <input
          id="search"
          v-model="keyword"  // 入力された値を取得する
          type="text"
          placeholder="タスク名を入力してください"
          class="form-control"
        >
      </div>
    </div>
............
............
............
  </div>
</template>

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

export default {
  name: "TaskIndex",
  components: {
    TaskDetailModal,
    TaskCreateModal,
    TaskEditModal,
    TaskList
  },
  data() {
    return {
      taskDetail: {},
      isVisibleTaskDetailModal: false,
      isVisibleTaskCreateModal: false,
      isVisibleTaskEditModal: false,
      taskEdit: {},
      keyword: '',   //  初期値を設定
    }
  },
  computed: {
    ...mapGetters('tasks', [ 'tasks' ]),
    ...mapGetters("users", ["authUser"]),

    todoTasks() {
      return this.filteredTasks.filter(task => {
        return task.status == "todo"
      })
    },

    doingTasks() {
      return this.filteredTasks.filter(task => {
        return task.status == "doing"
      })
    },

    doneTasks() {
      return this.filteredTasks.filter(task => {
        return task.status == "done"
      })
    },

    // 検索欄に文字が入力されるたびにこの算出プロパティが実行される
    filteredTasks() {
      return this.tasks.filter(task => {
        return task.title.indexOf(this.keyword) != -1  // indexOfは該当しない場合は-1を返す
      })
    }
  },
...........
...........
...........
}
</script>

VeeValidateを使ってバリデーションをつくる

はじめに

VeeValidateを使ってバリデーションをつくる方法について

VeeValidateをインストール

$ yarn add vee-validate

VeeValidateの設定ファイルを作成

$ touch app/javascript/plugins/veevalidate.js

上記のファイルをインクルードします。

javascript/packs/hello_vue.js

...
...
import '../plugins/veevalidate'
...
...

VeeValidateの設定

先程作成したplugins/veevalidate.jsファイルに設定を書きます

javascript/plugins/veevalidate.js

import Vue from 'vue'

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

// 使用するバリデーションのルールをインポート
import { required, email, confirmed, min } from 'vee-validate/dist/rules';

// インポートしたルールのエラーメッセージを日本語にする
extend('required', {
  ...required,
  message: "{_field_}は必須項目です"
});

extend('email', {
  ...email,
  message: "{_field_}の形式で入力してください"
});

extend('confirmed', {
  ...confirmed,
  message: "パスワードと一致しません"
});

extend('min', {
  ...min,
  params: ['length'],
  message: "{_field_}は{length}文字以上で入力してください"
});


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

バリデーションをフォームに設定

ユーザー登録フォーム

<template>
  <div
    id="register-form"
    class="container w-50 text-center"
  >
    <div class="h3 mb-3">
      ユーザー登録
    </div>

    // フォーム全体をvalidation-observeで囲む
    // handleSubmitを使ってフォーム送信前にバリデーションを自動で実行する
    <validation-observer v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <label for="name">ユーザー名</label>

        // 各フォームをvalidation-providerで囲む
        <validation-provider
          v-slot="{ errors }"   // エラーメッセージを格納する
          name="ユーザー名"
          rules="required"  // 使用するバリデーションルールを指定する
        >
          <input
            id="name"
            v-model="user.name"
            type="text"
            class="form-control"
            placeholder="username"
          >
          <p class="text-danger">
            {{ errors[0] }}   // 格納したエラーメッセージを表示する
          </p>
        </validation-provider>
      </div>
      <div class="form-group text-left">
        <label for="email">メールアドレス</label>
        <validation-provider
          v-slot="{ errors }"
          name="メールアドレス"
          rules="required|email"
        >
          <input
            id="email"
            v-model="user.email"
            type="email"
            class="form-control"
            placeholder="test@example.com"
          >
          <p class="text-danger">
            {{ errors[0] }}
          </p>
        </validation-provider>
      </div>
      <div class="form-group text-left">
        <label for="password">パスワード</label>
        <validation-provider
          v-slot="{ errors }"
          name="パスワード"
          rules="required"
          vid="password"  // vidを指定
        >
          <input
            id="password"
            v-model="user.password"
            type="password"
            class="form-control"
            placeholder="password"
          >
          <p class="text-danger">
            {{ errors[0] }}
          </p>
        </validation-provider>
      </div>
      <div class="form-group text-left">
        <label for="password_confirmation">パスワード(確認)</label>
        <validation-provider
          v-slot="{ errors }"
          name="パスワード(確認)"
          rules="required|min:3|confirmed:password"  //  vidで指定したフォームの入力内容を取得
        >
          <input
            id="password_confirmation"
            v-model="user.password_confirmation"
            type="password"
            class="form-control"
            placeholder="password"
          >
          <p class="text-danger">
            {{ errors[0] }}
          </p>
        </validation-provider>
      </div>
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(register)"  // handleSubmitでフォーム送信前にバリデーションをチェック
      >
        登録
      </button>
    </validation-observer>
  </div>
</template>

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

<style scoped>
</style>

JWTを使ってトークンベースの認証機能をつくる

はじめに

JWTを使ってトークンベースの認証機能をつくる方法について

JWTをインストール

gem 'jwt'
bundle install

sessionsコントローラの設定

controllers/api/sessions_controller.rb

class Api::SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password]) 
    # sorceryのメソッド

    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

  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

userモデルにモジュールをインクルードします。

class User < ApplicationRecord
  include JwtToken
.
.
end

ログインページを作成

javascript/pages/login/index.vue

<template>
  <div
    id="login-form"
    class="container w-50 text-center"
  >
    <div class="h3 mb-3">
      ログイン
    </div>
    <validation-observer v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <label for="email">メールアドレス</label>
        <validation-provider
          v-slot="ProviderProps"
          name="メールアドレス"
          rules="required|email"
        >
          <input
            id="email"
            v-model="user.email"
            type="email"
            class="form-control"
            placeholder="test@example.com"
          >
          <p class="error text-danger">
            {{ ProviderProps.errors[0] }}
          </p>
        </validation-provider>
      </div>
      <div class="form-group text-left">
        <label for="password">パスワード</label>
        <validation-provider
          v-slot="ProviderProps"
          name="パスワード"
          rules="required|min:3"
        >
          <input
            id="password"
            v-model="user.password"
            type="password"
            class="form-control"
            placeholder="password"
          >
          <p class="error text-danger">
            {{ ProviderProps.errors[0] }}
          </p>
        </validation-provider>
      </div>
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(login)"
      >
        ログイン
      </button>
    </validation-observer>
  </div>
</template>

<script>
import { mapActions } from "vuex"

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

<style scoped>
</style>

上記で使用するloginUserをstoreで定義する

javascript/plugins/axios.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) // /sessionsにpostリクエスト

// 返ってきたトークンをローカルストレージに保存
    localStorage.auth_token = sessionsResponse.data.token

// リクエストの認証ヘッダーにもトークンを保存
    axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`
  },
}

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

axiosのデフォルトヘッダーにトークンを追加 javascript/plugins/axios.js

import axios from 'axios'

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

// デフォルトヘッダーにいれることでapi通信の際にトークンを毎回設定しなくて済む
if (localStorage.auth_token) {
  axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${localStorage.auth_token}`
}

export default axiosInstance

トークンで認証を行う

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

  # トークンをデコードしてuser_idを取得する
    payload, = User.decode bearer_token
    @current_user ||= User.find_by(id: payload['user_id'])
  rescue JWT::ExpiredSignature
    nil
  end

  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

上記のモジュールをインクルードする

controllers/application_controller.rb

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

current_userを返すapiを定義する

class Api::UsersController < ApplicationController
...
...
  def me 
    render json: current_user
  end

  private
...
...
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

ヘッダーを修正する

ログイン前と後でヘッダーの表示項目を変更する。

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 avatar-image-wrapper">
            <img
              :src="authUser.avatar_url"
              class="rounded avatar-image"
            >
          </li>
          <li class="nav-item active">
            <router-link
              :to="{ name: 'ProfileIndex' }"
              class="nav-link"
            >
              プロフィール
            </router-link>
          </li>
          <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>
  .avatar-image-wrapper {
    line-height: 40px;
  }
  .avatar-image {
    width: 20px;
  }
</style>

storeでユーザーのログイン、ログアウトの処理を作成します。

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')
    axios.defaults.headers.common['Authorization'] = ''
    commit('setUser', null)
  },
}

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

タスクの編集・削除の権限を変更

自分の作成したタスクのみ編集・削除できるように修正します。

javascript/pages/task/index.vue

<template>
  <div class="container-fluid">
    <div
      id="search-form"
      class="form-row p-3"
    >
..........
..........    
..........
    <transition name="fade">
      <TaskDetailModal
        v-if="isVisibleTaskDetailModal" 
        :task="taskDetail"
        :auth-user="authUser"  // 追加
        @close-modal="handleCloseTaskDetailModal"
        @show-edit-modal="handleShowTaskEditModal"
        @delete-task="handleDeleteTask"
      />
    </transition>
...........
...........
  </div>
</template>

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

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

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">
            <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
      },
      status: {
        type: String
      },
      user_id: {
        type: Number,
        required: true
      },
    },
    // authUserを受け取る
    authUser: {
        type: Object,
        required: true,
      id: {
        type: Number,
        required: true
      }
    }
  },
  computed: {
    // 自分のタスクかどうかを判定
    isAuthUserTask() {
      return this.task.user_id === this.authUser.id
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    },
    handleShowTaskEditModal() {
      this.$emit('show-edit-modal', this.task)
    },
    handleDeleteTask() {
      this.$emit('delete-task', this.task)
    }
  }
}
</script>

<style scoped>
 .modal {
  display: block;
}
</style>

ログイン状態の保持を設定

javascript/router/index.vue

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";
import ProfileIndex from "../pages/profile/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",
    },
  ],
})

// router.beforeEachを使ってページ遷移時、リロード時に毎回この処理を実行する
// storeの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

storeにfetchAuthUserを定義

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
    }
  },
  updateUser({ commit, state }, user) {
    return axios.patch(`profile/${state.authUser.id}`, user)
      .then(res => {
        commit('setUser', res.data)
      })
  }
}

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

railsアプリにCircleCIを導入する

はじめに

railsアプリにCircleCIを導入してCI部分のRspecとRubocopを自動化してみました。

設定方法

まずはアプリのルートディレクトリに.circleciというディレクトリを作り、その配下にconfig.ymlというファイルを作ります。

そのファイルにcircleciの設定を記述していきます。

version: 2
jobs:
  build: # workflowsを使わない場合はbuildというジョブを定義する
    docker:
      - image: circleci/ruby:2.7.2-node-browsers-legacy # 使用するdockerのイメージを指定する
        environment:
          - BUNDLER_VERSION: 2.1.4 # bundlerのバージョン指定
          - RAILS_ENV: test # 実行するデータベース名を指定
         # database.yml.ciで使用する環境変数を設定
          - MYSQL_HOST: 127.0.0.1
          - MYSQL_USER: root
          - MYSQL_PASSWORD: ''
          - MYSQL_PORT: 3306
          - TZ: "Japan"
      # データベース側の環境
      - image: circleci/mysql:5.7
        environment:
          - MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          - MYSQL_ROOT_HOST: '%'

    working_directory: ~/repo

    steps:
      # githubのリポジトリを取得
      - checkout

      # yarnインストール
      - run:
          name: yarn Install
          command: yarn install

      # キャッシュがあればリストアする
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-

      # bundlerのインストールとbundle install実行
      - run:
          name: Install dependencies
          command: |
            gem install bundler -v 2.1.4
                                              # 並列処理
            bundle install --path=vendor/bundle --jobs 4 --retry 3
      # キャッシュする
      - save_cache:
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle
      
      # データベースのセットアップ
      - run:
          name: Database Setup
          command: |
            mv ./config/database.yml.ci ./config/database.yml 
            bundle exec rake db:create
            bundle exec rake db:schema:load
      # rubocop実行
      - run:
          name: Rubocop
          command: bundle exec rubocop

      # rspec実行
      - run:
          name: Run rspec
          command: |
            mkdir ./tmp/test-results # テスト結果を保存するディレクトリを作成
          # テストを分割して実行する設定
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES
      # 結果を保存
      - store_test_results:
          path: /tmp/test-results
      # テスト結果をCircleCIアプリのARTIFACTSに表示する
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results
test: &default
  adapter: mysql2
  encoding: utf8
  charset: utf8
  pool: 5
  username: <%= ENV.fetch("MYSQL_USER") %>
  password: <%= ENV.fetch("MYSQL_PASSWORD") %>
  host: <%= ENV.fetch("MYSQL_HOST") %>
  port: <%= ENV.fetch("MYSQL_PORT") %>
  database: ci_test

ローカルで動作確認

$ brew install circleci

下記コマンドで.circleci.config.ymlファイルの文法をチェック

$ circleci config validate .circleci/config.yml

下記コマンドで指定したジョブを実行

$ circleci local execute --job build

【エラー】Mysql2::Error: Incorrect string valueの対処法

はじめに

Mysql2::Error: Incorrect string valueの対処法について

該当のエラー

ActiveRecord::StatementInvalid: 
Mysql2::Error: Incorrect string value: .......
..............

原因

character_set_databasecharacter_set_serverの値がlatin1になっていることが原因でした。

mysql> show variables like "chara%";
+--------------------------+-------------------------------------------+
| Variable_name            | Value                                     |
+--------------------------+-------------------------------------------+
| character_set_client     | utf8                                      |
| character_set_connection | utf8                                      |
| character_set_database   | latin1                                      |
| character_set_filesystem | binary                                    |
| character_set_results    | utf8                                      |
| character_set_server     | latin1                                     |
| character_set_system     | utf8                                      |
| character_sets_dir       | /rdsdbbin/mysql-5.7.34.R5/share/charsets/ |
+--------------------------+-------------------------------------------+
8 rows in set (0.01 sec)

この値がlatin1だと日本語ではなくアルファベットで保存されるそう。

なのでこの値をutf8に変更しました。

AWSのRDSを使っていたので、該当のデータベースに関連付けされているパラメータグループから変更しました。

Image from Gyazo