私は現在、未経験からのエンジニア転職に向けてプログラミングスクールで学習をしている、いしかわと申します。
今回Webアプリケーションの個人開発でstimulusを用いたプレビュー画像表示機能を実装したのでアウトプットとして記事にしたいと思います。
どなたかの参考になれば幸いです。
プログラミング初学者なので、内容に誤り等ある可能性があります
誤りがありましたら教えてくださると幸いです
環境
・Mac M1
・Ruby3.2.2
・Ruby on Rails 7.0.8
※importmapではなくesbuildでバンドルしています
期待する実装内容
入力フィールドから画像を選択した時、その画像がいずれかの場所に表示される
その画像は保存されているわけではないプレビュー画像とする
stimulusコントローラの作成
javascript/controllers配下に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"]
inputとpreviewをこのコントローラーが操作する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つの変数を定義します
inputとpreviewは先ほど書いた通りプレビュー画像を読み込む要素と、表示させる要素です。
Stimulusはdata-target属性を使ってHTML要素とコントローラのプロパティを結びつけることができます
filesはユーザーが選択したファイルのリストです
widthはプレビュー画像のサイズを任意のサイズに変えることができるよう設定しています。これにより様々なビューファイルでこのstimulusコントローラを使用するのが楽になります
if (files && files[0]) {
ifによる条件式を定義しています
filesはinput.fileから取得されているのでFileListオブジェクトです。このFileListオブジェクトを参照した場合trueを返します。
※filesは空でもFileListオブジェクトは存在するのでtrueとなります
files[0]はfilesリストの最初の要素を示しています。つまりfilesがなんらかの要素を持っている場合にtrueを返します
この2つの条件が&&(論理AND)演算子で結ばれているので、ifの条件は「ユーザーが少なくとも1つのファイルを選択していて、それにアクセスできる状態である」となります
const reader = new FileReader()
変数readerをFileReaderオブジェクトとして定義しています
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}pxでwidthターゲットを用いてプレビュー画像を任意の幅に調整できるようにしています
reader.readAsDataURL(files[0])
readerに対してreadAsDataURLメソッドを使っています
readAsDataURLメソッドはfileの内容を読み込み、データURLとして返す(Base64エンコーディングされた文字列)メソッドです
ユーザーがfilesからファイルを選択した時、そのファイルはfiles配列の0番目に格納されるためfiles[0]と表すことができます
FileReaderオブジェクトであるreaderはfiles[0]の内容を非同期に読み込み、その内容をデータURLとして保持します。
読み込みが完了すると、FileReader オブジェクトの onload イベントが発火します。その結果、読み込まれたファイルのデータは e.target.result を通じてアクセス可能になります。
application.jsへimage_preview_controller.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情報を編集する時、ユーザーのアイコンを変更する場合のプレビュー機能実装を想定してビューファイルを作成しています
<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;">を追加することを明示しています