勉強履歴(と雑記)

プログラミング初心者のメモ書きです

Vue.js学習メモ(画像アップロード)

ログイン機能を実装しているタスク管理アプリで、 個々のユーザーがプロフィール画像を登録できるように、画像アップロード機能を実装することを考えます。
RailsのActiveStorageを使って画像登録します。

①userモデルにプロフィール画像用のカラム(avatar)を設定

user.rb

class User< ApplicationRecord

  has_one_attached :avatar

end

②ユーザーに紐づくプロフィール画像のurlを返すメソッドをモデルに

class User< ApplicationRecord

  def avatar_url
    avatar.attached? ? Rails.application.routes.url_helpers.rails_blob_path(avatar, only_path: true) : 
nil
end

③プロフィールのコントローラーとルーティングを設定

api/profile_controller.rb

class Api::ProfileController < ApplicationController
  before_action :authenticate!

  def update
    user = User.find(current_user.id)
    if user.update(user_params)
      render json: user, methods: [:avatar_url]
    else
      render json: user.errors, status: :bad_request
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :avatar)
  end
end
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resource :profile
  end
end

④userコントローラーにapiのエンドポイントとストロングパラメータを追加

api/users_controller.rb

class Api::UsersController < ApplicationController

  def me
    render json: current_user, methods: [:avatar_url]
  end

  private

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

⑤storeにユーザー更新のAPIリクエストを送る処理を記載

const actions = {
  .
  .
  .
  updateUser({ commit, state }, user) {
    return axios.patch(`profile/${state.authUser.id}`, user)
      .then(res => {
        commit('setUser', res.data)
      })
  }
}

⑥プロフィールページの作成

profile/index.vue

<template>
  <div id="profile-form">
    <ValidationObserver v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" rules="required">
          <label for="name">ユーザー名</label>
          <input 
            id="name" 
            v-model="user.name" 
            type="text" 
            class="form-control" 
            placeholder="username"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" ref="provider" rules="image">
          <label for="avatar" class="d-block >プロフィール画像</label>
          <img :src="user.avatar_url" class="my-3" width="150px">
          <input
            id="avatar"
            type="file"
            accept="image/png,image/jpeg"
            class="form-control-file"
            @change="handleChange"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(update)"
      >
        更新
      </button>
    </ValidationObserver>
  </div>
</template>

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

export default {
  name: 'ProfileIndex',
  data() {
    return {
      user: {
        name: "",
        avatar_url: ""
      },
      uploadAvatar: ""
    }
  },
  computed: {
    ...mapGetters('users', ['authUser'])
  },
  created() {
    this.user = Object.assign({}, this.authUser)
  },
  methods: {
    ...mapActions('users', ['updateUser']),
    async handleChange(event) {
      const { valid } = await this.$refs.provider.validate(event)
      if (valid) this.uploadAvatar = event.target.files[0]
    },
    update() {
      const formData = new FormData()
      formData.append("user[name]", this.user.name)
      if (this.uploadAvatar) formData.append("user[avatar]", this.uploadAvatar)

      try {
        this.updateUser(formData)
        this.$router.push({ name: "TaskIndex" })
      } catch (error) {
        console.log(error);
      }
    },
  }
}
</script>

<style scoped>
</style>

⑦プロフィールページのルーティング

router/index.js

import ProfileIndex from "../pages/profile/index"

const router = new Router({
  routes: [
    .
    .
    .
    {
      path: '/profile',
      component: ProfileIndex,
      name: 'ProfileIndex',
      meta: { requiredAuth: true },
    }
  ]
})

⑧ヘッダーにプロフィール画像の表示+プロフィールページへのリンクを設定

components/TheHeader.vue

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

ハイライトが上手く表示されてないので、次の土日に直します(直した)。