Skip to content

用HTMX和Alpine.JS实现表单提交

Updated: at 05:51 AM

Table of contents

Open Table of contents

构建项目开发环境

技术栈:

其中,django-browser-reload有点类似与Angular的HMR,每当html,js或者css在被改动之后保存,该模块会自动刷新页面以帮助开发者更快获得反馈。

在使用Alpine.js,HTMX和tailwindcss的时候,为了方便起见,我会使用CDN链接。如果对配置感兴趣的话也可以浏览这篇文章如何在Django中配置Alpine.js,HTMX和tailwindcss

<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
  />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>Demo</title>
  <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
  <script
    src="https://unpkg.com/[email protected]"
    integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
    crossorigin="anonymous"
  ></script>
  <script
    defer
    src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
  ></script>
</head>

HTMX Submit Form

表单提交

这里我们会以TodoList为原型,主要功能是添加Todo项目,保存到数据库,返回204响应并添加响应头HX-Trigger。

对应的代码:

def add_todo(request):
    if request.method == 'POST':
        new_todo = request.POST["todo"]
        Todo.objects.create(title=new_todo)
        headers = {
            "HX-Trigger": json.dumps({
                "show-message": {
                    "level": "info",
                    "message": "Todo added successfully!"
                },
                "todo-updated": None
            })
        }
        return HttpResponse(status=204, headers=headers)

HX-Trigger的功能就是在前端触发事件,以便运行监听该事件的代码,比如:刷新列表,弹消息框。如果只是单纯地触发事件,HX-Trigger:my-event就够了,但如果需要多加料让前端获取更多信息,那么就得以JSON的格式返回,所以需要用json.dumps()转换下。这里会触发两个事件show-messagetodo-updatedTips:Alpine.js只支持Kebab Case格式的事件即my-event,所以为了让show-message被x-on监听到,我们只能返回小写或者带有“-”的事件。但是HTMX均支持Camel Case和Kebab Case格式事件。

在流程图Step 3 - Return 204 response with HX-Trigger header中,我们也可以返回新的Todo list去替换当前的。而我选择返回204 No Content是因为在HX-Trigger中加入一个事件todo-updated,这样下面的代码就会发送一个请求到“todolist/”链接获取最新的Todo list。

<main>
  <div class="lg:px-8 mx-auto max-w-7xl sm:px-6">
    <div
      id="content"
      class="container mx-auto"
      hx-get="{% url 'todolist' %}"
      hx-trigger="load, todo-updated from:body"
    ></div>
  </div>
</main>

由于事件冒泡,todo-updated事件会一层层向上“冒泡”至body,所以我们需要添加from:body来监听事件,否则只会监听当前节点。

当“todolist/”请求被Django接收到,view函数会取出所有Todo对象,并且在todo.html模板中返回结果。

def todos(request):
    todos = Todo.objects.all()
    return render(request, template_name="form/todo.html", context={"todos": todos})
<div id="todo-list">
  <ul role="list" class="divide-y divide-gray-200">
    {% for todo in todos %}
    <li class="flex py-4">
      <div class="ml-3">
        <p class="text-lg font-bold text-gray-900">{{ todo }}</p>
      </div>
    </li>
    {% endfor %}
  </ul>
</div>
<form hx-post="{% url 'add' %}">
  <div class="flex">
    <div class="mr-3 w-full max-w-xs">
      <input
        type="text"
        placeholder="Name"
        name="todo"
        class="ring-offset-background flex h-10 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm placeholder:text-neutral-500 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
      />
    </div>

    <div class="flex justify-end">
      <button
        type="submit"
        class="ml-3 inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
      >
        Add
      </button>
    </div>
  </div>
</form>

显示消息框

消息框有非常多的样式,取决于开发者喜好。这里我们使用的是tailwindcss components中的消息框。 Alpinejs Show Notification

<div x-data="{ show: true, message: '' }" aria-live="assertive"
    @show-message.document="show=true; message=$event.detail.message; setTimeout(() => show=false, 2000)"
    class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
    <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
      <div
          x-show="show"
          x-transition:enter="transform ease-out duration-300 transition"
          x-transition:enter-start="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
          x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0"
          x-transition:leave="transition ease-in duration-100"
          x-transition:leave-start="opacity-100"
          x-transition:leave-end="opacity-0"
          class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
        ...
        ...
        ...
      </div>
    </dvi>
</div>

x-data中的show结合x-show将会控制消息框的显示与隐藏。x-transition可以帮助整个过程看上去更丝滑。

其中,这里的精华在于@show-message.document,它结合了HTMX的HX-Trigger事件和Alpine.js的x-on监听器。.document的功能类似于前面提到的from:body,为了监听到“冒泡”到最上层的show-message事件。

一旦发现了该事件,执行语句show=true; message=$event.detail.message; setTimeout(() => show=false, 2000)

Tips:message可以用x-text绑定。

总结

事件冒泡是HTMX与Alpine.js连接在一起的关键点。HX-Trigger触发事件,而x-on则使用.document或者.window来监听该事件。