Rails・jQueryで画像プレビュー機能の実装をしてみた

# はじめに

ポートフォリオのサイトを制作中、画像アップロード機能をつけていたのですが、プレビュー機能がないため、ちゃんと任意の画像が選択できていたのか分かりづらい問題がありました。

そのため画像プレビュー機能をつけてみようと思い、実装してみました。

課題は何点かあるものの、実装できたので、まとめていきたいと思います。

# 前提

- Ruby on Rails 6.0.2
- jQiery 3.5.1
- Bootstrap4

# 機能詳細

## 画像アップロードの流れ

1. ファイル入力のinputタグ押下
2. 画像選択
3. プラビュー画像を選択した画像に差替

## バリデーション時・投稿編集時

- 選択したファイルは保存された画像として大きく表示
- 「画像を変更する」ボタン押下→プレビュー画像とinputタグをtoggleで表示・非表示

# 実装

## View

```erb: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`は任意のパスを設定します。

```jsx: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

```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で行いました。

## 新規投稿時
![_2020-11-03_6.39.46.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261027/a272e305-da1e-859a-8f16-1ad079747812.gif)


## 投稿編集時
![_2020-11-03_6.41.47.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261027/836cef31-d46f-f59a-42e4-3430944f1282.gif)

# 課題

- 選択した画像名が長いと、ファイル名が横に長くなり、wrapしているHTML要素を超えてしまうこと
- 画像選択後、「取消」ボタン押下→プレビュー画像をnopthoに戻す機能を追加できると良い

# さいごに

次回は上記の課題を克服した、画像プレビュー機能を実装してみたいです。

# 参考記事

[【Rails】画像プレビュー機能の実装 - Qiita](https://qiita.com/matsubishi5/items/34276fce924aded4061a)

[Bootstrap4 custom-file mutiple 画像プレビュー](https://codepen.io/cccabinet/pen/OvNORg)