はじめに
railsのCRUD操作には慣れてきましたが、JavaScriptを使った非同期処理にまだまだ手こずっているので、学習記録として実装した非同期処理を記載します。
特に今回の実装ではchatGPTにかなり助けられた面があるため、なぜそのコードにしているのか、という点も含めて言語化していこうと思います。
やりたいこと
- 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])
- これもはじめはよく分からなかった記述の一つです
-
formData
をFormData
のインスタンスとして作成しています - 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を使ってストロングパラメーターに対応した形式に記述