コンテンツにスキップ

Django

このガイドは Hypermedia Components を Django + htmx と組み合わせます。 任意の django-htmx パッケージは 便利なリクエスト側ヘルパーを足しますが、以下はすべてそれなしでも 動きます。

HC の dist ファイルを static ディレクトリに置き、base.html から参照 します。

static/
assets/
hc/
hc.css
hc.behaviors.min.js
macros/
index.min.js
htmx.min.js
{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}My app{% endblock %}</title>
<link rel="stylesheet" href="{% static 'assets/hc/hc.css' %}">
<script defer src="{% static 'assets/htmx.min.js' %}"></script>
<script type="module" src="{% static 'assets/hc/hc.behaviors.min.js' %}"></script>
<script type="module" src="{% static 'assets/hc/macros/index.min.js' %}"></script>
{# auto-init の installCsrfHeader() ビヘイビアが毎リクエスト読み取り、
data-header で Django の期待するヘッダー名に変えます。 #}
<meta name="csrf-token" content="{{ csrf_token }}" data-header="X-CSRFToken">
</head>
<body>
{% block content %}{% endblock %}
<div class="hc-toast-region"
data-hc-toast-region
role="region"
aria-label="Notifications"></div>
</body>
</html>

<meta name="csrf-token"> タグはキットの blessed な CSRF 規約です: auto-init の behaviors バンドルに入っている installCsrfHeader() が リクエスト時に読み取り、すべての htmx リクエストにヘッダーを付与します — data-header で Django の X-CSRFToken に改名します。JavaScript を 書く必要はなく、meta に差し替えられたローテーション後のトークンも 自動で拾われます。(htmx ネイティブの配線が好みなら <body>data-hx-headers でも動きます — htmx → CSRF を参照。)

再利用可能なテンプレートスニペットは HC のパーツクラスとよく組み合い ます。シンプルなフィールドのインクルード:

{# templates/components/_field.html #}
<div class="hc-field"{% if invalid %} data-invalid="true"{% endif %}>
<label class="hc-field__label" for="{{ name }}">{{ label }}</label>
<input
id="{{ name }}"
name="{{ name }}"
value="{{ value|default_if_none:'' }}"
class="hc-input"
{% if invalid %}
aria-invalid="true"
aria-describedby="{{ name }}-error"
{% endif %}>
{% if message %}
<p id="{{ name }}-error" class="hc-field__message">{{ message }}</p>
{% endif %}
</div>

ページからの利用:

<form
method="post" action="{% url 'users:create' %}"
data-hx-post="{% url 'users:create' %}"
data-hx-target="this"
data-hx-swap="outerHTML">
{% include 'components/_field.html' with name='email' label='Email' value=form.email.value message=form.email.errors|first invalid=form.email.errors %}
<button class="hc-button" data-variant="primary" type="submit">Create</button>
</form>

カスタム HTML を書かずに Django のフォームウィジェットへ hc-input を 適用するには、フォームクラスで属性を設定します:

class UserForm(forms.Form):
email = forms.EmailField(
widget=forms.EmailInput(attrs={'class': 'hc-input'})
)

または {% django-widget-tweaks %} ライブラリを使ってテンプレート側で:

{% load widget_tweaks %}
{{ form.email|add_class:"hc-input"|attr:"aria-invalid:true" }}

htmx リクエストには、ターゲット領域のフラグメントだけを返します。 フラグメントごとに専用テンプレートを使うか、request.htmx で分岐 します。

views.py
from django.shortcuts import render
from django.template.loader import render_to_string
from django.http import HttpResponse
def list_items(request):
items = Item.objects.all()
template = 'items/_rows.html' if request.htmx else 'items/list.html'
return render(request, template, {'items': items})
def delete_item(request, item_id):
item = get_object_or_404(Item, pk=item_id)
item.delete()
return HttpResponse(
'',
headers={
'HX-Trigger': json.dumps({
'hc:toast': {
'message': f'Deleted "{item.name}".',
'variant': 'success',
},
}),
},
)

items/_rows.html:

{% for item in items %}
<tr id="item-{{ item.id }}">
<td>{{ item.name }}</td>
<td>
<span class="hc-badge" data-variant="{{ item.status }}">
{{ item.get_status_display }}
</span>
</td>
<td>
<span class="hc-action">
<button
class="hc-button"
data-size="sm"
data-variant="error"
type="button"
data-hc-confirm="Delete {{ item.name }}?"
data-hx-delete="{% url 'items:delete' item.id %}"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
data-hx-disabled-elt="this"
data-hx-indicator="closest .hc-action">
Delete
</button>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</span>
</td>
</tr>
{% endfor %}

クライアント側の通知には HX-Trigger レスポンスヘッダーを使います。 hc:toast のドキュメント化されたリスナーがトーストビヘイビアです。

import json
from django.http import HttpResponse
def save_item(request):
# …mutate state…
return HttpResponse(
rendered_fragment,
headers={
'HX-Trigger': json.dumps({
'hc:toast': {'message': 'Saved.', 'variant': 'success'},
}),
},
)

小さなヘルパーで JSON エンコードの繰り返しを避けられます:

def hx_trigger(response, events):
response['HX-Trigger'] = json.dumps(events)
return response
  • Django は POST / PUT / PATCH / DELETE に CSRF トークンを要求します。 base.html<meta name="csrf-token" … data-header="X-CSRFToken"> タグ(上記レイアウト参照)だけで十分です — installCsrfHeader() が すべての htmx リクエストにヘッダーを付与します。フォームボディで トークンを運べない DELETE / PATCH も含めてです。
  • meta タグはサーバ側で {{ csrf_token }} を読むため、JavaScript が CSRF クッキーを読む必要は一切ありません — 多層防御として CSRF_COOKIE_HTTPONLY = True を設定することもできます。
  • トークンのローテーション: Django はログイン時に CSRF トークンを ローテートします。レイアウトをキャッシュせず(上記のように) テンプレートから meta タグを描画すれば、次のフルページロードで 新しいトークンが届きます。

django-htmx ミドルウェアは request.htmx(htmx リクエストで truthy)、request.htmx.boosted、およびリクエストヘッダーを属性として 追加します。ミドルウェアなしなら、自分でヘッダーを確認します:

def list_items(request):
is_htmx = request.headers.get('HX-Request') == 'true'
template = 'items/_rows.html' if is_htmx else 'items/list.html'
return render(request, template, {'items': Item.objects.all()})
  • whitenoise — 本番でハッシュつきアセット URL が機能するよう STATIC_ROOTSTATICFILES_STORAGE を設定してください。HC の アセットは静的で、キャッシュバスティングの恩恵を受けます。
  • form.as_p のデフォルト — 同梱のフォーム描画は hc-input を 出力しません。フィールドを手で描画するか、widget attrs / widget-tweaks でクラスを注入してください。
  • HX-Trigger-After-Settle — htmx が DOM 変更のセトリングを終えた 後にだけ発火すべきイベントには、HX-Trigger の代わりに HX-Trigger-After-Settle ヘッダーを使ってください。