やりたいこと
アップロードしたい画像を選択したら、プレビュー表示できるようにしたい。
なぜ画像プレビュー機能が必要なのか?
フォームを作っただけでは、画像を選択してもどの画像が選択されたかアップロード前に確認することができない。
アップロードされてから違う画像をアップロードしてしまったと気づくようなことを防ぐために、この機能は必要。
どう進めるか
まず画像プレビュー用のブランチを立ち上げる。
次に「画像 rails プレビュー」で検索して参考記事を探す。
https://qiita.com/Masanori_N/items/71dbf648737f32dd8588
この記事を参考に進めていこう。
作業内容
jQueryで実装するため、jsファイルを作る。
javasciptディレクトリ直下に「image-preview.js」ファイルを設置した。
.new-post
= form_with model: @post, id: 'new_post' do |f|
.input-box
(省略)
.icon
.image-upload
%i.fa.fa-camera.fa-2x
= f.file_field :image, class: 'image_upload'
.btn-square
= f.submit '投稿する', class: 'post-btn'
.posts
= render @posts
画像ファイル選択時にイベントを発火させる
まずはファイルを選択した時にイベントが発火するようにする。
$(function() {
$(function() {
$(document).on('change', 'image_upload', function() {
console.log('hoge');
})
});
});
これだとイベントは発火しなかった。
$(document).on('change', '.image_upload', function() {
console.log('hoge');
})
クラス名の指定で「.」が抜けているというミスが原因だった。
'image_upload'
→'.image_upload'
と修正したら無事イベントは発火した。
画像データの読みこみ
いよいよここから難しくなる。
よくわからない時はconsole.log
で中身を確認するに限る。
$(function() {
$(function() {
$(document).on('change', '.image_upload', function() {
console.log('hoge');
//選択したfileのオブジェクトを取得
var file = this.files[0];
console.log(file);
})
});
});
console.log(file);
で選択したfileを取得できているか確認する。
検証画面で確認すると、
File {name: "image4.png", lastModified: 1596487703899, lastModifiedDate: Tue Aug 04 2020 05:48:23 GMT+0900 (日本標準時), webkitRelativePath: "", size: 110024, …}
と出てきたので、おそらくちゃんと画像fileを取得できている。
次にFileReaderを使ってFileReaderオブジェクトを生成する。
FileReaderとは何なのか?
https://developer.mozilla.org/ja/docs/Web/API/FileReader
によると、
FileReader オブジェクトを使うと、ユーザーのコンピューター内にあるファイル (もしくはバッファ上の生データ) をウェブアプリケーションから非同期的に読み込むことが出来ます。読み込むファイルやデータは File ないし Blob オブジェクトとして指定します。
とある。
なるほど、ローカルにある画像ファイルを非同期で読み込むことができるのか。
//FileReaderオブジェクトの生成
var reader = new FileReader();
これもコンソール画面で確認してみよう。
FileReader {readyState: 0, result: null, error: null, onloadstart: null, onprogress: null, …}
次は
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file);
これもよくわからないのでそのままググってみよう。
https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL
ここでもMDNが参考記事として出てきた。
これもFileオブジェクトを読み込むために必要らしい。
var hoge = reader.readAsDataURL(file);
console.log(hoge);
こんな感じで変数に代入して中身を見てみた。
すると、undefined
となった。
//readAsDataURLで指定したFileオブジェクトを読み込む
var hoge = reader.readAsDataURL(file);
console.log(hoge[0]);
これだとどうか?
これもダメだ。
これは中身を見ることはできないみたい。
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
ここに画像の読みこみ完了後の処理を書いていく
}
これもわからないので調べた。
https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload
FileReader.onload プロパティは、readAsArrayBuffer や readAsBinaryString、 readAsDataURL、readAsText でのコンテンツ読み込みが完了して、利用可能になると発火する load イベント時に実行されるイベントハンドラを含みます
3回読んでも完全にはわからない。
readAsDataURLで画像ファイルの読みこみが完了して、利用可能になるとloadイベントが発火する。
というところまではわかった。
//loadイベントが発火するかを調べる
reader.onload = function() {
console.log('hoge');
}
コンソール画面でちゃんと「hoge」と出てきたので、loadイベントは問題なく発火してるとわかった。
その次にloadイベントの結果を取得する。
//直前に実行したイベントが返した値を取得する
var image = this.result;
プレビュー用のHTMLを設置します。
ここが結構時間かかるかな?と思っていたが、一発でいけました!!
ここまでのコードはこんな感じです。
$(function() {
$(function() {
// プレビューHTML生成
function buildHTML() {
var html = `<div class="preview-box">
<div class="upper-box">
<img src="" alt="preview" class="upload-image">
</div>
<div class="lower-box">
<div class="delete-box">
<span>削除</span>
</div>
</div>
</div>`;
return html;
}
$(document).on('change', '.image_upload', function() {
//選択したfileのオブジェクトを取得
var file = this.files[0];
//FileReaderオブジェクトの生成
var reader = new FileReader();
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file);
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
console.log('hoge');
//直前に実行したイベントが返した値を取得する
var image = this.result;
// プレビュー用のhtmlを追加
var html = buildHTML();
$('.text').append(html);
//画像を追加
$(`.upper-box img`).attr('src', `${image}`);
}
})
});
});
プレビュー画像のサイズ変更
// プレビューHTML生成
function buildHTML() {
var html = `<div class="preview-box">
<div class="upper-box">
<img src="" alt="preview" class="upload-image" height="100px" width="100px">
</div>
<div class="lower-box">
<div class="delete-box">
<span>削除</span>
</div>
</div>
</div>`;
return html;
}
<img src="" alt="preview" class="upload-image" height="100px" width="100px">
imgタグのところにheightとwidthを追加すればOK👍
どのクラスに設定するかが大事で、親クラスに設定してもサイズは変わらないので注意です
プレビュー画像の削除
「削除」を押したらプレビュー画像が消えるようにしたい。
まず「削除」を押した時にイベントが発火するようにする。
$(document).on("click", '.delete-box', function(){
console.log('hogehoge');
})
これで問題なく発火した。
クラス名の指定'.delete-box'
が合っていればこれで発火する。
削除はremoveメソッドを使えばOK。
久しぶりだから( )
をつけるのを忘れていた😅
$(document).on("click", '.delete-box', function(){
$('.preview-box').remove();
})
これだけだと、プレビュー画像は消えたが、まださっきの画像ファイルを選択したままになってる。
// 「削除」を押すと削除イベントが発火する
$(document).on("click", '.delete-box', function(){
// プレビュー画像を削除
$('.preview-box').remove();
// inputタグに入ってる画像ファイルも削除
$('.image_upload').val("");
})
inputタグに入ってる画像ファイルも削除しておこう。
これでプレビュー画像を削除すると、ファイルの中身が空っぽになる。
2回連続ファイルを選択するとプレビュー画像が2つ表示されてしまう問題の解決
こんな感じで表示されてしまう。
これを選択するたびに入れ替わるように修正したい。
どうすればこの問題を解決できるか?
考えたのは、
- すでにプレビュー画像がある場合
- プレビュー画像がない場合
に条件分岐すればいいのでは?
- プレビュー画像あり → プレビュー画像を一度削除する必要あり
- プレビュー画像なし → ここまで書いてきたコードでOK
どうやってプレビュー画像がある場合とない場合の条件分岐をするか?
if($('.preview-box').length == 0){
プレビュー画像がない場合の処理
}else{
プレビュー画像がある場合の処理
}
こんな感じでlengthプロパティを使って条件分岐しました。
// プレビュー画像がまだ場合
if($('.preview-box').length == 0){
// プレビュー用のhtmlを追加
var html = buildHTML();
$('.icon').before(html);
//画像を追加
$(`.upper-box img`).attr('src', `${image}`);
// すでにプレビュー画像が存在する場合
}else{
// プレビュー画像を削除 ⇦ 違うのはここです
$('.preview-box').remove();
// プレビュー用のhtmlを追加
var html = buildHTML();
$('.icon').before(html);
//画像を追加
$(`.upper-box img`).attr('src', `${image}`);
}
参考記事:https://www.sejuku.net/blog/34465
最終的に出来上がったコードはこちら
$(function() {
$(function() {
// プレビューHTML生成
function buildHTML() {
var html = `<div class="preview-box">
<div class="upper-box">
<img src="" alt="preview" class="upload-image" height="100px" width="100px">
</div>
<div class="lower-box">
<div class="delete-box">
<span>削除</span>
</div>
</div>
</div>`;
return html;
}
$(document).on('change', '.image_upload', function() {
//選択したfileのオブジェクトを取得
var file = this.files[0];
//FileReaderオブジェクトの生成
var reader = new FileReader();
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file);
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
//直前に実行したイベントが返した値を取得する
var image = this.result;
// プレビュー画像がまだ場合
if($('.preview-box').length == 0){
// プレビュー用のhtmlを追加
var html = buildHTML();
$('.icon').before(html);
//画像を追加
$(`.upper-box img`).attr('src', `${image}`);
// すでにプレビュー画像が存在する場合
}else{
// プレビュー画像を削除
$('.preview-box').remove();
// プレビュー用のhtmlを追加
var html = buildHTML();
$('.icon').before(html);
//画像を追加
$(`.upper-box img`).attr('src', `${image}`);
}
}
})
// 「削除」を押すと削除イベントが発火する
$(document).on("click", '.delete-box', function(){
// プレビュー画像を削除
$('.preview-box').remove();
// inputタグに入ってる画像ファイルも削除
$('.image_upload').val("");
})
});
});
結局何をしたのか?
- プレビュー画像の表示ができた。
- プレビュー画像のサイズを小さくすることができた。
- プレビュー画像を削除できるようにした。
- 連続でファイルを選択したら、直前に選択したファイルのプレビューが消えて、新たにプレビューが表示されるようにした。
おまけ
各投稿の画像を大きくしたい。
before
after
- if post.image.present?
= image_tag post.image.url, class: 'post-image', width: '300px', height: '200px'
各投稿は部分テンプレートで上記のHTMLで設定しており、image_tagのところにwidthとheightを追加して設定しました。
ただここで問題が。。。
見てわかるとおり、画像が粗くなってしまいました。
これについては解決したらまた別の記事で書きたいと思います。