【Django】いいねボタンの非同期通信(Ajax)を実装する|簡単な掲示板アプリの作成

Djangoで作成した掲示板アプリを題材に「いいねボタン」の非同期通信(Ajax)を実装する方法を解説します。ページを再読み込みせずにボタンの状態だけを切り替える、快適なユーザー体験を実現します。JavaScript(jQuery)でサーバーと通信し、DjangoからJSON形式でデータを受け取って画面を更新する、実践的なWeb開発の基本が身につきます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Python: 3.10.11
  • Django: 5.0.3

サンプルコード

/app/views.py

/app/views.py
1         form = FavoriteForm(request.POST)
2         if form.is_valid():
3             form.instance.user = request.user
4-            form.save()
5-            return redirect('index')
6-    return redirect('index')
7+            favorite = form.save()
8+            board = favorite.board
9+            board.is_favorite = 1
10+            html = render_to_string('favorite_button.html', {'board': board, 'user': request.user}, request=request)
11+            return JsonResponse({'status': 'success', 'html': html})
12+    return JsonResponse({'status': 'error', 'message': '無効なリクエストです。'})
13 
14 @login_required
15 def remove_favorite(request):
16     if request.method == 'POST':
17-        favorite = Favorite.objects.get(user=request.user, board=request.POST.get('board'))
18-        favorite.delete()
19-        return redirect('index')
20-    return redirect('index')
21+        try:
22+            favorite = Favorite.objects.get(user=request.user, board_id=request.POST.get('board'))
23+            favorite.delete()
24+            board = favorite.board
25+            board.is_favorite = 0
26+            html = render_to_string('favorite_button.html', {'board': board, 'user': request.user}, request=request)
27+            return JsonResponse({'status': 'success', 'html': html})
28+        except Favorite.DoesNotExist:
29+            return JsonResponse({'status': 'error', 'message': 'お気に入りされていません。'})
30+    return JsonResponse({'status': 'error', 'message': '無効なリクエストです。'})
31 
32 def contact(request):
33     if request.method == 'POST':
34

/static/js/common.js

/static/js/common.js
1 // ページロード時にフラッシュメッセージをフェードアウトする
2 $(document).ready(function(){
3     $(".alert").delay(3000).fadeOut("slow");
4-});
5+});
6+
7+// お気に入りボタンの非同期通信
8+$(document).on('click', '.favorite-form', function(e) {
9+    e.preventDefault();
10+    let $form = $(this);
11+    let url = $form.attr('action');
12+    let data = $form.serialize();
13+
14+    $.ajax({
15+        url: url,
16+        type: 'POST',
17+        data: data,
18+        dataType: 'json',
19+    }).done(function(response){
20+        if (response.status === 'success') {
21+            $form.closest('td').html(response.html);
22+        } else {
23+            alert('エラーが発生しました: ' + response.message);
24+        }
25+    }).fail(function(response){
26+        alert('エラーが発生しました: リクエストを処理しています。もう一度試してください。');
27+    });
28+});
29

/templates/favorite_button.html

/templates/favorite_button.html
1+{% if user.is_authenticated %}
2+    {% if board.is_favorite %}
3+        <form action="{% url 'remove_favorite' %}" method="POST" class="favorite-form">
4+            {% csrf_token %}
5+            <input type="hidden" name="board" value="{{ board.id }}">
6+            <button class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
7+        </form>
8+    {% else %}
9+        <form action="{% url 'add_favorite' %}" method="POST" class="favorite-form">
10+            {% csrf_token %}
11+            <input type="hidden" name="board" value="{{ board.id }}">
12+            <button class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
13+        </form>
14+    {% endif %}
15+{% endif %}
16

/templates/index.html

/templates/index.html
1                     <tr>
2                         <td scope="row">{{ board.id }}</td>
3                         <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
4-                        <td>
5-                            {% if user.is_authenticated %}
6-                                {% if board.is_favorite %}
7-                                    <form action="{% url 'remove_favorite' %}" method="POST">
8-                                        {% csrf_token %}
9-                                        <input type="hidden" name="board" value="{{ board.id }}">
10-                                        <button type="submit" class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
11-                                    </form>
12-                                {% else %}
13-                                    <form action="{% url 'add_favorite' %}" method="POST">
14-                                        {% csrf_token %}
15-                                        <input type="hidden" name="board" value="{{ board.id }}">
16-                                        <button type="submit" class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
17-                                    </form>
18-                                {% endif %}
19-                            {% endif %}
20-                        </td>
21+                        <td>{% include 'favorite_button.html' with board=board user=user %}</td>
22                         <td>{{ board.created_at }}</td>
23                         <td>{{ board.updated_at }}</td>
24                     </tr>
25

