記事概要
画像ファイルのプレビューを表示させるアプリを作成する。
前提
- Ruby on Railsでアプリケーションを作成している
サンプルアプリ(GitHub)
手順1(JSファイルの作成)
-
app/javascriptディレクトリに、preview.jsを手動作成する -
config/importmap.rbを更新するimportmap.rb# 最終行に追記 pin "preview", to: "preview.js" -
app/javascript/application.jsを更新するapplication.js// 最終行に追記 import "preview"
手順2(プレビュー画像を表示するスペースを作成)
- 特定のページでのみ、JSファイルの処理を行うように記述する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); }); - ブラウザ確認を行う
- フォームを配置するページでは、コンソールに「preview.jsが読み込まれました」と表示されることを確認する
- フォームを配置しないページでは、コンソールに何も表示されないことを確認する
-
app/views/posts/_form.html.erbに、プレビュー画像を表示するスペースを作成する<%= form_with model: @post, id: 'new_post', local: true do |f| %> <%= render 'shared/error_messages', model: f.object %> <div class="message-field"> <%= f.text_field :text, placeholder: 'type a message' %> </div> <div class="image-field"> <div id="previews"></div><!-- 追加 --> <div class="click-upload"> <%= f.file_field :image %> </div> </div> <div class="submit-btn"> <%= f.submit '送信' %> </div> <% end %> - プレビュー画像を表示するスペースのHTML要素を、JavaScript側で取得する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); });
手順3(選択した画像情報を取得する)
- input要素で値の変化が起きた際に呼び出される関数を定義する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); }); }); - ブラウザ確認を行う
- inputの中にある画像を取得する
選択した画像ファイルは、発火したイベントpreview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); }); });eの中の、targetの中の、filesという配列に格納されている - ブラウザ確認を行う
- 画像情報を、変数に格納する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; }); }); - 取得した画像情報のURLを生成する
選択された画像ファイルは、ブラウザ側がpreview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); console.log(blob); }); });blobと呼ばれる形のデータで保持している。window.URL.createObjectURL(file)という記述により、上記データへとアクセスするURLを生成しているため、生成されたURLはblobという変数に代入 - ブラウザ確認を行う
手順4(プレビュー機能を実装する)
- 画像のHTMLを作成する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); console.log(blob); // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); }); }); - 画像URLをimg要素のsrc属性に設定する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); console.log(blob); // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); // 画像URLをimg要素のsrc属性に設定 previewImage.setAttribute('src', blob); }); }); - 生成した要素をブラウザに表示する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); console.log(blob); // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); // 画像URLをimg要素のsrc属性に設定 previewImage.setAttribute('src', blob); // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewList.appendChild(previewWrapper); }); });previewListは、プレビュー画像を表示するスペースのHTML要素 - ブラウザ確認を行う
手順5(画像の再選択機能を追加する)
- 画像を選択し直したら、古いプレビューは削除されるようにする
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; console.log("preview.jsが読み込まれました"); // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[image]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ // 古いプレビューが存在する場合は削除 const alreadyPreview = document.querySelector('.preview'); if (alreadyPreview) { alreadyPreview.remove(); }; console.log("input要素で値の変化が起きました"); // 画像を取得 console.log(e.target.files[0]); // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); console.log(blob); // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); // 画像URLをimg要素のsrc属性に設定 previewImage.setAttribute('src', blob); // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewList.appendChild(previewWrapper); }); }); - ブラウザ確認を行う
手順6(複数枚投稿機能の実装)
手順6-1(DBリセット)
- 複数枚投稿機能を実装すると、以前のデータが原因でエラーとなってしまう。エラー回避のためにデータベースをリセットする
% rails db:migrate:reset
手順6-2(サーバーサイドを修正)
- アソシエーションを設定する
post.rb
class Post < ApplicationRecord # has_one_attached :image has_many_attached :images # 複数枚のファイルを紐付け validates :text, presence: true # validates :image, presence: true validates :images, presence: true end - データ送信方法を複数枚用にするため、
app/views/posts/_form.html.erbのフォームを編集する- 現状、「ファイルを選択」ボタンのname属性は、
post[image]という形になっている。これだと1枚のみ送信可能
-
post[images][]に変更することで、複数同じname属性があった場合に、それぞれが配列の形でパラメーターとして送信される
※[]を付けなかった場合、複数同じname属性があったとしても、最後の1つしか送られない - data属性を追記する
※画像削除等を行う際に、何番目の画像が操作されたのかを判断するために使用する<%= form_with model: @post, id: 'new_post', local: true do |f| %> <%= render 'shared/error_messages', model: f.object %> <div class="message-field"> <%= f.text_field :text, placeholder: 'type a message' %> </div> <div class="image-field"> <div id="previews"></div><!-- 追加 --> <div class="click-upload"> <%#= f.file_field :image %><!-- 削除 --> <%= f.file_field :images, name: 'post[images][]', data: {index: 0} %><!-- 複数ファイル送信可能 --> </div> </div> <div class="submit-btn"> <%= f.submit '送信' %> </div> <% end %>
- 現状、「ファイルを選択」ボタンのname属性は、
- ブラウザの検証ツールを開き、「ファイルを選択」ボタンのhtml構造が下記のようになっていることを確認する
- ストロングパラメーターを編集する
※permitの中でも、posts_controller.rb
# 省略 private def post_params # params.require(:post).permit(:text, :image) params.require(:post).permit(:text, {images: []}) # 複数ファイル送信可能 end # 省略images: []の記述は必ず最後にする。最後以外だとエラー発生 - 画像表示を複数枚投稿機能に対応させる
-
app/views/posts/index.html.erbを編集し、トップページは最初の画像のみを表示するように設定する<h3>トップページ</h3> <h3><%= link_to '新規投稿', new_post_path%></h3> <% @posts.each do |post| %> <div class="posted-content"> <%#= image_tag post.image %> <%= image_tag post.images[0] %><!-- 最初の画像のみを表示 --><br> <%= post.text%><br> <%= link_to '詳細', post_path(post.id)%> </div> <% end %> -
app/views/posts/show.html.erbを編集し、詳細ページは画像の表示を2段構造にし、1段目には1枚目の画像を、2段目には2枚目以降の画像が表示されるように設定する<h3>詳細ページ</h3> <div class="posted-content"> <%#= image_tag @post.image %><!--<br>--><!-- 削除 --> <!-- 複数画像を表示 --> <%= image_tag @post.images[0], class: "main-image" %><br> <div class="other-images"> <% @post.images[1..-1].each do |image| %> <div class="other-image"> <%= image_tag image %> </div> <%end%> </div> <!-- 複数画像を表示 --> <%= @post.text%><br> <%= link_to '編集', edit_post_path(@post.id)%> </div> - CSSを修正する
image.css
img { height: 100px; width: 100px; /* 縦横比を保ったまま、画像を縦横100pxのサイズに収めるプロパティです */ object-fit: contain; } /* 複数枚投稿機能の実装により、追記 */ .other-images { display: flex; } .other-image { margin-right: 10px; } .image-delete-button { text-align: center; border: solid 1px; cursor: pointer; } .main-image { height: 200px; width: 200px; }
-
手順6-3(2枚目以降を選択できるようにする)
- 発火イベントの対象を編集する
preview.js
// 省略 // input要素を取得 //const fileField = document.querySelector('input[type="file"][name="post[image]"]'); const fileField = document.querySelector('input[type="file"][name="post[images][]"]'); // 省略 - 2枚目用のファイル選択ボタンを表示させる
画像1枚につき1つのfile_field(「ファイルを選択」ボタン)が必要なため、1枚目のプレビュー画像表示が終わったら2枚目用のfile_fieldが表示されるようにするpreview.js// 省略 // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewList.appendChild(previewWrapper); // 2枚目用のfile_fieldを作成 const newFileField = document.createElement('input'); newFileField.setAttribute('type', 'file'); newFileField.setAttribute('name', 'post[images][]'); // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); // 省略 - ブラウザで確認すると、1枚目を選択した後に2枚目用の
file_fieldが表示される
- プレビュー画像と追加されたファイル選択ボタンに番号をつける
file_fieldとプレビュー画像それぞれに番号を振り、どのfile_fieldにどのプレビュー画像が紐づいているかを管理する
そうすることで、特定の画像を削除した際などに、セットとなるfile_fieldとプレビュー画像を一緒に操作することができる- 現在は、最初から表示されているfile_fieldのみ、data属性による番号が付与される設定になっている
- file_fieldで値の変化が起きた際に、file_fieldに付与されている番号(data-index)を取得する
preview.js
// 省略 // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ // data-index(何番目を操作しているか)を取得 const dataIndex = e.target.getAttribute('data-index'); console.log( dataIndex); // 古いプレビューが存在する場合は削除 const alreadyPreview = document.querySelector('.preview'); if (alreadyPreview) { alreadyPreview.remove(); }; // 省略 - 取得したdata-indexの値を使用して、プレビュー画像にもdata-indexを付与する
preview.js
// 省略 // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 取得したdata-indexの値を使用して、プレビュー画像にもdata-indexを付与 previewWrapper.setAttribute('data-index', dataIndex); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); // 省略 - 新しく追加されるfile_fieldへ番号を付与する
「最後のfile_fieldのdata-index +1」と計算をしたいので、data-indexを数値に変換した上で計算するpreview.js// 省略 // 2枚目用のfile_fieldを作成 const newFileField = document.createElement('input'); newFileField.setAttribute('type', 'file'); newFileField.setAttribute('name', 'post[images][]'); // 最後のfile_fieldを取得 const lastFileField = document.querySelector('input[type="file"][name="post[images][]"]:last-child'); // nextDataIndex = 最後のfile_fieldのdata-index + 1 const nextDataIndex = Number(lastFileField.getAttribute('data-index')) +1; // +1された値を、新しく追加するfile_fieldのdata-indexの値として渡す newFileField.setAttribute('data-index', nextDataIndex); // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); // 省略:last-childは擬似クラスと呼ばれるキーワードであり、'input[type="file"][name="post[images][]"]という要素の中でも最後の要素という意味
手順6-4(コードを整理)
- console.logを削除
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; // input要素を取得 //const fileField = document.querySelector('input[type="file"][name="post[image]"]'); const fileField = document.querySelector('input[type="file"][name="post[images][]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', function(e){ // data-index(何番目を操作しているか)を取得 const dataIndex = e.target.getAttribute('data-index'); // 古いプレビューが存在する場合は削除 const alreadyPreview = document.querySelector('.preview'); if (alreadyPreview) { alreadyPreview.remove(); }; // 取得した画像ファイルの情報を定義 const file = e.target.files[0]; // 取得した画像情報のURLを生成 const blob = window.URL.createObjectURL(file); // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); // 取得したdata-indexの値を使用して、プレビュー画像にもdata-indexを付与 previewWrapper.setAttribute('data-index', dataIndex); // 表示する画像を生成 const previewImage = document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); // 画像URLをimg要素のsrc属性に設定 previewImage.setAttribute('src', blob); // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewList.appendChild(previewWrapper); // 2枚目用のfile_fieldを作成 const newFileField = document.createElement('input'); newFileField.setAttribute('type', 'file'); newFileField.setAttribute('name', 'post[images][]'); // 最後のfile_fieldを取得 const lastFileField = document.querySelector('input[type="file"][name="post[images][]"]:last-child'); // nextDataIndex = 最後のfile_fieldのdata-index + 1 const nextDataIndex = Number(lastFileField.getAttribute('data-index')) +1; // +1された値を、新しく追加するfile_fieldのdata-indexの値として渡す newFileField.setAttribute('data-index', nextDataIndex); // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); }); }); - 関数ごとに処理を切り分け、コードを見やすく整理する
preview.js
document.addEventListener('turbo:load', function(){ // 新規投稿・編集ページのフォームを取得 const postForm = document.getElementById('new_post'); // プレビューを表示するためのスペースを取得 const previewList = document.getElementById('previews'); // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; // プレビュー画像を生成・表示する関数 const buildPreviewImage = (dataIndex, blob) =>{ // 画像を表示するためのdiv要素を生成 const previewWrapper = document.createElement('div'); previewWrapper.setAttribute('class', 'preview'); previewWrapper.setAttribute('data-index', dataIndex); // 表示する画像を生成 const previewImage= document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); previewImage.setAttribute('src', blob); // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewList.appendChild(previewWrapper); }; // file_fieldを生成・表示する関数 const buildNewFileField = () => { // 2枚目用のfile_fieldを作成 const newFileField = document.createElement('input'); newFileField.setAttribute('type', 'file'); newFileField.setAttribute('name', 'post[images][]'); // 最後のfile_fieldを取得 const lastFileField = document.querySelector('input[type="file"][name="post[images][]"]:last-child'); // nextDataIndex = 最後のfile_fieldのdata-index + 1 const nextDataIndex = Number(lastFileField.getAttribute('data-index')) +1; // +1された値を、新しく追加するfile_fieldのdata-indexの値として渡す newFileField.setAttribute('data-index', nextDataIndex); // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); }; // input要素で値の変化が起きた際に呼び出される関数の中身 const changedFileField = (e) => { // data-index(何番目を操作しているか)を取得 const dataIndex = e.target.getAttribute('data-index'); // 古いプレビューが存在する場合は削除 const alreadyPreview = document.querySelector('.preview'); if (alreadyPreview) { alreadyPreview.remove(); }; const file = e.target.files[0]; const blob = window.URL.createObjectURL(file); buildPreviewImage(dataIndex, blob); buildNewFileField(); }; // input要素を取得 const fileField = document.querySelector('input[type="file"][name="post[images][]"]'); // input要素で値の変化が起きた際に呼び出される関数 fileField.addEventListener('change', changedFileField); });
手順6-5(2枚目以降のプレビュー画像表示を実装)
- 追加されたfile_fieldにイベントをセットする
3枚目以降用の「ファイルを選択」のボタンが、表示されるようになったpreview.js
// 省略 // +1された値を、新しく追加するfile_fieldのdata-indexの値として渡す newFileField.setAttribute('data-index', nextDataIndex); // 追加されたfile_fieldにchangeイベントをセット newFileField.addEventListener("change", changedFileField); // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); // 省略
※プレビュー画像は新しく選択した画像と差し替えになってしまい、まだ正しい挙動にはなっていない - 正しくプレビュー表示するため、既存の画像を削除する下記記述は一旦削除する
preview.js
// 古いプレビューが存在する場合は削除 const alreadyPreview = document.querySelector('.preview'); if (alreadyPreview) { alreadyPreview.remove(); }; - ブラウザで、複数枚のプレビュー画像が表示されていることを確認する
手順6-6(画像の変更機能を実装)
- data-indexを活用した画像差し替えの記述を追記する
preview.js
// 省略 // プレビューを正しく表示させるために削除 const file = e.target.files[0]; const blob = window.URL.createObjectURL(file); // data-indexを使用して、既にプレビューが表示されているかを確認する const alreadyPreview = document.querySelector(`.preview[data-index="${dataIndex}"]`); if (alreadyPreview) { // クリックしたfile_fieldのdata-indexと、同じ番号のプレビュー画像が既に表示されている場合は、画像の差し替えのみを行う const alreadyPreviewImage = alreadyPreview.querySelector("img"); alreadyPreviewImage.setAttribute("src", blob); return null; }; buildPreviewImage(dataIndex, blob); buildNewFileField(); // 省略 - ブラウザにて、画像が差し替えられることを確認する
- 画像差し替え時に画像を選択しなかった場合、file_fieldの中身はなくなるがプレビュー画像だけ表示されたままになる

画像再選択をキャンセルした場合に画像データが消えてしまうのは仕様なため仕方ないが、プレビュー画像が残り、あたかも画像データが残っているように見えてしまうのは避けたい - 画像差し替え時に画像を選択しなかった場合、プレビュー画像を削除する記述を追記する
preview.js
// 省略 // 生成したfile_fieldを表示 const fileFieldsArea = document.querySelector('.click-upload'); fileFieldsArea.appendChild(newFileField); }; // 指定したdata-indexを持つプレビューとfile_fieldを削除する const deleteImage = (dataIndex) => { const deletePreviewImage = document.querySelector(`.preview[data-index="${dataIndex}"]`); deletePreviewImage.remove(); const deleteFileField = document.querySelector(`input[type="file"][data-index="${dataIndex}"]`); deleteFileField.remove(); }; // input要素で値の変化が起きた際に呼び出される関数の中身 const changedFileField = (e) => { // data-index(何番目を操作しているか)を取得 const dataIndex = e.target.getAttribute('data-index'); // プレビューを正しく表示させるために削除 //// 古いプレビューが存在する場合は削除 //const alreadyPreview = document.querySelector('.preview'); //if (alreadyPreview) { // alreadyPreview.remove(); //}; // プレビューを正しく表示させるために削除 const file = e.target.files[0]; // fileが空 = 何も選択しなかったのでプレビュー等を削除して終了する if (!file) { deleteImage(dataIndex); return null; }; const blob = window.URL.createObjectURL(file); // 省略 - 画像差し替え時に画像を選択しなかった場合、プレビュー画像が削除されることを確認する
手順6-7(画像の削除機能を実装)
- 削除ボタンを設置する
- 削除ボタンを生成する
preview.js
// 省略 // 表示する画像を生成 const previewImage= document.createElement('img'); previewImage.setAttribute('class', 'preview-image'); previewImage.setAttribute('src', blob); // 削除ボタンを生成 const deleteButton = document.createElement("div"); deleteButton.setAttribute("class", "image-delete-button"); deleteButton.innerText = "削除"; // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewWrapper.appendChild(deleteButton); //削除ボタンを追加 previewList.appendChild(previewWrapper); // 省略 - CSSファイルを更新する
preview.css
#previews { display: flex; } .preview { margin-right: 30px; } /* 削除ボタン */ .image-delete-button { text-align: center; border: solid 1px; cursor: pointer; margin-bottom: 5px; } - 削除ボタンが表示されることを確認する
- 削除ボタンを生成する
- 削除機能を実装する
preview.js
// 省略 // 削除ボタンを生成 const deleteButton = document.createElement("div"); deleteButton.setAttribute("class", "image-delete-button"); deleteButton.innerText = "削除"; // 削除ボタンをクリックしたらプレビューとfile_fieldを削除させる deleteButton.addEventListener("click", () => deleteImage(dataIndex)); // 生成したHTMLの要素をブラウザに表示させる previewWrapper.appendChild(previewImage); previewWrapper.appendChild(deleteButton); //削除ボタンを追加 previewList.appendChild(previewWrapper); // 省略 - 削除ボタンをクリックしたらプレビュー画像とfile_fieldが削除されることを確認する
手順6-8(投稿できる枚数を制限する)
- フロントサイドで制限する
- 画像の最大枚数を
imageLimitsとして定義するpreview.js// 省略 // 新規投稿・編集ページのフォームがないならここで終了。「!」は論理否定演算子。 if (!postForm) return null; // 投稿できる枚数の制限を定義 const imageLimits = 3; // プレビュー画像を生成・表示する関数 const buildPreviewImage = (dataIndex, blob) =>{ // 省略 - 現在表示されているプレビュー画像の枚数が
imageLimits未満の場合にのみ、新しいfile_fieldが追加されるようにする-
buildNewFileField()を削除するpreview.js// 省略 if (alreadyPreview) { // クリックしたfile_fieldのdata-indexと、同じ番号のプレビュー画像が既に表示されている場合は、画像の差し替えのみを行う const alreadyPreviewImage = alreadyPreview.querySelector("img"); alreadyPreviewImage.setAttribute("src", blob); return null; }; buildPreviewImage(dataIndex, blob); //投稿枚数制限のため、削除 //buildNewFileField(); // 省略 - 下記を追記する
preview.js
// 省略 if (alreadyPreview) { // クリックしたfile_fieldのdata-indexと、同じ番号のプレビュー画像が既に表示されている場合は、画像の差し替えのみを行う const alreadyPreviewImage = alreadyPreview.querySelector("img"); alreadyPreviewImage.setAttribute("src", blob); return null; }; buildPreviewImage(dataIndex, blob); //投稿枚数制限のため、削除 //buildNewFileField(); // 画像の枚数制限に引っかからなければ、新しいfile_fieldを追加する const imageCount = document.querySelectorAll(".preview").length; if (imageCount < imageLimits) buildNewFileField(); // 省略
-
- ブラウザで、3枚以上画像を選択できないことを確認する
- 3枚目を追加後にファイルを削除した場合、新しい「ファイル選択」ボタンが表示されないことを確認する
- 3枚目の画像が削除された場合には、file_fieldをもう一つ生成するように記述する
preview.js
// 省略 // 指定したdata-indexを持つプレビューとfile_fieldを削除する const deleteImage = (dataIndex) => { const deletePreviewImage = document.querySelector(`.preview[data-index="${dataIndex}"]`); deletePreviewImage.remove(); const deleteFileField = document.querySelector(`input[type="file"][data-index="${dataIndex}"]`); deleteFileField.remove(); // 画像の枚数が最大のときに削除ボタンを押した場合、file_fieldを1つ追加する const imageCount = document.querySelectorAll(".preview").length; if (imageCount == imageLimits - 1) buildNewFileField(); }; // 省略 - 3枚目を追加後にファイルを削除した場合、新しい「ファイル選択」ボタンが表示されることを確認する
- 画像の最大枚数を
- サーバーサイドで制限する





