【Rails/JavaScript】アクティブストレージの画像をプレビュー表示させる方法

## 初めに

アクティブストレージの画像をプレビュー表示させる方法をまとめました。
実装の備忘録としてJavaScript１つ１つの記述の意味も一緒に記録しています。

## プレビュー表示の方法

### ①プレビュー用のjsファイルを作成
今回は「preview.js」という名前で作成しました。
　　

```app/javascript/preview.js　
 🗂 app
   ∟ 🗂 javascript
  　　　　　 ∟ 🗂 channels
  　　　　　 ∟ 🗂 packs
    　　　   ∟ application.js
  　　　　　 ∟  preview.js　　　　　　　　　　　　　　　　#こちらを作成
```

### ②application.jsを編集

行うことは２点です。
１.turbolinks（高速化）をコメントアウトする
　　→理由はJavaScriptの動作に影響を及ぼす可能性があるためです。
2. require('../preview') を追記し、jsファイルを使えるようにする


```app/javascript/packs/application.js

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

```
### ③プレビュー画像を表示するスペースを作成する

:red_circle:ファイルをアップロードする場所の例
![スクリーンショット 2022-06-09 16.56.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2655769/f762e036-206b-9125-705f-1b60aa31def2.png)

:red_circle:ファイルをアップロードする場所に以下を追記する。
（場所はお好みの場所に指定してOK）
```erb
<div class="image-field">
    <div id="previews"></div>     <%# こちらの１行を追加 %>
    <div class="click-upload">
      <%= f.file_field :image %>
    </div>
```
:red_circle:フォーム自体にidを付与しておく
↓全体的なフォーム図


```erb

<%= form_with model: @post, id: 'new_post', local: true do |f| %> 

  <%# 上記フォーム内に「id: 'new_post'」を付与しておく %>

<%= 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 %>
```


### ④jsファイルにプレビューのための記述をする

```preview.js
document.addEventListener('DOMContentLoaded', 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="item[image]"]');
  // input要素で値の変化が起きた際に呼び出される関数
  fileField.addEventListener('change', function(e){
    // 古いプレビューが存在する場合は削除
    const alreadyPreview = document.querySelector('.preview');
    if (alreadyPreview) {
      alreadyPreview.remove();
    };
    const file = e.target.files[0];
    const blob = window.URL.createObjectURL(file);
    // 画像を表示するためのdiv要素を生成
    const previewWrapper = document.createElement('div');
    previewWrapper.setAttribute('class', 'preview');
    // 表示する画像を生成
    const previewImage = document.createElement('img');
    previewImage.setAttribute('class', 'preview-image');
    previewImage.setAttribute('src', blob);

    // 生成したHTMLの要素をブラウザに表示させる
    previewWrapper.appendChild(previewImage);
    previewList.appendChild(previewWrapper);
  });
});
```

### 中身の補足

##### どこのinputの情報を取ってきたの？ 

```js
// input要素を取得
  const fileField = document.querySelector('input[type="file"][name="item[image]"]');
```
![スクリーンショット 2022-06-09 16.56.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2655769/f762e036-206b-9125-705f-1b60aa31def2.png)
ブラウザの検証で上記の「ファイルを選択」ボタンを選択すると以下が出てきます。
![スクリーンショット 2022-06-09 17.06.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2655769/98e381d3-8ae8-6c70-5e1f-61e0296a97c5.png)
nameはアプリによって変わりますので、適切な値を入れてください。

##### inputの中にある画像取得方法は？
選択した画像ファイルは、発火したイベントeの中の、targetの中の、filesという配列に格納されています。
```js
const file = e.target.files[0];
// 以下で中身を確認することができる
console.log(e.target.files[0]);
```
プレビューの画像として表示させるためには、画像情報のURLを生成する必要があります。

##### 取得した画像情報のURLを生成する方法は？

createObjectURL()メソッドを使うことで、画像データへアクセス可能なURLを生成することができるようになります。

```js
    // inputの中にある画像を取得し、変数に代入
    const file = e.target.files[0];
    // 変数に代入した画像のURLを生成し、変数に代入
    const blob = window.URL.createObjectURL(file);
    // 以下で生成した画像URLを確認できる
    console.log(blob);
```

##### 生成したURLをHTMLとして表示させるには？
HTML要素を生成するcreateElement()メソッドを使うことで、img要素を生成し、URL付けることができるようになります。
要素.setAttribute('class', 'クラス名');などでクラス名等を付けることも可能です。

```js
const element = document.createElement('要素');
要素.setAttribute('class', 'クラス名')
```
使用例
```js
document.createElement('div');
// =>  <div></div>が生成される

document.createElement('input');
// =>  <input>が生成される
```

```js
 // 画像を表示するためのdiv要素を生成
    const previewWrapper = document.createElement('div');
    previewWrapper.setAttribute('class', 'preview');
    // 表示する画像を生成
    const previewImage = document.createElement('img');
    previewImage.setAttribute('class', 'preview-image');
    previewImage.setAttribute('src', blob);   //先ほど作成した画像URLを含めて生成される
```
HTMLを生成しましたが、生成したHTMLはまだブラウザには表示されていない状態です。

##### 作成したHTMLの要素をブラウザに表示するには？
指定した親要素の中に要素を追加するappendChild()メソッドを使用します。

```js
親要素.appendChild(追加する子要素);
```
使用例
```js
const divElement = document.createElement('div');
const inputElement = document.createElement('input');

divElement.appendChild(inputElement);
// => <div><input></div>
```
```js
   // 以下は③プレビュー画像を表示するスペースを作成するで作成したid
    const previewList = document.getElementById('previews');
   // 上記にjsファイル内で作成したHTML要素を入れ込む
    previewWrapper.appendChild(previewImage);
    previewList.appendChild(previewWrapper);
```
これで画像を選択したら、プレピューが表示されるようになります。

##### 新しく画像を選択すると、それ以前の画像が表示されてしまう！
条件式で削除する記述を初めに追加することで解決されます。

```js
   // 古いプレビューが存在する場合は削除
    const alreadyPreview = document.querySelector('.preview');
    if (alreadyPreview) {
      alreadyPreview.remove();
    };
```


## まとめ

最後までご覧下さりありがとうございました。
こちらの実装の中で、１つ１つ順を追って理解することの重要性を再認識しました。
今後、JQueryでJavaScriptの記述をどこまで簡略化できるかに奮闘していきたいと思います。

初学者のため、記入漏れや記述ミスがありましたら教えていただけると助かります。

こちらの記事がどなたかの参考になりましたら幸いです。