0
0

プロフィール画像の編集を非同期処理で実装する方法

Posted at

はじめに

railsのCRUD操作には慣れてきましたが、JavaScriptを使った非同期処理にまだまだ手こずっているので、学習記録として実装した非同期処理を記載します。
特に今回の実装ではchatGPTにかなり助けられた面があるため、なぜそのコードにしているのか、という点も含めて言語化していこうと思います。

やりたいこと

20240915_recording.gif

  • JavaScriptから画像をPOSTする
  • POST後非同期処理でprofile画像を表示する

具体的な方法

  • axiosとrails-ujsを導入し、javascriptからpost/putする
  • javascriptでプロフィール画像の設定状況を確認し、条件に応じて画像の出し分けをする

前提

  • rails6を使用
  • UserとProfileは1対1の関係
  • profileはユーザー登録時に自動で作成されない

コードとポイント

show.html.haml

%p= @user.username
.imgWrapper
  - if @profile.avatar.attached?
    = image_tag @profile.avatar, class: 'imgWrapper_avatar hidden', id: 'avatar-image'
  = image_tag 'default_avatar.png', class: 'imgWrapper_avatar hidden' , id:'default-avatar'

#modal-overlay
  #modal-content
    %p アバターをアップロード
    = file_field_tag :avatar, id: 'avatar-input'
    %button{ id: 'uploadAvatarBtn' } Upload Avatar

    %button#close-modal 閉じる

ポイント

if @profile.avatar.attached?    
  • この記述がないとprofileが存在しない場合や、アバターが存在しない場合にエラーが出ます。
= image_tag @profile.avatar, class: 'imgWrapper_avatar hidden', id: 'avatar-image'
= image_tag 'default_avatar.png', class: 'imgWrapper_avatar hidden' , id:'default-avatar'
  • 別途javascriptでhiddenクラスを取り除くことで表示させられるようにしています
  • htmlの状態としては2️つの画像が存在しており、デフォルトでは両方非表示化しています
#modal-overlay
  #modal-content
    %p アバターをアップロード
    = file_field_tag :avatar, id: 'avatar-input'
    %button{ id: 'uploadAvatarBtn' } Upload Avatar

    %button#close-modal 閉じる
  • jQueryでモーダルとして表示できるようにしています
  • javascriptで処理するためform_withは不要です
  • id: 'avatar-input'から画像データを取得し、id: 'uploadAvatarBtn'のクリックをトリガーにしてputを実行できるようにしています

profiles_controller.rb

class ProfilesController < ApplicationController
  before_action :authenticate_user!
  skip_before_action :check_avatar, only: [:edit, :show, :update]

  def show
    @user = current_user
    @profile = @user.prepare_profile
  end

  def update
    @profile = current_user.prepare_profile
    @profile.assign_attributes(profile_params)
    if @profile.save
      render json: { message: 'Profile updated successfully' }
    else
      render json: { error: 'Failed to update profile' }
    end
  end

  def edit
    profile = current_user.profile
    avatar_status = profile&.avatar&.attached? || false
    render json: {
      hasAvatar: avatar_status,
      avatarUrl: url_for(profile&.avatar)
    }
  end

  private

  def profile_params
    params.require(:profile).permit(:avatar)
  end

end

user.rb

  def prepare_profile
    profile || build_profile
  end

ポイント

  def prepare_profile
    profile || build_profile
  end
 def update
    @profile = current_user.prepare_profile
    @profile.assign_attributes(profile_params)
    if @profile.save
  • 今回はnew,createアクションを設定せず、updateにcreate的な働きも持たせています
  • そのため、プロフィールが存在しない場合にはbuildするように設計しています
  • 既存のprofileを引っ張ってきている可能性と、新しいprofileをbuildしている可能性がどちらもあるため、@profile.updateではなく、assign_attributesでパラメータを渡し、それをsaveする設計にしています
  def edit
    profile = current_user.profile
    avatar_status = profile&.avatar&.attached? || false
    render json: {
      hasAvatar: avatar_status,
      avatarUrl: url_for(profile.avatar)
    }
  end

avatar_status = profile&.avatar&.attached? || false

  • profileが未作成の場合またはアバター画像が登録されていない場合にfalseを返すようにしています
  • javascript側でhasAvatarがtrueかfalseかによって画像を出し分ける際に使用します

