私は現在、未経験からのエンジニア転職に向けてプログラミングスクールで学習をしている、いしかわと申します。
今回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;">
を追加することを明示しています