コード解説

変更点: いいね追加処理(add_favorite)のレスポンスをJSON形式に変更

/app/views.py
1         form = FavoriteForm(request.POST)
2         if form.is_valid():
3             form.instance.user = request.user
4-            form.save()
5-            return redirect('index')
6-    return redirect('index')
7+            favorite = form.save()
8+            board = favorite.board
9+            board.is_favorite = 1
10+            html = render_to_string('favorite_button.html', {'board': board, 'user': request.user}, request=request)
11+            return JsonResponse({'status': 'success', 'html': html})
12+    return JsonResponse({'status': 'error', 'message': '無効なリクエストです。'})

これまでいいねを追加した後は、redirect('index') を使ってページ全体を再読み込みしていました。この変更では、非同期通信に対応するため、レスポンスの形式を変更しています。

JsonResponse は、Pythonの辞書型データをJSON形式に変換して、ブラウザ(JavaScript)に返します。これにより、ページ全体を再読み込みすることなく、データだけをやり取りできます。

ここでは、処理が成功したことを示す 'status': 'success' と、更新後のいいねボタンのHTMLを格納した 'html' の2つの情報を返しています。 render_to_string という関数は、指定したテンプレートファイル(favorite_button.html)をサーバー側でHTML文字列に変換する役割を持ちます。このHTML文字列をJavaScript側で受け取り、画面の一部を書き換えることで、非同期での表示更新が実現します。

変更点: いいね解除処理(remove_favorite)のレスポンスをJSON形式に変更

/app/views.py
1 @login_required
2 def remove_favorite(request):
3     if request.method == 'POST':
4-        favorite = Favorite.objects.get(user=request.user, board=request.POST.get('board'))
5-        favorite.delete()
6-        return redirect('index')
7-    return redirect('index')
8+        try:
9+            favorite = Favorite.objects.get(user=request.user, board_id=request.POST.get('board'))
10+            favorite.delete()
11+            board = favorite.board
12+            board.is_favorite = 0
13+            html = render_to_string('favorite_button.html', {'board': board, 'user': request.user}, request=request)
14+            return JsonResponse({'status': 'success', 'html': html})
15+        except Favorite.DoesNotExist:
16+            return JsonResponse({'status': 'error', 'message': 'お気に入りされていません。'})
17+    return JsonResponse({'status': 'error', 'message': '無効なリクエストです。'})

いいね追加処理と同様に、いいねを解除する処理のレスポンスも redirect から JsonResponse に変更しています。

いいねが解除された後、新しい状態のいいねボタン(いいねされていない状態のボタン)のHTMLを render_to_string で生成し、JSONデータとしてブラウザに返します。

また、try...except を使ってエラーハンドリングを追加しています。万が一、データベースに存在しないいいねを解除しようとした場合にエラーが発生しないよう、Favorite.DoesNotExist という例外を補足し、エラーメッセージをJSONで返すようにしています。これにより、より安全なプログラムになります。

変更点: いいねボタンのHTMLを別テンプレートに分離

/templates/index.html
1                     <tr>
2                         <td scope="row">{{ board.id }}</td>
3                         <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
4-                        <td>
5-                            {% if user.is_authenticated %}
6-                                {% if board.is_favorite %}
7-                                    <form action="{% url 'remove_favorite' %}" method="POST">
8-                                        {% csrf_token %}
9-                                        <input type="hidden" name="board" value="{{ board.id }}">
10-                                        <button type="submit" class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
11-                                    </form>
12-                                {% else %}
13-                                    <form action="{% url 'add_favorite' %}" method="POST">
14-                                        {% csrf_token %}
15-                                        <input type="hidden" name="board" value="{{ board.id }}">
16-                                        <button type="submit" class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
17-                                    </form>
18-                                {% endif %}
19-                            {% endif %}
20-                        </td>
21+                        <td>{% include 'favorite_button.html' with board=board user=user %}</td>
22                         <td>{{ board.created_at }}</td>
23                         <td>{{ board.updated_at }}</td>
24                     </tr>

これまで index.html に直接書かれていたいいねボタンのHTMLコードを、{% include %} タグを使って別のファイルから読み込むように変更しました。

この変更の目的は、コードの「部品化」です。いいねボタンの表示ロジックを favorite_button.html という専用ファイルにまとめることで、2つのメリットが生まれます。

  1. コードの再利用性向上: 他のページでも同じいいねボタンを使いたい場合に、{% include %} タグを一行書くだけで済みます。
  2. 非同期処理との連携: 前述の views.pyrender_to_string を使ってボタン部分だけのHTMLを生成する必要があるため、ボタン部分が独立したファイルになっていると非常に都合が良いのです。

