はじめに
RailsでAjaxを実装するとなり、ほぼ初めてJavaScriptに触りました。
そんな私が理解に苦しんだところやハマったポイントをまとめました。
実装するもの
Ajax関数による非同期通信でコメントの編集をするというものです。
以下のような流れになります。
編集ボタンを押す
→編集フォームが現れる
編集中、キャンセルボタンを押す
→変更せずにコメントを表示
編集後、更新ボタンを押す
→データを更新して、編集後のコメントを表示する
→不適な場合(バリデーションエラー)、エラーメッセージを表示し、編集フォームはそのままにする
そもそもAjaxの処理の流れってどうなってんの
基本的に、イベントの中にAjax関数を入れることになります。
Ajax関数の役割ですが、
HTTP通信でページを読み込みます。
jQuery日本語リファレンス
Ajaxリクエストを送信するオプションをキーと値のペアで指定します。
JavaScript 日本語リファレンス
らしいです。これだけを見ると、初学者からすると非常にわかりづらいので、順々に追っています。
$.ajax({
type: 'PATCH', // HTTP通信の種類
url: 'comments/:id', // データの送り先のURL ※idが書いていないのでこのままでは動きません
data: { // サーバーへ送信するデータ
body: 'コメント'
}
}).done(function () {
// 成功処理
}).fail(function (result) {
// 失敗処理
});
typeとurlは$ rails routes
ですぐにわかります。
Ajax関数がtypeとurlからPATCH comments/:id
にリスエストとして送り、ルーティングを通ってコントローラのアクションへdataを送ります。
Ajaxによって送られたデータはコントローラ側ではparams
で受け取れます。
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
で取得できます。
コントローラ側が特に何も返さなかったり、status: 200
などを返したときはdoneへ、エラーやstatus: 400
などを返したときはfailへ分岐します。
この辺りのイメージは、イラスト図解式 この一冊で全部わかるWeb技術の基本のChapter3を読むと理解が深まりました。
ダメなパターン
では、私がやらかしたことを以下に書きます。
status: 422を返さない(コントローラ側)
# ダメパターン
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: 200
はhead :created
と書くこともできます。
formとAjax関数で2つのリクエストを送ってしまう(js側)
// ダメパターン
$(document).on("click", ".update-button", function(){
$.ajax({
// 省略
});
});
このようにした場合、更新ボタンを押すと、
- フォームから送信されたデータ(通常のform_withのsubmitによるもの)
- Ajax関数によって送信されたデータ
と二重に送られてしまいます。
// オッケーパターン
$(document).on("click", ".js-comment-update-button", function(e){
e.preventDefault();
$.ajax({
// 省略
});
});
このようにe.preventDefault()と書くことで、通常の動作を制御することができます。
独立したイベントを入れ子にしてしまう(js側)
このときの私の思考:送信ボタンを押すというイベントは編集ボタンをクリックした後じゃないと実行しないから、この中に入れよう。
これに関しては、私のjsの理解度の低さが浮き彫りとなったミスだと思います。
$(function() {
// 編集ボタンを押したとき
$(document).on("click", ".edit-button", function() {
// 処理A
// 送信時
$(document).on('click', '.update-button', function(){
// 処理B
});
});
});
何がダメかというと、送信ボタンを押した後も処理Aが終了しないのです。
図に示すと、このような入れ子型になります。
$(function() {
$(document).on("click", ".edit-button", function() {
// 処理A
$(document).on('click', '.update-button', function(){
// 処理B
});
// ここで止まってしまい、処理Aから抜けられない
});
});
そのため、再び編集ボタンを押したときには、新たな処理Aが実行されます。その後更新ボタンを押すと、処理Bが2回実行されることになります。
(もちろん、その後同じことを繰り返すと3回、4回、……とどんどん膨れ上がります)
というわけで、一度のイベントで行われる処理同士は入れ子にせず、並列させるようにしましょう。
完成したコード
省略しているところもありますが、こんな感じです。
$(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
で繋げることができるそうです。
おわりに
自分なりにまとめてみましたが、足りないところ・間違っているところなどがございましたらコメントでご指摘をお願いします。