入力フォームで複数選択した画像を送信前に表示する

はじめに

実務にて、ライブラリやフレームワークを利用しないで、入力フォームで複数選択された画像ファイルを送信前に表示してほしいという小仕事の依頼があり、それを解決した情報になります。ソースコードをコピペ(コピー&ペースト)して、ファイル選択用の画像ファイル「plus.png」を用意するだけで簡単に流用ができます。デザインに関しましては、適宜修正をなさってください。

ソースコード

<style>
img {
  width: 150;
  height: 150;
  margin: 10;
  object-fit: cover;
}

label {
  font-size: 5vw;
}

label > input {
  display:none;
}

.output {
  overflow-x: scroll;
  white-space: nowrap;
}

.error {
  color: red;
  font-size: 3vw;
}
</style>
<div id="outputFilesId" class="output">
  <img src="plus.png" onclick="add()">
</div>
<hr>
<span id="error" class="error"></span>
<form enctype="multipart/form-data">
  <label for="inputFilesId">
    写真を追加する
    <input type="file" id="inputFilesId" multiple accept="image/png, image/jpeg">
  </label>
</form>
<hr>

<script>
const MAX_FILE = 6
const MAX_FILE_SIZE = 1e7

let inputFiles = []
let outputFiles = []

inputFilesId.addEventListener('change', e => {
  try {
    inputFiles = Array.from(e.target.files)
    if ((outputFiles.length + inputFiles.length) > MAX_FILE) throw new Error(`${MAX_FILE} 枚以下の画像を選択してください。`)
    inputFiles.forEach(inputFile => {
      if (inputFile.size >= MAX_FILE_SIZE) throw new Error(`10 MB以下の画像を選択してください。(${inputFile.name})`)
      outputFiles.forEach(outputFile => {
        if (outputFile.name === inputFile.name) throw new Error(`同じ画像を選択することはできません。(${outputFile.name})`)
      })
    })
    inputFiles.forEach(file => {outputFiles.push(file)})
    output()
  } catch (e) {
    error.textContent = e.message
  }
})

add = () => inputFilesId.click()

remove = e => {
  outputFiles = outputFiles.filter(file => file.name !== e.getAttribute('data-name'))
  output()
}

output = () => {
  let tmp = []
  outputFiles.forEach(outputFile => {
    tmp += `<img src="${window.URL.createObjectURL(outputFile)}" onclick="remove(this)" data-name="${outputFile.name}">`
  })
  if (outputFiles.length < MAX_FILE) tmp += `<img src="plus.png" onclick="add()">`
  outputFilesId.innerHTML = tmp
  error.textContent = ''
}
</script>

検証環境

解説

ファイルの検証

input 要素の change イベント発生後、addEventListener() メソッドで処理が走る仕掛けを作ります。

<input type="file" id="inputFilesId" multiple accept="image/png, image/jpeg">
inputFilesId.addEventListener('change', e => {
  // 処理
})

例外処理文 を記述します。

try {
  // 処理
} catch (e) {
  error.textContent = e.message
}

Array.from() メソッドで新しい Array インスタンスを複製して変数に代入しています。

let inputFiles = []
inputFiles = Array.from(e.target.files)

Array.length メソッドで、出力ファイルと選択ファイルの合計数が 定数 よりも大きくなっていないか判定します。大きい場合は、例外を throw してエラーメッセージを表示します。

const MAX_FILE = 6
if ((outputFiles.length + inputFiles.length) > MAX_FILE) throw new Error(`${MAX_FILE} 枚以下の画像を選択してください。`)

Array.prototype.forEach() メソッドで各選択ファイルに対して処理が走るようにして…

inputFiles.forEach(inputFile => {
  // 処理
})

File.size でファイル容量を byte 単位で取得、定数 以上の容量になっていないか判定します。ファイル容量が 定数 以上の場合は、例外 throw してエラーメッセージを表示します。

const MAX_FILE_SIZE = 1e7
if (inputFile.size >= MAX_FILE_SIZE) throw new Error(`10 MB以下の画像を選択してください。(${inputFile.name})`)

Array.prototype.forEach() メソッドで各出力ファイルに対して処理が走るようにして…

outputFiles.forEach(outputFile => {
  // 処理
})

選択ファイルの中に出力ファイルと同じファイルが存在するか File.name で判定しています。同じファイルが存在する場合は、例外を throw してエラーメッセージを表示します。

if (outputFile.name === inputFile.name) throw new Error(`同じ画像を選択することはできません。(${outputFile.name})`)

選択ファイルの検証後、Array.prototype.forEach() メソッドで出力ファイルの配列に push しています。

inputFiles.forEach(file => {outputFiles.push(file)})

以上です。下記の関数は、次の「選択ファイルの出力」に続きます。

output()

ファイルの出力

選択ファイルを出力する 関数 を定義します。

outputFiles.forEach(outputFile => {
  // 処理
})

Array.prototype.forEach() メソッドで各出力ファイルに対して処理が走るようにして…

outputFiles.forEach(outputFile => {
  // 処理
})

各出力ファイルの img 要素を生成して変数に代入します。src 属性に URL.createObjectURL() でオブジェクト URL を設定、onclick 属性にファイルを削除する関数(remove)を設定、data-name 属性(カスタムデータ属性)に File.name でファイル名を設定します。

let tmp = []
tmp += `<img src="${window.URL.createObjectURL(outputFile)}" onclick="remove(this)" data-name="${outputFile.name}">`

innerHTML プロパティで生成した img 要素を設定します。

<div id="outputFilesId" class="output">
  ...
</div>
outputFilesId.innerHTML = tmp

最後に、エラーメッセージを空行にします。

error.textContent = ''

以上です。

ファイルの追加(画像クリック)

img 要素の onclick 属性に 関数 を設定、input 要素に対して click() メソッドで click イベントを発生させます。

<img src="plus.png" onclick="add()">
add = () => inputFilesId.click()

以上です。

ファイルの削除

関数 を定義、出力ファイルから filter() メソッドでクリックされた img 要素の data-name 属性(カスタムデータ属性)値と同値以外の新しい配列を生成して再出力します。

remove = e => {
  outputFiles = outputFiles.filter(file => file.name !== e.getAttribute('data-name'))
  output()
}

以上です。

おわりに

開発チームに技術力がない場合、ライブラリやフレームワークを敢えて利用しない手も有りです。

参考