【Rails + JavaScript】投稿画面に画像プレビュー機能を実装しよう！

画像を投稿する際に選択した画像がプレビューできる機能を実装していきます。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。

JavaScript初心者にもわかりやすいようにメソッドやイベントについては外部リンクを参照しながら解説していきます。

画像投稿機能実装については[前回](https://qiita.com/mitaninjin/items/c3d66ca0e806c13930a5)記事にしておりますので、そちらを参照してください。

https://qiita.com/mitaninjin/items/c3d66ca0e806c13930a5


#####完成イメージ
![5c93c9251ef0f4ecbbbe1eb8ccc91142.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/894c12c3-89c5-8797-36c5-3d73c80c1b19.gif)

##事前準備
####Javascriptファイルの作成
まずは、プレビュー機能を実装するためのpreview.jsを作成します。

```:ターミナル
touch app/javascript/packs/preview.js
```

ファイルが作成できたらpreview.jsを読み込むための記述をapplication.jsにしていきます。
また、turbolinksはコメントアウトします。

```javascript:app/javascript/packs/application.js
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

require("@rails/ujs").start()
// require("turbolinks").start()  // コメントアウト
require("@rails/activestorage").start()
require("channels")
require('./preview')  // 追記

//以下略
```

####画像を表示するスペースの作成
画像を表示する場所をビューファイルに指定します。

```erb:app/views/recipes/new.html.erb

<%= form_with model: @recipe, local: true do |f| %>

#中略
    <div class="form-group">
      <label class="text-secondary">画像</label><br>
      <%= f.file_field :image %>
    </div>

    <div id="new-image"></div>  #追記

#以下略

<% end %>
```

##Javascriptファイルの編集
preview.jsを新規投稿ページでしか発火しないようにif文を作成します。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  
}
```

次にHTMLが最初に読み込まれたときに作動する関数を定義していきます。
**[addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)**メソッドと**[DOMContentLoaded](https://developer.mozilla.org/ja/docs/Web/API/Document/DOMContentLoaded_event)**イベントを使います。

[addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)の使い方は下記の通りです。

```javascript
要素.addEventListener(イベント, 関数, オプション);
```




それでは処理を記述していきます。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    
  });
}
```

続いて、検証ツールを用いて投稿フォームのファイル選択ボックスのidを確認しましょう。
![e4ef3687bbb1149610a8aa9301d91f83.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/9669942a-9e2e-0b6b-d175-eed854c53a3f.gif)

今回のレシピアプリの場合は**id="recipe_image"**だったのでこの**recipe_image**を**[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)**メソッドで取得していきます。
そして、投稿フォームのファイル選択ボックスに変化（**[change](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/change_event)**）が起こったときに行われる処理を記述していきます。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    document.getElementById('recipe_image').addEventListener('change', (e) =>{
      console.log(e);
    });
  });
}
```

アロー関数の「e」は[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)で取得した投稿フォームのファイル選択ボックスの中身になります。(eはeventの略)

では、本当に中身が取得できたいるか確かめて見ましょう。
以下のようにコンソールに出力されていれば成功です。
![event.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/259896d1-a726-5ac3-7110-bf2825914ba8.gif)

では、取得した情報を定数に格納します。

e.target.files[0]で取得したファイルの情報を定数fileに格納し、**[URL.createObjectURL(file)](https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL)**で取得した情報を文字列に変換し、定数blobに格納します。

そして、blobを引数にcreateImageHTML( )という関数を呼び出します。(createImageHTML( )はこのあと作成します。）

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    document.getElementById('recipe_image').addEventListener('change', (e) => {

      console.log(e);  //削除

//ここから追記
      const file = e.target.files[0];  
      const blob = window.URL.createObjectURL(file); 
      createImageHTML(blob); 
//ここまで追記
    });
  });
}
```

それでは、createImageHTML( )を作成しましょう。
まずは、[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)でnew.html.erbに先ほど追加したdiv要素のidのnew-imageを取得します。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
//ここから追記
    const createImageHTML = (blob) => {  
      const imageElement = document.getElementById('new-image'); 
    }; 
//ここまで追記

    document.getElementById('recipe_image').addEventListener('change', (e) => {
      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}
```

次に**[createElement](https://developer.mozilla.org/ja/docs/Web/API/Document/createElement)**メソッドでHTML要素の「img」を作成し、blobImageに格納します。
そして、**[setAttribute](https://developer.mozilla.org/ja/docs/Web/API/Element/setAttribute)**でclassとsrcをimgに付与します。
classを付与しているのはCSSを当てるためです。

[setAttribute](https://developer.mozilla.org/ja/docs/Web/API/Element/setAttribute)の使い方は以下の通りです。

```javascript
要素.setAttribute("データ名",データ);
```

以上のことを踏まえて、記述していきましょう。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const createImageHTML = (blob) => {
      const imageElement = document.getElementById('new-image');
//ここから追記
      const blobImage = document.createElement('img'); 
      blobImage.setAttribute('class', 'new-img') 
      blobImage.setAttribute('src', blob); 
//ここまで追記
    };

    document.getElementById('recipe_image').addEventListener('change', (e) => {
      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}
```

最後に、**[appendChild](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)**メソッドを使ってnew.html.erbに追加したdiv要素の中にimg要素を入れます。

[appendChild](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)の使い方は以下の通りです。

```javascript
親要素.appendChild(追加する子要素);
```

それでは、preview.jsに追記しましょう。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const createImageHTML = (blob) => {
      const imageElement = document.getElementById('new-image');
      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage); //追記
    };

 //以下省略
```
[setAttribute](https://developer.mozilla.org/ja/docs/Web/API/Element/setAttribute)で付与したクラス「new-img」にCSSをあてます。

```css:style.css
.new-img{
  width: 400px;
  object-fit: cover;
}
```

実際にビューを確認してみましょう。
![てり.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/cc75f7b3-6a16-5cbe-fc66-dc30f6d0650c.jpeg)
画像が表示され以下のようにimg要素にclass属性とsrc属性がセットされていれば成功です。
![6007c0be3679ea9ee27b2e55cab74e10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/5eb1ea2c-d3fd-cbf6-585a-0f486bf64129.png)

##既存のプレビューを削除しよう
現状だと画像ファイルを選択し直すとどんどん画像がプレビューされていくという問題点があります。
![542452a03055e4b5d01981c40f15b950.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/18395804-59d7-749c-1ca9-920a6d1e25ee.gif)
この問題を解決していきましょう。

**[querySelector](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector)**メソッドを使ってimg要素を取得し、imageContentに格納します。

そして、if文を使いimageContentに値が入っている場合**[remove](https://developer.mozilla.org/ja/docs/Web/API/ChildNode/remove)**されます。
（img要素がない、つまりimageContentがnullの場合はif文がfalseとなり、実行されません。）

```javascript:app/javascript/packs/preview.js
//中略
    document.getElementById('recipe_image').addEventListener('change', (e) => {
//ここから追記
      const imageContent = document.querySelector('img'); 
      if (imageContent){ 
        imageContent.remove(); 
      } 
//ここまで追記

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}
```
それでは確認してみましょう。
以下の通り、画像ファイルを選択し直すと再プレビューされれば成功です。
![957bdc1ca5a903937315a55e59b92ff5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/278debd7-034d-446e-4e20-c3db29935ec6.gif)

以下、完成形のコードです。

```javascript:app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const createImageHTML = (blob) => {
      const imageElement = document.getElementById('new-image');
      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
    };

    document.getElementById('recipe_image').addEventListener('change', (e) => {
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}
```