This page looks best with JavaScript enabled
⚠️

【jQuery】初学者の自分がAjaxでハマったポイント

 ·   ·  ☕ 5 分で読めます
✏️

はじめに

RailsでAjaxを実装するとなり、ほぼ初めてJavaScriptに触りました。
そんな私が理解に苦しんだところやハマったポイントをまとめました。

実装するもの

Ajax関数による非同期通信でコメントの編集をするというものです。
以下のような流れになります。

編集ボタンを押す
  → 編集フォームが現れる
編集中、キャンセルボタンを押す
  → 変更せずにコメントを表示
編集後、更新ボタンを押す
  → データを更新して、編集後のコメントを表示する
  → 不適な場合(バリデーションエラー)、エラーメッセージを表示し、編集フォームはそのままにする

そもそもAjaxの処理の流れってどうなってんの

基本的に、イベントの中にAjax関数を入れることになります。
Ajax関数の役割ですが、

HTTP通信でページを読み込みます。
jQuery日本語リファレンス

Ajaxリクエストを送信するオプションをキーと値のペアで指定します。
JavaScript日本語リファレンス

らしいです。これだけを見ると、初学者からすると非常にわかりづらいので、順々に追っています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$.ajax({
  type: "PATCH", // HTTP通信の種類
  url: "comments/:id", // データの送り先のURL ※idが書いていないのでこのままでは動きません
  data: {
    // サーバーへ送信するデータ
    body: "コメント",
  },
})
  .done(function () {
    // 成功処理
  })
  .fail(function (result) {
    // 失敗処理
  });

typeとurlは$ rails routesですぐにわかります。
Ajax関数がtypeとurlから判断して、コントローラのアクションへdataを送ります。
Ajaxによって送られたデータはコントローラ側ではparamsで受け取れます。

1
2
3
4
5
6
7
8
  def update
    @comment = current_user.comments.find(params[:id])
    if @comment.update(body: params[:body]) # 'コメント'を受け取る
      render status: 200
    else
      render json: { comment: @comment, errors: { messages: @comment.errors.full_messages } }, status: 422
    end
  end

コントローラがrenderによって返したデータは.done(function (result)resultに格納されます。
statusはresult.status、jsonはresult.responseJSONで取得できます。

簡単に図に表すとこんな感じです。
Untitled Diagram.jpg

コントローラ側が特に何も返さなかったり、status: 200などを返したときはdoneへ、エラーやstatus: 400などを返したときはfailへ分岐します。

この辺りのイメージは、イラスト図解式 この一冊で全部わかるWeb技術の基本のChapter3を読むと理解が深まりました。

ダメなパターン

では、私がやらかしたことを以下に書きます。

status: 422を返さない(コントローラ側)

1
2
3
4
# ダメパターン
  def update
      render json: { comment: @comment, errors: { messages: @comment.errors.full_messages } } unless @comment.update(body: params[:body])
  end

updateでは保存に失敗したときにfalseを返すだけでステータスを返さないため、js側では成功として認識します。そのため、.doneの方に処理が流れてしまいます。
update!に変更するか(自動的に例外を返してくれる)、renderで明示的にstatusを返すようにすると、ちゃんと.failに流れてくれます。

ちなみに、headでヘッダのみのレスポンスを生成することもできるので、上のrender status: 200head :createdと書くこともできます。

formとAjax関数で2つのリクエストを送ってしまう(js側)

1
2
3
4
5
6
// ダメパターン
$(document).on("click", ".update-button", function () {
  $.ajax({
    // 省略
  });
});

このようにした場合、更新ボタンを押すと、

  • フォームから送信されたデータ(通常のform_withのsubmitによるもの)
  • Ajax関数によって送信されたデータ

と二重に送られてしまいます。

1
2
3
4
5
6
7
// オッケーパターン
$(document).on("click", ".js-comment-update-button", function (e) {
  e.preventDefault();
  $.ajax({
    // 省略
  });
});

このようにe.preventDefault()と書くことで、通常の動作を制御することができます。

独立したイベントを入れ子にしてしまう(js側)

このときの私の思考:送信ボタンを押すというイベントは編集ボタンをクリックした後じゃないと実行しないから、この中に入れよう。
これに関しては、私のjsの理解度の低さが浮き彫りとなったミスだと思います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$(function () {
  // 編集ボタンを押したとき
  $(document).on("click", ".edit-button", function () {
    // 処理A

    // 送信時
    $(document).on("click", ".update-button", function () {
      // 処理B
    });
  });
});

何がダメかというと、送信ボタンを押した後も処理Aが終了しないのです。
図に示すと、このような入れ子型になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$(function () {
  $(document).on("click", ".edit-button", function () {
    // 処理A

    $(document).on("click", ".update-button", function () {
      // 処理B
    });
    // ここで止まってしまい、処理Aから抜けられない
  });
});

そのため、再び編集ボタンを押したときには、新たな処理Aが実行されます。その後更新ボタンを押すと、処理Bが2回実行されることになります。
(もちろん、その後同じことを繰り返すと3回、4回、……とどんどん膨れ上がります)

というわけで、一度のイベントで行われる処理同士は入れ子にせず、並列させるようにしましょう。

完成したコード

省略しているところもありますが、こんな感じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$(function() {
  // 編集ボタンを押したとき
  $(document).on('click', '.edit-button', function() {
    // 処理
  });

  // キャンセルしたとき
  .on('click', '.cancel-button', function() {
    // 処理
  });

  // 送信したとき
  .on('click', '.update-button', function(e){
    e.preventDefault();
    const commentId = $(this).data("comment-id");
    const editBody = $('textarea#comment-' + commentId).val();
    //処理
    $.ajax({
      type: 'PATCH',
      url: 'comments/' + commentId,
      data: {
        body: editBody
      }
    }).done(function () {
      // 成功処理
    }).fail(function () {
      // 失敗処理
    });
  });
});

ちなみに、先述の並列の処理(編集ボタンを押したとき/キャンセルしたとき/送信したとき)は上のように.onで繋げることができるそうです。

Share on

END
END
@aiandrox

 
目次