【Rails】ajaxでチャットを投稿したい（テキスト複数行、画像複数枚）

# はじめに
ポートフォリオとして作っていたグループチャットアプリで、画像を複数投稿できるようにしました。
その投稿をajaxで表示する方法を本記事で書いていきます！

投稿機能はできていることを前提にしてますので、できていない方は
前回記事：[【Rails】画像を複数投稿したい](https://qiita.com/miyyuk/items/cacdc5abb009d3307c5d)
を見ていただければと思います！

#目指すゴール
目指すの動作は前回同様です！これをAjaxでいきます。
![ezgif.com-video-to-gif (9).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/b6e0730d-3b3e-5dbd-5a65-66ce35ac805f.gif)
#前提
1. 投稿機能は実装済み（テキスト複数行&画像複数枚が投稿できる）
2. jQueryを導入の上、turbolinksは今回使用しないので、コメントアウトしている。
3. テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています)
<img width="534" alt="スクリーンショット 2020-06-14 18.16.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/44da731f-9d46-d883-3623-8128cfbef7fd.png">

##開発環境
ruby 2.5.1
Rails 5.2.4.2
Haml 5.1.2
jQuery

#いざ実装
jQueryを導入してください。▶︎(https://github.com/rails/jquery-rails)
最後に完成コードを載せていますので、結果だけ教えてって方は読み飛ばしてください。

##1. ajaxでリクエストを送信しよう
フォームの投稿ボタンがクリックされたら、イベントを発火しajaxでデータを送信します。

```posts.js
$(function () {
  // formのidである'#new_post'が'submit'されたらイベント発火
  $('#new_post').on('submit', function (e) {
    // ブラウザが最初から持っているアクションをキャンセル
    e.preventDefault()
    // formのinput要素をJavaScriptのオブジェクトとしてキーバリュー形式で表示
    var formData = new FormData(this);
    // formのリクエスト送信先のパスをattrメソッドで取得
    var url = $(this).attr('action');
    // ajaxでデータを送信
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      // formDataを使用する場合は必要
      processData: false,
      contentType: false
    })
  });
});
```
formの`id`や`action`はブラウザの要素検証から確認します。
<img width="1439" alt="スクリーンショット 2020-06-16 17.05.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/a15aa391-91b0-03b1-1102-678354053a7c.png">

##2. posts#createで投稿を保存し、JSON形式でレスポンスする
ajaxでJSON形式で送られてきたデータを保存します。
コントローラを以下のように編集します。

```posts_controller.rb
# 省略
  def create
    @post = @group.posts.new(post_params)
    if @post.save
      if params[:post_files].present?
        params[:post_files][:file].each do |a|
          @post_file = @post.post_files.create!(file: a, post_id: @post.id)
        end
      end
      # json形式で来たリクエストに対してJSON形式のレスポンスを返す
      respond_to do |format|
        format.json
      end
    else
      @posts = @group.posts.includes(:user).order(created_at: "DESC")
      render :index
    end
  end
# 省略
```
続いて、jbuilderを使用して、保存された投稿をJSON形式で返します。
まずは`create.json.jbuilder`を作成します。

```
$ touch app/views/posts/create.json.jbuilder
```
`create.json.jbuilder`を以下のように編集します。

```app/views/messages/create.json.jbuilder
json.id               @post.id
json.user_avater      @post.user.avater_url
json.user_name        @post.user.name
json.created_at       @post.created_at.strftime("%Y/%m/%d %H:%M")
json.content          @post.content
json.post_files       @post.post_files
# コメントと"見ました"ボタンのリンク（不要の場合は消してOK）
json.post_link        group_post_path(@group, @post) <---"posts#show"のパス
json.looks_link       group_post_looks_path(@group, @post)
```

フォームの投稿ボタンをクリックしたとき、ターミナルで以下のようなログが出ていれば成功です！
<img width="1251" alt="スクリーンショット 2020-06-16 17.37.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/0e415e4a-ba87-bc89-c184-9e055161a759.png">

##3. 返ってきたJSONを受取り、表示するHTMLを作っていく
テンプレートリテラル記法を使用し、表示するHTMLを作っていきます。
テンプレートリテラルって？って方はこちらの記事がとてもわかりやすいです。
▶︎ [JavaScript の テンプレートリテラル を極める！](https://qiita.com/kura07/items/c9fa858870ad56dfec12)

ここからが本番です。
なぜなら、画像が何枚投稿されているかによって、HTMLの表示って変わりますよね？

```html
<div class="post-files">
  <object class="post-files__file" data="画像１.jpg"></object>
  <object class="post-files__file" data="画像2.jpg"></object>
  ...
</div>
```
このように、画像があるのか、何枚あるのかで`objectタグ`の数が変わるので厄介です。（個人的感想）

結論、画像を表示させるHTMLだけ切り出しておいて、画像の数だけ挿入するという方法を取りました。
以下解説コードです。

```posts.js
$(function () {
  // appendHTMLメソッドで画像部分のHTMLを作成しています
  function appendHTML(file) {
    // 引数として受け取ったfileを${変数}として挿入しています
    var fileHTML = `<object class="post-files__file" data="${file.file.url}"></object>`
    return fileHTML;
  }

  function buildHTML(post, filesHTML) {
    // 長くなるので重要なところだけ記述してます
    var html =
      `<div class="post">
       // 省略（完成コード参照）
        <div class="middle">
          <div class="post-content">
            ${ post.content}
          </div>
          <div class="post_files">
            ${filesHTML}
          </div>
       // 省略（完成コード参照）
      </div>`
    return html;
  }
  
  $('#new_post').on('submit', function (e) {
    e.preventDefault()
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
      .done(function (data) {
        // 画像を表示するHTMLを定義しています
        var filesHTML = '';
        // != は"等しくない"ということなので、"dataのpost_filesが0でなければ"となります
        if (data.post_files != 0) {
          // post_filesのfileを一つずつ取り出して処理（HTMLと一緒ですね！）
          data.post_files.forEach(function (file) {
            // 定義しておいたfilesHTMLに、fileの数だけappendHTMLメソッドを挿入する
            // fileを引数として渡している
            filesHTML += appendHTML(file);
          })
        }
        // 上述のbuildHTMLというメソッドで、htmlを組み立てる
        // 引数に(data, filesHTML)を渡している
        var html = buildHTML(data, filesHTML);
        //　postsクラスの一番上にhtmlを挿入する
        $('.posts').prepend(html);
        // formに入力された値をリセットする
        $('form')[0].reset();
        // 連続で投稿ボタンをクリックできるようにする
        $('.submit-btn').prop('disabled', false);
      })
      .fail(function () {
        alert("投稿に失敗しました\nリロードしてください");
        $('.submit-btn').prop('disabled', false);
      })
  });
});
```
ポイントは`doneメソッド`直後のこの部分。

```posts.js
        var filesHTML = '';
        if (data.post_files != 0) {
          data.post_files.forEach(function (file) {
            filesHTML += appendHTML(file);
          })
        }
```
これで何をしているかというと、下図のように画像が何枚であろうと対応できるコードにしています。
<img width="1349" alt="スクリーンショット 2020-06-16 18.47.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/347dd9e3-2355-9cfe-c77e-ddc55c8b6a07.png">
`appendHTMLメソッド`により、`filesHTML`の中に`fileHTML`がシャって入っていく感じです。

ちなみに、テンプレートリテラルで一行ずつHTMLのコードを書いていくのは時間がかかるし、スペルミスを誘発します。
そこで、ブラウザの要素検証からコピペすることをおすすめします。
コピペしたい要素を選択して右クリックし、「Edit as HTML」を選択します。
範囲を選択してコピペし、変数の部分だけ書き換えています。
<img width="1440" alt="スクリーンショット 2020-06-16 17.48.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/2064f8a9-b10e-a572-b87c-d624df41f804.png">

##4. textareaで入力された改行の処理をプラス
コードはほぼ完成していますが、ここで長文入力に対応したフォームにしていきます。
textareaの大きさが固定になっていると、長文を入力する際、スクロールしないと前の文章が確認できません。
そこで、以下のコードをプラスします。

```posts.js
  $(document).on('change keyup keydown paste cut',
    '#textarea', function () {
      if ($(this).outerHeight() > this.scrollHeight) {
        $(this).height(1)
      }
      while ($(this).outerHeight() < this.scrollHeight) {
        $(this).height($(this).height() + 1)
      }
    });
```
するとこのように文章をコピペして貼り付けたり、行数が変化したりすると、フォームの大きさが変わります。
![ezgif.com-video-to-gif (11).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/599a9a38-aad0-6a60-8e5d-b09f6c77102d.gif)

ただ、今のままですと、ajaxでは改行が反映された形で表示されません!
せっかく改行して見やすくしているわけですから、そのまま表示させましょう！

```posts.js
$(function () {
// 省略
  function buildHTML(post, filesHTML) {
    // 正規表現を用いて、入力されたテキストを加工
    var content = post.content.replace(/\n|\r\n|\r/g, '<br>');
    var html =
      `<div class="post">
       // 省略（完成コード参照）
        <div class="middle">
          <div class="post-content">
            ${ content}　　　<--------定義したcontentに変えます
          </div>
          <div class="post_files">
            ${filesHTML}
          </div>
       // 省略（完成コード参照）
      </div>`
    return html;
  }
//　省略
});
```

**これでajaxで表示するコードが完成しました！**

##5. 完成コード
```posts.js
$(function () {
  function appendHTML(file) {
    var fileHTML = `<object class="post-files__file" data="${file.file.url}"></object>`
    return fileHTML;
  }
  function buildHTML(post, filesHTML) {
    var content = post.content.replace(/\n|\r\n|\r/g, '<br>');
    var html =
      `<div class="post">
        <div class="top">
          <img id="avater" src="${ post.user_avater}">
          <div class="top__right">
            <div class="top__userinfo">
              <div class="top__userinfo--user-name">
                ${ post.user_name}
              </div>
              <div class="top__userinfo--datetime">
                ${ post.created_at}
              </div>
            </div>
          </div>
        </div>
        <div class="middle">
          <div class="post-content">
            ${ content}
          </div>
          <div class="post_files">
            ${filesHTML}
          </div>
          <div class="look" id="look_${post.id}">
            <div class="looked-count">
              <i class="fa fa-check"></i>
              0 人
            </div>
            <a class="look-btn" data-remote="true" rel="nofollow" data-method="post" href="${ post.looks_link}">見ました</a>
          </div>
        </div>
        <div class="bottom">
          <a class="comment-link" href="${ post.post_link}"><i class="fa fa-comment-dots"></i>
            コメントする
          </a>
        </div>
      </div>`
    return html;
  }

  $(document).on('change keyup keydown paste cut',
    '#textarea', function () {
      if ($(this).outerHeight() > this.scrollHeight) {
        $(this).height(1)
      }
      while ($(this).outerHeight() < this.scrollHeight) {
        $(this).height($(this).height() + 1)
      }
    });

  $('#new_post').on('submit', function (e) {
    e.preventDefault()
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
      .done(function (data) {
        var filesHTML = '';
        if (data.post_files != 0) {
          data.post_files.forEach(function (file) {
            filesHTML += appendHTML(file);
          })
        }
        var html = buildHTML(data, filesHTML);
        $('.posts').prepend(html);
        $('form')[0].reset();
        $('.submit-btn').prop('disabled', false);
      })
      .fail(function () {
        alert("投稿に失敗しました\nリロードしてください");
        $('.submit-btn').prop('disabled', false);
      })
  });
});
```


# 参考サイト
- [jQueryで自動リサイズする textarea を数行で実装する方法](https://pisuke-code.com/jquery-make-textarea-auto-resize/)
- [JavaScript：HTMLテキストエリアに改行を追加する方法は？](https://www.it-swarm.dev/ja/javascript/javascript%EF%BC%9Ahtml%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%A8%E3%83%AA%E3%82%A2%E3%81%AB%E6%94%B9%E8%A1%8C%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95%E3%81%AF%EF%BC%9F/957567883/)
- [ancestryによる多階層構造データを用いて、動的カテゴリーセレクトボックスを実現する~Ajax~](https://qiita.com/ATORA1992/items/bd824f5097caeee09678)

なぜancestry？って思われた方もいると思います。
実は、プログラミングスクールでフリマアプリをチーム開発していた際に、他のメンバーがこのカテゴリー選択機能を実装していました。
そのコードを見て、`appendOptionメソッド`の仕組みが画像の複数投稿のajaxにも使えるのではないか！？と思い、実装してみた次第です。
というわけなので、参考サイトにそのカテゴリー選択機能のサイトを入れておきました。

もっと良い実装の仕方をご存知の方、ぜひコメントください！！
最後までお読みいただきありがとうございました！