0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails + Stimulusで複数画像プレビューを実装した

0
Posted at

私の備忘録として記載しました。
登山ログ投稿機能で、
複数画像プレビューをStimulusで実装した。

特に、

  • 再選択時に画像が消える問題
  • FileList の扱いづらさ

に苦戦したため、
JavaScript側で画像状態を管理する構成にした。

やりたかったこと

  • 画像を1枚ずつ追加
  • 画像の複数枚の同時追加
  • プレビュー表示
  • 任意削除

困ったこと

multiple file input は
再選択時に以前の画像が消えた。

解決方法

selectedFilesをJavaScript側で管理した。

画像プレビューの処理の流れ

  1. file inputで画像選択
  2. input.files(FileList)取得
  3. selectedFilesへ追加
  4. syncInputでinput.filesへ再同期
  5. renderPreviewで画面再描画
ユーザー画像選択
↓
input.files(FileList)
↓
selectedFilesへ保存
↓
syncInput()
↓
input.files再生成
↓
renderPreview()
↓
画面更新

1. ユーザー画像選択

file input で画像を選択するとchangeイベントが発火する。
例えば今回なら、Railsの

<%= f.file_field :images, multiple: true %>

これが最終的にHTMLではこんな感じになります。

<input type="file" multiple>

この input 要素へ
JavaScript側からアクセスすると、

this.imagesTarget

で input 要素を取得できる。

さらに、

this.imagesTarget.files

で FileList を取得した。


2. FileListの取得

this.imagesTarget.files

で、
input要素が保持している
ファイル一覧(FileList)を取得できる。

ただし FileList は通常の配列ではないため、
そのままでは配列メソッドを扱いづらい。

そのため、

Array.from(this.imagesTarget.files)

で通常配列へ変換した。


3. selectedFilesへ保存

this.selectedFiles = [...this.selectedFiles, ...newFiles]

selectedFiles を
JavaScript側の正しい状態として扱った。

これにより、

  • 1枚ずつ追加
  • 任意削除
  • 再描画

を柔軟に行えるようになった。


4. syncInput()

selectedFiles の状態から新しい FileList を生成し、
input.filesへ再同期する。

最終的なフォーム送信時は、
input.files の内容が送信される。

しかし今回は
selectedFiles を本体として管理しているため、

syncInput()

で selectedFiles の状態を
input.files へ再同期した。

const dataTransfer = new DataTransfer()

を使い、
新しい FileList を生成している。


5. renderPreview()

selectedFiles の状態を元に
毎回プレビューHTMLを再生成した。

これにより、
画像削除後も
画面表示と状態を同期しやすくなった。

URL.createObjectURL(file)

で一時URLを生成し、
imgタグへ設定した。

[補足:メモリ管理について]

URL.createObjectURL で生成したURLは、ページを閉じるまでメモリを占有し続けます。プレビューを再描画する前に URL.revokeObjectURL() を呼ぶことで、ブラウザが重くなるのを防ぐことができます。


6. 画面更新

this.previewTarget.insertAdjacentHTML(
  "beforeend",
  previewHtml
)

で preview領域へHTMLを追加し、
現在の selectedFiles の状態を画面へ再描画した。

最終的なコード(一部抜粋)

画像選択時

previewImages() {
  const newFiles = Array.from(this.imagesTarget.files)

  this.selectedFiles = [
    ...this.selectedFiles,
    ...newFiles
  ]

  this.syncInput()
  this.renderPreview()
}

FileList同期

syncInput() {
  const dataTransfer = new DataTransfer()

  this.selectedFiles.forEach(file => {
    dataTransfer.items.add(file)
  })

  this.imagesTarget.files = dataTransfer.files
}

プレビュー再描画

renderPreview() {
  this.previewTarget.innerHTML = ""

  this.selectedFiles.forEach(file => {
    const imageUrl = URL.createObjectURL(file)

    const previewHtml = `
      <img src="${imageUrl}">
    `

    this.previewTarget.insertAdjacentHTML(
      "beforeend",
      previewHtml
    )
  })
}

まとめ

今回の実装では、
「画像を表示する」よりも、

  • どこを正しい状態として持つか
  • どう画面へ同期するか

を考えることが重要だった。

特に selectedFiles を中心に管理する構成にしたことで、
画像追加・削除・再描画をシンプルに扱えるようになった。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?