3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Ruby on Rails]stimulusで画像プレビュー機能を実装[Javascript]

Posted at

はじめに

この記事はRuby on Railsでアップロードした画像をhotwire/stimulusを使って画像プレビューを表示させるやり方をまとめています。
Active Storageを使用した画像アップロード機能を実装済みであり、問題なく画像をアップロードできる状態前提で書いていますのでご了承ください。

実装したプレビューは画像一枚のみ表示です。

環境

Rails 7.2.2.2
Ruby 3.3.6
stimulus-rails 1.3.4
Docker環境

完成品

スクリーンショット 2025-10-30 21.58.32.png

hotwire/stimulusの事前確認

ディレクトリはこのようになっています。
app/javascript/
├─ application.js
└─ controllers/
    ├─ index.js  ← Stimulusのエントリーファイル
    └─ preview_controller.js   ← プレビューコントローラ


app/javascript/application.js

import "@hotwired/turbo-rails"
import "./controllers"
import "../assets/stylesheets/application.tailwind.css"

このファイルはすでにデフォルトでこのように記載されていると思います。
controllersディレクトリを読み込む設定import "./controllers"があるか確認してください。

preview_controller.jsファイル生成、プレビュー機能実装

rails generate stimulus previews

app/javascript/controllers/previews_controller.jsが生成されます。
app/javascript/controllers/index.jsには

index.js
import { application } from "./application"
// 下二行が追加
import PreviewsController from "./previews_controller"
application.register("previews", PreviewsController)

同じディレクトリにあるpreviews_contorollerを読み込む設定が追記されると思います。

app/javascript/controllers/previews_controller.js

previews_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  // ターゲットの定義
  static targets = ["input", "preview", "image"]
  // デバッグ用のログ出力
  connect() {
    console.log("Preview controller connected")
  }

  preview(event) {
    //選択された最初のファイルを変数fileに格納
    const file = event.target.files[0]
    const maxSizeInBytes = 10 * 1024 * 1024 // 10MB
    const validTypes = ["image/jpeg", "image/jpg", "image/png"]

    // MIMEタイプのチェック
    if (!validTypes.includes(file.type)) {
     // 不正なファイル形式の場合のアラート表示
      alert("JPEG、JPG、PNG形式のファイルを選択してください。")
      this.removeImage()
      return
    }
    // ファイルサイズのチェック
    if (file.size < maxSizeInBytes ) {
      //readerをfilereaderオブジェクトとして定義
      const reader = new FileReader()
      // ファイルをデータURLとして読み込む
      reader.readAsDataURL(file)
      // readerにファイル読み込み完了時の処理を定義
      reader.onload = (e) => {
        // 読み込んだデータをプレビュー画像のsrcに設定,e.targetでイベントを発生させたFilereaderオブジェクトを取得
        this.imageTarget.src = e.target.result
        // プレビューエリアのhiddenクラスを削除して表示
        this.previewTarget.classList.remove("hidden")
      }
    } else {
      // ファイルが大きすぎる場合のアラート表示
      alert("ファイルサイズは10MB以下にしてください。")
      this.removeImage()
    }
  }

  // 画像削除処理
  removeImage() {
    this.inputTarget.value = null
    this.imageTarget.src = null
    this.previewTarget.classList.add("hidden")
  }

  // デバッグ用
  disconnect() {
    this.revokeCurrentObjectURL()
    console.log("Preview controller disconnected")
  }
}

view

app/views/posts/_image_preview.html.erb

_image_preview.html.erb
<!-- Stimulusコントローラーを適用 -->
<div class="mb-6" data-controller="previews">
  <%= form.label :image, "画像", class: "block text-sm font-medium text-gray-700 mb-2" %>
  <p class="text-sm text-black mb-4">※JPEG/JPG/PNG形式の画像のみ,10MB以下</p>

<!-- 既存の画像があれば表示(編集時のみ) -->
  <% if post.persisted? && post.image.attached? %>
    <div class="current-image mb-4">
      <p class="text-sm text-gray-600 mb-2">現在の画像:</p>
      <div class="relative inline-block">
        <%= image_tag post.image,
          alt: "現在の画像",
          class: "max-w-full h-auto rounded-lg shadow-md max-h-64" %>
      </div>
    </div>
    <p class="text-sm text-gray-600 mb-2">新しい画像を選択する場合:</p>
  <% end %>

<!-- ファイル入力フィールド -->
  <%= form.file_field :image,
    accept: "image/jpeg, image/jpg, image/png",
      data: {
              previews_target: "input",
              action: "change->previews#preview"
            } %>

<!-- プレビュー表示エリア -->
  <div data-previews-target="preview" class="mt-4 hidden">
    <div class="relative inline-block">
      <img data-previews-target="image"
           src=""
           alt="プレビュー画像"
         class="max-w-full h-auto rounded-lg shadow-md max-h-64">
        <button type="button"
          data-action="click->previews#removeImage"
          class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm hover:bg-red-600">
          ×
        </button>
    </div>
  </div>
</div>

解説

はじめに、このview全体をdata-controller="previews"で囲うことでStimulusにこの要素に previews コントローラーを適用することを宣言します。
jsファイルのstatic targets = ["input", "preview", "image"]でviewのターゲット(data-previews-targetの部分)を定義します。

  • ユーザーがファイルを選択
    ファイル選択フィールドの内容が変わる(ユーザーがファイルを選択)とpreviews#previewが発火します。
    previewメソッドではMIMEタイプ、ファイルのサイズをチェックしています。問題なければ、FilePreaderを使って選択されたファイルをBase64のデータURLに変換します。
    (ちなみにMIMEとは、テキスト以外の様々な種類のデータ(例えば、画像や音声ファイル)を取り扱うための標準規格のことです。base64とは、64進数を意味する言葉で、すべてのデータをアルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表すエンコード方式のことであり、MIMEの規格ではこのエンコード方式を用いるよう定められています。)
    reader.onloadでreaderの読み込み完了後に、画像のデータURLの入ったe.target.resultを、imageターゲットにあるタグのsrc属性に代入しています。
    最後にプレビュー表示エリアの非表示状態を解除してプレビューを画面上に表示します。

  • プレビューの×を押して画像を削除
    プレビューで表示された画像の右上にある×を押すとpreviews#removeImageが発火します。
    inputターゲットのvalue、imageターゲットのsrc属性をnullにし、プレビュー表示エリアを非表示にします。

参考URL

3
1
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?