4
2

[Ruby on Rails]stimulusを用いたプレビュー画像表示機能実装(初学者向け)

Posted at

私は現在、未経験からのエンジニア転職に向けてプログラミングスクールで学習をしている、いしかわと申します。

今回Webアプリケーションの個人開発でstimulusを用いたプレビュー画像表示機能を実装したのでアウトプットとして記事にしたいと思います。
どなたかの参考になれば幸いです。

プログラミング初学者なので、内容に誤り等ある可能性があります
誤りがありましたら教えてくださると幸いです

環境
・Mac M1
・Ruby3.2.2
・Ruby on Rails 7.0.8
※importmapではなくesbuildでバンドルしています

期待する実装内容

入力フィールドから画像を選択した時、その画像がいずれかの場所に表示される
その画像は保存されているわけではないプレビュー画像とする

stimulusコントローラの作成

javascript/controllers配下にimage_preview_controller.jsを新たに作成します

image_preview_controller.js
// stimulusのControllerクラスをインポートすることを宣言
import { Controller } from "stimulus"

// 他のファイルからこのコントローラをインポートできるようにすることを宣言
export default class extends Controller {

    static targets = ["input", "preview"]

    previewImage() {
        const input = this.inputTarget
        const preview = this.previewTarget
        const files = input.files
        const width = this.data.get('width')

        if (files && files[0]) {
        const reader = new FileReader()
        reader.onload = (e) => {
            preview.innerHTML = `<img src="${e.target.result}" style="max-width: ${width}px;">`;
            }
        reader.readAsDataURL(files[0])
        }
    }    
}

ターゲット名を定義

static targets = ["input", "preview"]

inputpreviewをこのコントローラーが操作するDOM要素(ターゲット)として定義しています
後述しますが
inputはプレビュー表示させたいファイルを読み込むfile_field
previewはプレビュー画像を表示させるdivタグを指定します

previewImageメソッドの定義

previewImage() {

ここからプレビュー画像を表示させるpreviewImageメソッドを作成していきます

const input = this.inputTarget
const preview = this.previewTarget
const files = input.files
const width = this.data.get('width')

このメソッド内で使用するために4つの変数を定義します
inputpreviewは先ほど書いた通りプレビュー画像を読み込む要素と、表示させる要素です。
Stimulusはdata-target属性を使ってHTML要素とコントローラのプロパティを結びつけることができます
filesはユーザーが選択したファイルのリストです
widthはプレビュー画像のサイズを任意のサイズに変えることができるよう設定しています。これにより様々なビューファイルでこのstimulusコントローラを使用するのが楽になります

if (files && files[0]) {

ifによる条件式を定義しています
filesinput.fileから取得されているのでFileListオブジェクトです。このFileListオブジェクトを参照した場合trueを返します。
filesは空でもFileListオブジェクトは存在するのでtrueとなります
files[0]filesリストの最初の要素を示しています。つまりfilesがなんらかの要素を持っている場合にtrueを返します

この2つの条件が&&(論理AND)演算子で結ばれているので、ifの条件は「ユーザーが少なくとも1つのファイルを選択していて、それにアクセスできる状態である」となります

const reader = new FileReader()

変数readerFileReaderオブジェクトとして定義しています
FileReaderオブジェクトは非同期にファイルを読み込むことができます

reader.onload = (e) => {}

変数readerにファイル読み込みが完了した(onload)場合、実行される関数を記述しています
この関数に記述されている(e)はイベントオブジェクトであり、今回のようにFileReaderオブジェクトのonloadイベントハンドラで(e)として参照される場合、以下のような情報が含まれています

target: イベントを発生させた`FileReader`オブジェクト自体指します
target.result: FileReader が読み込んだファイルの内容で、Base64エンコーディングされたデータURLです
type: イベントの種類を表します
timeStamp: イベントが発生した時刻をミリ秒単位で示します

特に、e.targetはイベントを発生させたFileReaderオブジェクト自体を指します。e.target.resultには読み込んだファイルの内容が含まれており、これはBase64エンコーディングされたデータURLの形式で提供されます。このデータURLを画像のsrc属性に設定することで、ブラウザ上に画像を直接表示することが可能になります。

preview.innerHTML = `<img src="${e.target.result}" style="max-width: ${width}px;">`;

previewのターゲット要素にHTMLを追加しています。(e)target.result情報を抜き出すことにより、読み込んだファイルの内容をimgタグで出力しています
またstyle="max-width: ${width}pxwidthターゲットを用いてプレビュー画像を任意の幅に調整できるようにしています

reader.readAsDataURL(files[0])

readerに対してreadAsDataURLメソッドを使っています
readAsDataURLメソッドはfileの内容を読み込み、データURLとして返す(Base64エンコーディングされた文字列)メソッドです
ユーザーがfilesからファイルを選択した時、そのファイルはfiles配列の0番目に格納されるためfiles[0]と表すことができます
FileReaderオブジェクトであるreaderfiles[0]の内容を非同期に読み込み、その内容をデータURLとして保持します。
読み込みが完了すると、FileReader オブジェクトの onload イベントが発火します。その結果、読み込まれたファイルのデータは e.target.result を通じてアクセス可能になります。

application.jsへimage_preview_controller.jsを追加

application.js
import { Application } from "stimulus";
import ImagePreviewController from "./controllers/image_preview_controller";

const application = Application.start();
application.register("image-preview", ImagePreviewController);

esbuildを使用しているため、以上の手順でコントローラをapplication.jsに追加します

ビューファイルで使用

今回はユーザーのprofile情報を編集する時、ユーザーのアイコンを変更する場合のプレビュー機能実装を想定してビューファイルを作成しています

edit.html.erb
<div>
    <% if @profile.avatar.attached? %>
        <%= image_tag(@profile.avatar_thumbnail)%>
    <% end %>
    <div data-controller="image-preview" data-image-preview-width="250">
        <%= f.file_field :avatar, data: { "image-preview-target": "input", "action": "change->image-preview#previewImage"}" %>
        <div data-image-preview-target="preview"></div>
    </div>
</div>
<div data-controller="image-preview" data-image-preview-width="250">

image_previewコントローラを使用することを宣言し、widthの値を250としてstimulusコントローラに渡しています
このwidthの値は自由に設定できるので、別のビューページでサイズ違いのプレビューを表示させたい時も同じコントローラを使い回すことで簡単に実装できます

<%= f.file_field :avatar, data: { "image-preview-target": "input", "action": "change->image-preview#previewImage"}" %>

このinput要素がinputターゲットであることを指定しています
また"action": "change->image-preview#previewImage"とすることでchangeイベントが発生したときImagePreviewメソッドが発火するようにしています

<div data-image-preview-target="preview"></div>

この要素にPreviewImageメソッドが発火した時に生成される<img src="${e.target.result}" style="max-width: ${width}px;">を追加することを明示しています

4
2
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
4
2