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

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

## やりたいこと
![20240915_recording.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875060/54a3a46d-6e39-79e5-b899-29421081bf27.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])
```
- これもはじめはよく分からなかった記述の一つです
- `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を使ってストロングパラメーターに対応した形式に記述