with board=board user=user の部分は、favorite_button.htmlboarduser という変数を渡すための記述です。

変更点: 再利用可能ないいねボタンテンプレートの作成

/templates/favorite_button.html
1+{% if user.is_authenticated %}
2+    {% if board.is_favorite %}
3+        <form action="{% url 'remove_favorite' %}" method="POST" class="favorite-form">
4+            {% csrf_token %}
5+            <input type="hidden" name="board" value="{{ board.id }}">
6+            <button class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
7+        </form>
8+    {% else %}
9+        <form action="{% url 'add_favorite' %}" method="POST" class="favorite-form">
10+            {% csrf_token %}
11+            <input type="hidden" name="board" value="{{ board.id }}">
12+            <button class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
13+        </form>
14+    {% endif %}
15+{% endif %}

これは新しく作成されたファイルで、いいねボタンのHTMLと表示切り替えのロジックが記述されています。

index.html から分離されたこのコードは、board.is_favorite の値(いいね済みかどうか)をみて、表示するフォームを切り替えています。

  • is_favorite が真(True)の場合: いいね解除用のフォーム(remove_favorite へPOST)を表示します。
  • is_favorite が偽(False)の場合: いいね追加用のフォーム(add_favorite へPOST)を表示します。

重要な点として、両方のフォームに class="favorite-form" というクラス名が付けられています。これは、後述するJavaScriptが「これはAjaxで処理すべきいいねボタンだ」と識別するための目印になります。

変更点: Ajaxによる非同期通信を実装するJavaScriptの追加

/static/js/common.js
1 // お気に入りボタンの非同期通信
2+$(document).on('click', '.favorite-form', function(e) {
3+    e.preventDefault();
4+    let $form = $(this);
5+    let url = $form.attr('action');
6+    let data = $form.serialize();
7+
8+    $.ajax({
9+        url: url,
10+        type: 'POST',
11+        data: data,
12+        dataType: 'json',
13+    }).done(function(response){
14+        if (response.status === 'success') {
15+            $form.closest('td').html(response.html);
16+        } else {
17+            alert('エラーが発生しました: ' + response.message);
18+        }
19+    }).fail(function(response){
20+        alert('エラーが発生しました: リクエストを処理しています。もう一度試してください。');
21+    });
22+});

このJavaScript(jQuery)コードが、非同期通信を実現する心臓部です。

  • $(document).on('click', '.favorite-form', ...): .favorite-form というクラス名を持つ要素がクリックされた時に、中の処理を実行するという意味です。on を使うことで、Ajaxによって後から動的に追加された要素(ボタンを押し直した後の新しいボタン)にも対応できます。

  • e.preventDefault();: フォームがクリックされたときのデフォルトの動作(=ページの送信と再読み込み)をキャンセルします。これを書くことで、ページが遷移しなくなります。

  • $.ajax({ ... }): 非同期通信を行うためのjQueryの命令です。

    • url: フォームの action 属性から取得したURL(/add_favorite//remove_favorite/)を指定します。
    • type: 'POST': 通信方法をPOSTに指定します。
    • data: serialize() でフォームの中身(どの投稿に対するいいねか、など)を送信用のデータ形式に変換して指定します。
    • dataType: 'json': サーバーから返ってくるデータがJSON形式であることを示します。
  • .done(function(response){ ... }): 通信が成功したときの処理です。引数 response には、Djangoの views.py から返されたJSONデータ(例: {status: 'success', html: '...'})が入ります。

    • response.status'success' であれば、$form.closest('td').html(response.html); を実行します。これは「クリックされたフォームに一番近い <td> タグを見つけ、その中身をサーバーから受け取ったHTML(response.html)で丸ごと書き換える」という命令です。これにより、ページ全体をリロードすることなく、ボタン部分だけが新しい表示に切り替わります。
  • .fail(function(){ ... }): 通信自体が失敗した(サーバーにリクエストが届かなかったなど)場合に、エラーメッセージを表示します。

おわりに

今回は、JavaScript(jQuery)を使ったAjaxで、ページを再読み込みしない「いいね」機能を実装しました。Djangoのビューでは、ページ全体を再描画するredirectの代わりにJsonResponseを使い、更新後のボタンのHTMLをJSON形式で返すように変更したのが大きなポイントです。JavaScript側では、そのJSONデータを受け取り、サーバーから送られてきたHTMLでボタン部分だけを動的に書き換える役割を担っています。このサーバーとクライアントが連携する仕組みは、ユーザー体験を向上させるWeb開発の基本であり、非常に実践的なスキルです。

関連コンテンツ