6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-16

はじめに

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

投稿機能はできていることを前提にしてますので、できていない方は
前回記事:【Rails】画像を複数投稿したい
を見ていただければと思います!

#目指すゴール
目指すの動作は前回同様です!これをAjaxでいきます。
ezgif.com-video-to-gif (9).gif
#前提

  1. 投稿機能は実装済み(テキスト複数行&画像複数枚が投稿できる)
  2. jQueryを導入の上、turbolinksは今回使用しないので、コメントアウトしている。
  3. テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています)
スクリーンショット 2020-06-14 18.16.05.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のidactionはブラウザの要素検証から確認します。
スクリーンショット 2020-06-16 17.05.10.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)

フォームの投稿ボタンをクリックしたとき、ターミナルで以下のようなログが出ていれば成功です!
スクリーンショット 2020-06-16 17.37.12.png

##3. 返ってきたJSONを受取り、表示するHTMLを作っていく
テンプレートリテラル記法を使用し、表示するHTMLを作っていきます。
テンプレートリテラルって?って方はこちらの記事がとてもわかりやすいです。
▶︎ JavaScript の テンプレートリテラル を極める!

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

<div class="post-files">
  <object class="post-files__file" data="画像1.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);
          })
        }

これで何をしているかというと、下図のように画像が何枚であろうと対応できるコードにしています。
スクリーンショット 2020-06-16 18.47.50.png
appendHTMLメソッドにより、filesHTMLの中にfileHTMLがシャって入っていく感じです。

ちなみに、テンプレートリテラルで一行ずつHTMLのコードを書いていくのは時間がかかるし、スペルミスを誘発します。
そこで、ブラウザの要素検証からコピペすることをおすすめします。
コピペしたい要素を選択して右クリックし、「Edit as HTML」を選択します。
範囲を選択してコピペし、変数の部分だけ書き換えています。
スクリーンショット 2020-06-16 17.48.18.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

ただ、今のままですと、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);
      })
  });
});

参考サイト

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

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?