10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Raw JavaScript を書くのはツラいけど、Reactなどを必要とするほどのリッチなUIを作りたいわけでないため、モノリシックなアーキテクチャで楽にJSを使いたい。そのような場合の選択肢として、HTMXというものがあることを最近知りました。

少し前ですが、DjangoCon Europe 2022 でそのような事例紹介の発表がありました。

実際に、DjangoでHTMXを使ってみたいと思います。

1. ボタンを押したら、メッセージを表示する

まずは、次の画像のように、ボタンがクリックされたら、Ajaxを利用してメッセージを取得し、画面に表示される機能を作ってみます。

スクリーンショット 2024-01-29 19.06.38.png

スクリーンショット 2024-01-29 19.07.56.png

Raw JavaScript の場合

比較対象として、Raw JavaScript の場合から作ります。

views.py
from django.http import JsonResponse
from django.views import View
from django.shortcuts import render

class JavaScriptView(View):
    def get(self, request, *args, **kwargs):
        questions = Question.objects.all()
        context = {"questions": questions}
        return render(request, "sample/javascript.html", context)

class JsGetMessages(View):
    def get(self, request, *args, **kwargs):
        message = "Hello, World!"
        return JsonResponse({'message': message})
javascript.html
<html lang="ja">
    <head>
        <title>JS-Sample</title>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>

    <body class="p-6">
        <h1 class="text-3xl font-bold">
            JavaScript Sample
        </h1>

        <!-- Message -->
        <div class="mt-6 flex">
            <button type="button" id="messageButton"
                class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            >
                Click me
            </button>
            <div class="ml-6 text-xl" id="messageText">
                Let's display a Message by clicking a button!
            </div>
        </div>

    </body>

    <script>
        // Buttonが押されたら、Messageを表示
        document.getElementById('messageButton').addEventListener('click', function() {
            // サーバーからメッセージを取得
            fetch('/question/js/get-messages/')
                .then(response => response.json())
                .then(data => {
                    const messageText = document.getElementById('messageText');
                    messageText.innerText = data.message
                });
        });
    </script>
</html>

スクリーンショット 2024-01-29 19.12.53.png

HTMX の場合

続いて、HTMXを用いて同様の機能を実装してみます。

views.py
from django.http import JsonResponse
from django.views import View
from django.shortcuts import render

class HtmxView(View):
    def get(self, request, *args, **kwargs):
        questions = Question.objects.all()
        context = {"questions": questions}
        return render(request, "sample/htmx.html", context)

class HtmxGetMessages(View):
    def get(self, request, *args, **kwargs):
        message = "Hello, World!"
        return render(request, 'sample/parts/message.html', {'message': message})

Raw JS のように JSON形式で返すと、JSONの内容がそのまま表示されてしまうので、置き換えたい要素をpartsとして作成します。

parts/message.html
<div class="ml-6 text-xl" id="messageText">
    {{ message }}
</div>
htmx.html
<html lang="ja">
    <head>
        <title>HTMX-Sample</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://unpkg.com/htmx.org"></script>
    </head>

    <body class="p-6">
        <h1 class="text-3xl font-bold">
            HTMX Sample
        </h1>

        <!-- Message -->
        <div class="mt-6 flex">
            <button type="button" id="messageButton" hx-get="/question/htmx/get-messages/" hx-trigger="click" hx-target="#messageText" hx-swap="outerHTML"
                class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            >
                Click me
            </button>
            <div class="ml-6 text-xl" id="messageText">
                Let's display a Message by clicking a button!
            </div>
        </div>
    </body>
</html>

スクリーンショット 2024-01-29 19.17.34.png

HTMXを用いて、JavaScript を全く書かずに同様の機能を実装できました。htmlのタグに「hx」から始まる記述をすることで、 JavaScriptの記述と同様の処理を実現できるようです。

2. 選択された質問に合わせて、回答の選択肢を更新する

次はもう少し複雑な機能を実装してみましょう。選択された質問に合わせて、回答の選択肢を更新する機能を作っていきます。

Raw JavaScript の場合

models.py
from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    is_correct = models.BooleanField(default=False)
views.py
from .models import Question, Choice

class JsGetChoices(View):
    def get(self, request, *args, **kwargs):
        question_id = request.GET.get('question_id')
        choices = Choice.objects.filter(question_id=question_id)
        return JsonResponse({'choices': [{'id': choice.id, 'choice_text': choice.choice_text} for choice in choices]})
