【Rails】画像プレビュー機能をJavaScriptで実装

# はじめに
この記事ではRailsでアップロードした画像をJavascriptでプレビュー表示させる方法についてまとめています。すでに画像のアップロード自体はできている方を前提として書いておりますのでご了承ください。JSについては不慣れなところがあり恐縮です。

https://railsguides.jp/active_storage_overview.html

# 対象者
### 環境
- Ruby 3.2.3
- Rails 7.1.3.2
- esbuildで開発
- Tailwindcss使用

### 前提
- Active Storageの導入が完了している
- 画像のアップロードができている
- 1つのフォームでなく複数のフォームでプレビュー表示させたい方

↓参考記事

https://qiita.com/taki39384/items/691017789c1ca594f308

# 完成品

1つだけでなく複数のフォームに対応できます。

### 新規作成画面

[![Image from Gyazo](https://i.gyazo.com/23827c2db3f0b081f3222b8b52b83c7c.gif)](https://gyazo.com/23827c2db3f0b081f3222b8b52b83c7c)

プレビュー画像を1枚のみ表示します。

### 編集画面

[![Image from Gyazo](https://i.gyazo.com/c715431857fe1ac4dca4d9d2fb58f9e0.gif)](https://gyazo.com/c715431857fe1ac4dca4d9d2fb58f9e0)

すでに登録されている画像を表示させて、新しくアップロードされると変更されます。

# プレビュー表示

ここではまず```new.html.erb```にプレビュー表示させる方法についてまとめます。

### フォーム

フォームとプレビュー表示させる箇所にIDをつけます。

#### フォームにIDをつける
```
<%= form_with model: インスタンス変数, id: '任意_form' do |f| %>
```

#### プレビュー表示用
```
<div id="previews_任意" class="">
    <% if @インスタンス変数.persisted? && @インスタンス変数.image.attached? %>
        <div class="preview">
            <%= image_tag @インスタンス変数.image, class: '' %>
        </div>
    <% end %>
</div>
```
「**persisted?**」と「**attached?**」は「**もし保存されていて、画像がついていたら、**」という意味で、条件に一致するならコードが実行されます。

https://railsdoc.com/page/model_persisted_q

https://railsguides.jp/active_storage_overview.html#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3

このコードの記述箇所としては、アップロードフィールド(**file_field**)の近くで良いかと思います。

私の書いたコードはこちらです。
```:_form.html.erb
<!-- form_withにidをつけます -->
<%= form_with model: @tackle, id: 'tackle_form', class: 'md:mt-10 mt-5' do |f| %>

    <!-- バリデーションエラーメッセージ -->
    <%= render 'shared/error_messages', object: f.object %>
    
    <div class='md:mt-10 mt-5'>
        <%= f.label :name, class: 'text-black font-bold md:text-xl text-lg required' %>
        <%= f.text_field :name, class: 'shadow appearance-none border-2 border-blue rounded w-full md:py-2 py-1 md:px-3 px-2 text-black leading-tight focus:outline-none focus:shadow-outline', placeholder: t('.placeholder.name') %>
    </div>

    <!-- ファイルのアップロードフィールド -->
    <div class='md:mt-10 mt-5'>
        <%= f.label :image, class: 'text-black font-bold md:text-xl text-lg' %>
        <%= f.file_field :image, class: 'shadow appearance-none border-2 border-blue rounded w-full md:py-2 py-1 md:px-3 px-2 text-black leading-tight focus:outline-none focus:shadow-outline' %>
    </div>

    <!-- プレビュー表示用に記述 -->
    <div class="md:mt-10 mt-5" id="previews_tackle">
        <% if @tackle.persisted? && @tackle.image.attached? %>
            <div class="preview">
                <%= image_tag @tackle.image, class: 'preview-image' %>
            </div>
        <% end %>
    </div>

    <div class='md:mt-8 mt-4 text-center'>
        <button type="submit" class="md:text-base text-sm btn font-bold bg-blue text-white hover:bg-blue-700 md:py-2 py-1 md:px-10 px-5 rounded focus:outline-none focus:shadow-outline"><%= t('.submit') %></button>
    </div>
<% end %>
```

こちらを参考に、ご自身のインスタンス変数、クラスに合わせてください。

また、こちらのフォームは```new.html.erb```と```edit.html.erb```で使うために「**パーシャル/レンダリング**」していますのでご注意ください。

https://railsguides.jp/layouts_and_rendering.html

### JS
プレビューファイルを作成して読み込みます。

#### preview.js

```app>javascript>preview.js```を作成します。

そして下記のように記述してプレビューを機能させます。

```:preview.js
document.addEventListener('turbo:load', function() {
  const forms = document.querySelectorAll('form[id$="_form"]');

  forms.forEach(form => {
    const fileField = form.querySelector('input[type="file"][name$="[image]"]');
    if (!fileField) return;

    const previewList = form.querySelector(`#previews_${form.id.split('_')[0]}`);

    fileField.addEventListener('change', function(e) {
      const alreadyPreview = previewList.querySelector('.preview');
      if (alreadyPreview) {
        alreadyPreview.remove();
      }

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);

      const previewWrapper = document.createElement('div');
      previewWrapper.setAttribute('class', 'preview');

      const previewImage = document.createElement('img');
      previewImage.setAttribute('class', 'preview-image');
      previewImage.setAttribute('src', blob);

      previewWrapper.appendChild(previewImage);
      previewList.appendChild(previewWrapper);
    });
  });
});
```

書いている内容はこちらの記事とほぼ同じですが、複数のフォームで使うために変えています。

https://qiita.com/taki39384/items/691017789c1ca594f308

一部のみ解説すると

- 2行目：**id**が```_form```で終わるフォームを全て取得
```
const forms = document.querySelectorAll('form[id$="_form"]');
```
- 4行目：```input[type="file"][name$="[image]"]```を持つファイル入力フィールドを探す
```
const fileField = form.querySelector('input[type="file"][name$="[image]"]');
```
具体的に```<%= f.file_field :image, class: '' %>```を取得します。
- 6行目：プレビューを表示するためのエリアを特定し、**id**が```previews_```で始まる要素を取得
```
const previewList = form.querySelector(`#previews_${form.id.split('_')[0]}`);
```

:::note warn
同じWebページにおいて**ID**は**一意(ユニーク)** である必要があります。重複がないようにします。
:::


#### preview.js読み込み

```app>javascript>application.js```で読み込みます。
```
import "./preview"
```

#### プレビューサイズの調整

[![Image from Gyazo](https://i.gyazo.com/6915088481fdf16edc4760c63387a7f2.png)](https://gyazo.com/6915088481fdf16edc4760c63387a7f2)

検証モードより```preview-image```というクラスが当たっているので、直接CSSファイルに指定します。
```:application.tailwind.css
.preview-image {
  width: 100%;
  height: auto;
}
```
ここは好きなようにCSSを当ててください。

# おわりに
JSは難しいですが、なんとかできました。この記事がお役に立てば嬉しいです。

この記事を通じて気づいたことやご意見がありましたら教えてくださいますと幸いです。

# 参考

https://qiita.com/taki39384/items/691017789c1ca594f308

https://railsdoc.com/page/model_persisted_q

https://railsguides.jp/active_storage_overview.html#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3

https://railsguides.jp/layouts_and_rendering.html