Table of contents
构建项目开发环境
技术栈:
- Alpine.js (3.15)
- HTMX (2.0)
- tailwindcss (4.1)
- daisyUI (5)
- Django (6.0)
- django-browser-reload
在之前的博客中,有介绍过如果利用HTMX和Alpine.JS来实现表单的提交,感兴趣的可以参考这篇文章用HTMX和Alpine.JS实现表单提交。
这篇文章主要是因为对Django 6中新出的特性template partials特别感兴趣,想看下这个能为目前的流程带来哪些优化!
表单提交
这次我们以一个更特别的场景来演示:Web APP可以配置数据库的连接,在保存credential之前允许用户先验证准确性。

首先我们准备一个基本的表单,但是把表单触发的逻辑分别交给Test Connection和Save。因为表单本身只能通过action指定一个URL,当处理多个请求时,比如上面两个按钮,要么一个请求绑定到form action,另一个用js fetch或者htmx Javascript API处理。
这种设计会把用到大量js代码,如果用按钮触发表单提交,代码量就大大减少了,而且逻辑也清晰。
对应的代码:
{% partialdef demo-form inline %}
<form
class="space-y-6"
id="target"
x-data="{
testing: false,
saving: false
}"
>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Database</label
>
<div class="mt-2">
<input type="text" name="database" required class="input w-full" />
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Username</label
>
<div class="mt-2">
<input type="text" name="username" required class="input w-full" />
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Password</label
>
<div class="mt-2">
<input type="text" name="password" required class="input w-full" />
</div>
</div>
{% if messages %} {% for message in messages %} {% if message.level ==
DEFAULT_MESSAGE_LEVELS.ERROR %}
<div role="alert" class="alert alert-error alert-soft">
<span>{{ message }}</span>
</div>
{% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
<div role="alert" class="alert alert-success alert-soft">
<span>{{ message }}</span>
</div>
{% endif %} {% endfor %} {% endif %}
<div
class="flex items-center justify-between gap-x-2"
hx-target="#target"
hx-swap="outerHTML"
>
<div class="flex-1">
<button
class="btn btn-outline btn-primary w-full"
hx-post="{% url 'verify' %}"
@htmx:before-send="testing=true"
>
<span x-show="!testing">Test Connection</span>
<span x-show="testing"
><span class="loading loading-ring loading-md"></span>Testing...</span
>
</button>
</div>
<div class="flex-1">
<button
class="btn btn-outline btn-success w-full"
hx-post="{% url 'save' %}"
@htmx:before-send="saving=true"
>
<span x-show="!saving">Save</span>
<span x-show="saving"
><span class="loading loading-ring loading-md"></span>Saving...</span
>
</button>
</div>
</div>
</form>
{% endpartialdef demo-form %}
可以看到我们在示例中用到了django的新特性{% partialdef %}{% endpartialdef %}, 加了inline的话就可以直接在定义的地方渲染出来,不然得用{% partial %}来指定渲染位置。
把这个文件单独保存成html,接着用{% include %}也可以实现同样的功能,但是include是渲染整个html,没办法渲染一小部分。利用partial我们甚至可以把很多重复利用的组件放到一个html里面,然后用{% include ‘template.html#demo-partial’ %}来使用。
没错,django-cotton已经实现了组件化,但是这种原生特性还是用起来很方便的。
另外,在HTMX中,只要form中的任意element触发了non-GET请求,比如POST,那么request body会默认使用form里面的所有input,用属性name来识别各自,不需要额外使用hx-include去指定提交数据。示例中的database,username和password会被提交到django服务器。
为了改善用户体验,在提交请求之前(@htmx:before-send)我们修改testing和saving的状态,这样用户可以知道目前的执行状态。

有人会问,那什么时候把状态改回来?不用改,因为我们返回的是新的表单,而hx-target和hx-swap指定了去替换原来的。这样之前的状态会被重置,同时还可以显示服务器返回的message。
def verify(request):
sleep(3)
messages.success(request, "Password is correct!")
return render(request, 'index.html#demo-form')

Save的逻辑是类似的,这里就不赘述了。
功能改进
虽然说testing的状态会被重置,有个side-effect就是当服务器判断username或者password错误的话,错误信息会显示,可是需要修改的字段也被重置了。用户得重新再输入,很不方便。

改进的方案就是:
- Test Connection的响应只返回验证状态
- Save的响应不变,依旧返回新的表单并替换旧的,显示message
{% partialdef demo-form inline %}
<form
class="space-y-6"
id="target"
x-data="{
testing: false,
saving: false
}"
>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Database</label
>
<div class="mt-2">
<input type="text" name="database" required class="input w-full" />
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Username</label
>
<div class="mt-2">
<input type="text" name="username" required class="input w-full" />
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900"
>Password</label
>
<div class="mt-2">
<input type="text" name="password" required class="input w-full" />
</div>
</div>
<div id="message">
{% partialdef message inline %} {% if messages %} {% for message in messages
%} {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
<div role="alert" class="alert alert-error alert-soft">
<span>{{ message }}</span>
</div>
{% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
<div role="alert" class="alert alert-success alert-soft">
<span>{{ message }}</span>
</div>
{% endif %} {% endfor %} {% endif %} {% endpartialdef message %}
</div>
<div class="flex items-center justify-between gap-x-2">
<div class="flex-1">
<button
class="btn btn-outline btn-primary w-full"
hx-post="{% url 'verify' %}"
hx-target="#message"
hx-swap="innerHTML"
@htmx:before-send="testing=true"
@htmx:after-request="testing=false"
>
<span x-show="!testing">Test Connection</span>
<span x-show="testing"
><span class="loading loading-ring loading-md"></span>Testing...</span
>
</button>
</div>
<div class="flex-1">
<button
class="btn btn-outline btn-success w-full"
hx-post="{% url 'save' %}"
hx-target="#target"
hx-swap="outerHTML"
@htmx:before-send="saving=true"
>
<span x-show="!saving">Save</span>
<span x-show="saving"
><span class="loading loading-ring loading-md"></span>Saving...</span
>
</button>
</div>
</div>
</form>
{% endpartialdef demo-form %}
可以看到,Test Connection把返回的数据把id=“message”的元素替换了,这样的话输入的数据依旧还在。但是由于这个响应不会重置表单,意味着testing的状态需要加一段逻辑来重置@htmx:after-request="testing=false"
def verify(request):
sleep(3)
messages.error(request, "Password is wrong!")
return render(request, 'index_v2.html#message')
这里可以直接使用定义好的message来返回,不需要单独写一个文件,非常方便。

Tips:由于现在Testing Connection和Save的返回需要替换的内容不再是同一个元素了,所以我们需要为他们各自定义hx-target和hx-swap。同时,hx-target和hx-swap是可以被继承的,如果值是一样的话,只要在父元素定义一次就好了,像第一个示例。
总结
django的新特性还是非常好用的,结合HTMX以及Alpine.JS简直了!哈哈哈哈~