Skip to content

用HTMX和Alpine.JS实现表单提交(Django v6)

Updated: at 05:51 AM

Table of contents

Open Table of contents

构建项目开发环境

技术栈:

在之前的博客中,有介绍过如果利用HTMX和Alpine.JS来实现表单的提交,感兴趣的可以参考这篇文章用HTMX和Alpine.JS实现表单提交

这篇文章主要是因为对Django 6中新出的特性template partials特别感兴趣,想看下这个能为目前的流程带来哪些优化!

表单提交

这次我们以一个更特别的场景来演示:Web APP可以配置数据库的连接,在保存credential之前允许用户先验证准确性。

DB Connection Form

首先我们准备一个基本的表单,但是把表单触发的逻辑分别交给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,usernamepassword会被提交到django服务器。

为了改善用户体验,在提交请求之前(@htmx:before-send)我们修改testing和saving的状态,这样用户可以知道目前的执行状态。

DB Connection Testing

有人会问,那什么时候把状态改回来?不用改,因为我们返回的是新的表单,而hx-target和hx-swap指定了去替换原来的。这样之前的状态会被重置,同时还可以显示服务器返回的message。

def verify(request):
    sleep(3)
    messages.success(request, "Password is correct!")
    return render(request, 'index.html#demo-form')

DB Connection Tested

Save的逻辑是类似的,这里就不赘述了。

功能改进

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

DB Connection Tested 2

改进的方案就是:

{% 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来返回,不需要单独写一个文件,非常方便。

DB Connection Tested 3

Tips:由于现在Testing Connection和Save的返回需要替换的内容不再是同一个元素了,所以我们需要为他们各自定义hx-target和hx-swap。同时,hx-target和hx-swap是可以被继承的,如果值是一样的话,只要在父元素定义一次就好了,像第一个示例。

总结

django的新特性还是非常好用的,结合HTMX以及Alpine.JS简直了!哈哈哈哈~