はじめに
ポートフォリオのサイトを制作中、画像アップロード機能をつけていたのですが、プレビュー機能がないため、ちゃんと任意の画像が選択できていたのか分かりづらい問題がありました。
そのため画像プレビュー機能をつけてみようと思い、実装してみました。
課題は何点かあるものの、実装できたので、まとめていきたいと思います。
前提
- Ruby on Rails 6.0.2
- jQiery 3.5.1
- Bootstrap4
機能詳細
画像アップロードの流れ
- ファイル入力のinputタグ押下
- 画像選択
- プラビュー画像を選択した画像に差替
バリデーション時・投稿編集時
- 選択したファイルは保存された画像として大きく表示
- 「画像を変更する」ボタン押下→プレビュー画像とinputタグをtoggleで表示・非表示
実装
View
app/views/posts/_form.html.erb
<%= form_with(model: post, local: true) do |form| %>
<!--・・・省略・・・-->
<div class="form-file custom-file mb-3">
<% if post.image.present? %>
<div class="form-image-uploader__saved-img">
<span class="form-image-uploader__saved-img-inner">
<%= image_tag post.image.to_s %>
</span>
</div>
<div class="block-toggle">
<div class="block-toggle__press">
<div class="btn btn-outline-secondary">
<i class="fas fa-arrow-circle-down"></i> 画像を変更する
</div>
</div>
<div class="block-toggle__content" style="display:none;">
<label for="post_image" class="form-image-uploader__preview">
<%= image_tag 'nophoto.jpg' %>
</label>
<%= form.file_field :image, class:'form-image-uploader__save' %>
<%= form.hidden_field :image_cache, class:'form-image-uploader__cache' %>
</div>
</div>
<% else %>
<label for="post_image" class="form-image-uploader__preview">
<%= image_tag 'nophoto.jpg' %>
</label>
<%= form.file_field :image, class:'form-image-uploader__save' %>
<%= form.hidden_field :image_cache, class:'form-image-uploader__cache' %>
<% end %>
</div>
<!--・・・省略・・・-->
<% end %>
JS
ImgUplorader
クラスのthis.noPhotoImgPath
は任意のパスを設定します。
app/javascript/packs/application.js
/*Slide Toggle
------------------------------------------------------*/
$(document).on('turbolinks:load', () =>{
$('.block-toggle__press .btn').on('click', event =>{
$(event.currentTarget).parent('.block-toggle__press').next('.block-toggle__content').slideToggle(700);
});
});
/*Image uplorader
------------------------------------------------------*/
$(document).on('turbolinks:load', () =>{
const imgUplorader = new ImgUplorader;
imgUplorader.copyToSaveInput();
});
class ImgUplorader{
constructor(){
this.selectorPreview = '.form-image-uploader__preview';
this.selectorSave = '.form-image-uploader__save';
this.selectorCache = '.form-image-uploader__cache';
this.noPhotoImgPath = '/assets/nophoto.jpg';// <= nophoto画像のパスを設置
}
/*
* Change preview image to nophoto image when image is not selected
* @param input : Element of current target
*/
copyToSaveInput(){
$(document).on('change', this.selectorSave, event => {
const input = $(event.currentTarget);
const filesLength = input[0].files.length;
const cacheDefaultVal = $(input).next(this.selectorCache)[0].defaultValue;
// Change preview image to nophoto image when image is not selected
if (this.hasNotImg(filesLength)) {
this.changeNoPhotoImg(input);
return;
}
// Change preview image to selected image when image is selected
this.changeSelectedImg(input);
});
}
/*
* Return true when input doesn't have file
* @param filesLength : file length of input
* @return bool
*/
hasCacheDefaultImg(filesLength){
if(filesLength == 0){
return true;
}
return false;
}
/*
* Return true when input doesn't have file
* @param filesLength : file length of input
* @return bool
*/
hasNotImg(filesLength){
if(filesLength == 0){
return true;
}
return false;
}
/*
* Change preview image to nophoto image when image is not selected
* @param input : Element of current target
*/
changeNoPhotoImg(input){
$(input).prev(this.selectorImg).children('img').attr('src', this.noPhotoImgPath);
}
/*
* Change preview image to selected image when image is selected
* @param input : Element of current target
*/
changeSelectedImg(input){
const reader = new FileReader();
reader.onload = (progressEvent) => {
$(input).prev(this.selectorImg).children('img').attr('src', progressEvent.currentTarget.result);
}
const file = input[0].files[0];
reader.readAsDataURL(file);
}
}
SCSS
app/assets/stylesheets/application.scss
/*form-image-uploader
------------------------------------------------------*/
.form-image-uploader {
@at-root {
#{&}__saved-img {
margin-bottom: 1em;
@at-root {
#{&}-inner {
border: 1px solid #ced4da;
border-radius: 0.25rem;
display: inline-block;
}
}
img {
max-height: 300px;
}
}
#{&}__preview {
display: inline-block;
border: 1px solid #ced4da;
border-radius: 0.25rem;
position: relative;
cursor: pointer;
img {
max-width: 100px;
width: auto;
max-height: 100px;
height: 100%;
&:hover {
opacity: 0.7;
}
}
}
}
}
/*block-toggle
------------------------------------------------------*/
.block-toggle {
@at-root {
#{&}__press {
cursor: pointer;
margin-bottom: 0.5em;
}
#{&}__content {
border: 1px solid #ced4da;
border-radius: 0.25rem;
padding: 0.5em;
}
}
}
検証
下記の動作検証をChrome、Firefox、Safariで行いました。
新規投稿時
投稿編集時
課題
- 選択した画像名が長いと、ファイル名が横に長くなり、wrapしているHTML要素を超えてしまうこと
- 画像選択後、「取消」ボタン押下→プレビュー画像をnopthoに戻す機能を追加できると良い
さいごに
次回は上記の課題を克服した、画像プレビュー機能を実装してみたいです。