javascript.html
<html lang="ja">
    <head>
        <title>JS-Sample</title>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>

    <body class="p-6">
        <h1 class="text-3xl font-bold">
            JavaScript Sample
        </h1>

        <!-- 問題のセレクトボックス -->
        <div class="mt-6">
            <label for="questions" class="block mb-2 text-xl font-medium text-gray-900 dark:text-white">問題を選択してください。</label>
            <select id="questionSelect" class="bg-gray-50 border border-gray-300 text-gray-900 text-l rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                {% for question in questions %}
                    <option value="{{ question.id }}">{{ question.question_text }}</option>
                {% endfor %}
            </select>
        </div>

        <!-- 回答のセレクトボックス -->
        <div class="mt-6">
            <label for="choices" class="block mb-2 text-xl font-medium text-gray-900 dark:text-white">回答を選択してください。</label>
            <select id="choiceSelect" class="bg-gray-50 border border-gray-300 text-gray-900 text-l rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
            </select>
        </div>
    </body>

    <script>
        // 質問のセレクトボックスでの選択変更を監視
        document.getElementById('questionSelect').addEventListener('change', function() {
            const questionId = this.value;

            // サーバーから選択肢データを取得
            fetch(`/question/js/get-choices/?question_id=${questionId}`)
                .then(response => response.json())
                .then(data => {
                    const choiceSelect = document.getElementById('choiceSelect');

                    // 既存の選択肢をすべて削除
                    while (choiceSelect.firstChild) {
                        choiceSelect.removeChild(choiceSelect.firstChild);
                    }

                    // 取得した選択肢データをセレクトボックスに追加
                    data.choices.forEach(function(choice) {
                        const option = new Option(choice.choice_text, choice.id);
                        choiceSelect.add(option);
                    });
                });
        });
    </script>
</html>

スクリーンショット 2024-01-29 19.25.14.png

スクリーンショット 2024-01-29 19.25.22.png

このように、問題を選択すると、質問の選択肢が更新されます。

HTMX の場合

vews.py
from .models import Question, Choice

class HtmxGetChoices(View):
    def get(self, request, *args, **kwargs):
        question_id = request.GET.get('question_id')
        choices = Choice.objects.filter(question_id=question_id)
        return render(request, 'sample/parts/choice_list.html', {'choices': choices})
parts/choice_list.html
<select id="choiceSelect" class="bg-gray-50 border border-gray-300 text-gray-900 text-l rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
    {% for choice in choices %}
        <option value="{{ choice.id }}">{{ choice.choice_text }}</option>
    {% endfor %}
</select>
 htmx.html
<html lang="ja">
    <head>
        <title>HTMX-Sample</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://unpkg.com/htmx.org"></script>
    </head>

    <body class="p-6">
        <h1 class="text-3xl font-bold">
            HTMX Sample
        </h1>

        <!-- 問題のセレクトボックス -->
        <div class="mt-6">
            <label for="questions" class="block mb-2 text-xl font-medium text-gray-900 dark:text-white">問題を選択してください。</label>
            <!-- HTMXを使用して change event を処理する -->
            <select id="questionSelect" hx-get="/question/htmx/get-choices/" hx-vals='js:{"question_id": calculateValue()}' hx-trigger="change" hx-target="#choiceSelect" hx-swap="outerHTML"
                class="bg-gray-50 border border-gray-300 text-gray-900 text-l rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
            >
                {% for question in questions %}
                    <option name="question" value="{{ question.id }}">{{ question.question_text }}</option>
                {% endfor %}
            </select>
        </div>

        <!-- 回答のセレクトボックス -->
        <div class="mt-6">
            <label for="choices" class="block mb-2 text-xl font-medium text-gray-900 dark:text-white">回答を選択してください。</label>
            <!-- HTMXを使用して選択肢を更新する -->
            <select id="choiceSelect" class="bg-gray-50 border border-gray-300 text-gray-900 text-l rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
            </select>
        </div>
    </body>

    <script>
        function calculateValue() {
            return document.getElementById('questionSelect').value;
        }
    </script>
</html>

スクリーンショット 2024-01-29 19.28.27.png

スクリーンショット 2024-01-29 19.28.34.png

選択された option の value を親要素である select に渡すのに苦労しました。結局、JSで選択された option の value を返す関数を作って、select に渡すようにしましたが、よりスマートな方法があれば教えていただきたいです。

それでも、HTMXを使用することで確かにJSの記述量は(コード全体の記述量も)格段に減りました。大規模あるいは複雑なWebアプリケーションの場合はReactなどを導入してフロントエンドを分離させるべきだと思いますが、小規模かつ単純なWebアプリケーションにおいてHTMXは有用な選択肢かもしれません。今後もう少し本格的に使用してみたいと思います。

参考資料

公式ドキュメント

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?