avatarUrl: url_for(profile&.avatar)

  • このControllerで一番最後に記載した箇所です
  • url_forメソッドでprofile.avatarのURLを取得しています
  • これがあることにより、javascript側で表示したい画像のsrc属性に新しく設定したプロフィール画像のURLを指定できます
  • 端的に言うと、新しく設定した画像を非同期処理で表示できるようになります
  • 細かい点ですが、profileは存在しない可能性があるのでボッチ演算子を使用しています

application.js

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

import $ from 'jquery'
import axios from 'axios'
import { csrfToken } from 'rails-ujs'

axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

const initializeModal = () => {
  // モーダルが表示されないように最初に非表示に設定
  $('#modal-overlay').hide()

  // 画像クリックでモーダル表示
  $('.imgWrapper_avatar').on('click', () => {
    $('#modal-overlay').show() // モーダルを表示
  })

  // モーダルの閉じるボタンでモーダルを非表示にする
  $('#close-modal').on('click', () => {
    $('#modal-overlay').hide() // モーダルを閉じる
  })

  // 背景をクリックしてもモーダルを閉じる
  $('#modal-overlay').on('click', (e) => {
    if (e.target === e.currentTarget) { // モーダルの外側をクリックした場合のみ閉じる
      $(e.currentTarget).hide();
    }
  })
}

const closeModal = () => {
  $('#modal-overlay').hide()
}

const showAvatar = () => {
  axios.get('/profile/edit')
  .then(response => {
    const avatarStatus = response.data.hasAvatar
    const avatarUrl = response.data.avatarUrl

    if (avatarStatus === true && avatarUrl) {
      $('#avatar-image').attr('src', avatarUrl).removeClass('hidden')
    } else {
      $('#default-avatar').removeClass('hidden')
    }
  })
}

const uploadAvatar = () => {
  // inputタグからファイルを取得
  const fileInput = $('#avatar-input')[0]

  // ファイルが選択されていない場合にアラートを表示
  if(!fileInput.files.length) {
    alert('No file selected!')
    return
  }

  const formData = new FormData()
  formData.append('profile[avatar]', fileInput.files[0])

  axios.put('/profile', formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
  .then(response => {
    console.log('File uploaded successfully:', response.data)
    showAvatar()
    closeModal()

  })
  .catch(error => {
    console.error('Error uploading file:', error);
  })
}


$(document).on('turbolinks:load', () => {
  initializeModal()
  showAvatar()

  $('#uploadAvatarBtn').on('click', (event) => {
    event.preventDefault()
    uploadAvatar()
  })
})

ポイント

const fileInput = $('#avatar-input')[0]
  • jQueryに慣れていないので、特に[0]の意味がわからなかったです
  • 今回はidで指定しているの一意に決まりますが、class名などで指定した際に複数の要素を取得する可能性があります
  • そのためjQueryオブジェクトは複数の要素を配列として扱えるようになっているみたいです
  • ゆえに、$('#avatar-input')[0]はDOM要素としてのid='avatar-inputを取得しています
  • こうすることで.filesや.valなども使用可能になります
  const formData = new FormData()
  formData.append('profile[avatar]', fileInput.files[0])
  • これもはじめはよく分からなかった記述の一つです
  • formDataFormDataのインスタンスとして作成しています
  • FormDataはrailsなどのバックエンドにフォームで扱うようなデータを送るために使うオブジェクトのようです
  • profile[avatar]と記述することで、rails側にはparams[:profile][:avatar]として処理されます
  • これを使うことでストロングパラメーターの設定をパスできます
 axios.put('/profile', formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
  • 第1引数はPath、第2引数は受け渡したいデータ、第3引数はformDataが画像ファイルを含むことを明示しています
  • 第3引数はデフォルトではテキスト形式のみを受け付けるようなので、画像ファイルも扱えるようにmultipart/form-dataを指定しています

学びになったこと

  • javascrip側(フロントエンド)とController側(バックエンド)の情報の行き来をイメージしながら記述を進めることでjavascriptの書き方が腑に落ちた感じがあります
  • フォームからの送信内容をjavascriptで取得し、テーブルに送るやり方
    • $( '#inputTafId' )[0] でinputタグに入力したデータを取得
    • FormDataを使ってストロングパラメーターに対応した形式に記述
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0