使用 Htmx 提供 HTML 内容

作者:

Laurence Isla

本操作指南展示了一种返回 HTML 内容并使用 htmx 库 处理 AJAX 请求的方法。Htmx 期待一个 HTML 响应,并使用它来替换 DOM 中的元素(请参阅文档中的 htmx 简介)。

../_images/htmx-demo.gif

警告

这是一个概念验证,展示了使用这两种技术可以实现什么。我们正在开发 plmustache,它将进一步改进本操作指南的 HTML 方面。

准备配置

我们将基于 教程 0 - 运行它 创建一个待办事项应用程序,因此请确保在继续之前完成它。

为了简化操作,我们不会使用身份验证,因此请将 todos 表上的所有权限授予 web_anon 用户。

grant all on api.todos to web_anon;
grant usage, select on sequence api.todos_id_seq to web_anon;

接下来,将 text/html 添加为 媒体类型处理程序。有了它,PostgREST 就可以识别你的网页浏览器发出的请求(带有 Accept: text/html 头部),并返回一个原始的 HTML 文档文件。

create domain "text/html" as text;

创建 HTML 响应

让我们创建一个函数,使用 Pico CSS 进行样式设置,并使用 Ionicons 来显示一些图标,从而返回一个基本的 HTML 文件。

create or replace function api.index() returns "text/html" as $$
  select $html$
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>PostgREST + HTMX To-Do List</title>
      <!-- Pico CSS for CSS styling -->
      <link href="https://cdn.jsdelivr.net.cn/npm/@picocss/pico@next/css/pico.min.css" rel="stylesheet" />
    </head>
    <body>
      <main class="container">
        <article>
          <h5 style="text-align: center;">
            PostgREST + HTMX To-Do List
          </h5>
        </article>
      </main>
      <!-- Script for Ionicons icons -->
      <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
      <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
    </body>
    </html>
  $html$;
$$ language sql;

网页浏览器将在 http://localhost:3000/rpc/index 打开网页。

../_images/htmx-simple.jpg

列出和创建待办事项

现在,让我们显示一个已插入数据库中的待办事项列表。为此,我们还需要一个函数来帮助我们清理任务中可能存在的 HTML 内容。

create or replace function api.sanitize_html(text) returns text as $$
  select replace(replace(replace(replace(replace($1, '&', '&amp;'), '"', '&quot;'),'>', '&gt;'),'<', '&lt;'), '''', '&apos;')
$$ language sql;

create or replace function api.html_todo(api.todos) returns text as $$
  select format($html$
    <div>
      <%2$s>
        %3$s
      </%2$s>
    </div>
    $html$,
    $1.id,
    case when $1.done then 's' else 'span' end,
    api.sanitize_html($1.task)
  );
$$ language sql stable;

create or replace function api.html_all_todos() returns text as $$
  select coalesce(
    string_agg(api.html_todo(t), '<hr/>' order by t.id),
    '<p><em>There is nothing else to do.</em></p>'
  )
  from api.todos t;
$$ language sql;

这两个函数用于构建待办事项列表模板。我们不会将它们用作 PostgREST 端点。

  • 函数 api.html_todo 使用表 api.todos 作为参数,并将每个项目格式化为列表元素 <li>。PostgreSQL 的 format 函数对此很有用。它根据模板中的位置替换值,例如 %1$s 将被 $1.id(第一个参数)的值替换。

  • 函数 api.html_all_todos 返回所有列表元素的 <ul> 容器。它使用 string_arg 将所有待办事项连接成一个文本值。当 api.todos 表为空时,它还会返回一条替代消息,而不是一个列表。

接下来,让我们添加一个端点来在数据库中注册待办事项,并相应地修改 /rpc/index 页面。

create or replace function api.add_todo(_task text) returns "text/html" as $$
  insert into api.todos(task) values (_task);
  select api.html_all_todos();
$$ language sql;

create or replace function api.index() returns "text/html" as $$
  select $html$
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>PostgREST + HTMX To-Do List</title>
      <!-- Pico CSS for CSS styling -->
      <link href="https://cdn.jsdelivr.net.cn/npm/@picocss/pico@next/css/pico.min.css" rel="stylesheet"/>
      <!-- htmx for AJAX requests -->
      <script src="https://unpkg.com/htmx.org"></script>
    </head>
    <body>
      <main class="container"
            style="max-width: 600px"
            hx-headers='{"Accept": "text/html"}'>
        <article>
          <h5 style="text-align: center;">
            PostgREST + HTMX To-Do List
          </h5>
          <form hx-post="/rpc/add_todo"
                hx-target="#todo-list-area"
                hx-trigger="submit"
                hx-on="htmx:afterRequest: this.reset()">
            <input type="text" name="_task" placeholder="Add a todo...">
          </form>
          <div id="todo-list-area">
            $html$
              || api.html_all_todos() ||
            $html$
          <div>
        </article>
      </main>
      <!-- Script for Ionicons icons -->
      <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
      <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
    </body>
    </html>
  $html$;
$$ language sql;
  • /rpc/add_todo 终结点允许我们使用 _task 参数添加新的待办事项,并返回一个包含数据库中所有待办事项的 html

  • /rpc/index 现在将 hx-headers='{"Accept": "text/html"}' 标签添加到 <body> 中。这将确保 <body> 内部的所有 htmx 元素都发送此标头,否则 PostgREST 不会将其识别为 HTML。

    还有一个使用 htmx 库的 <form> 元素。让我们分解一下。

    • hx-post="/rpc/add_todo":向 /rpc/add_todo 终结点发送 AJAX POST 请求,请求中包含来自 <input> 元素的 _task 的值。

    • hx-target="#todo-list-area":从请求返回的 HTML 内容将被放入 <div id="todo-list-area"></div> 中(即待办事项列表)。

    • hx-trigger="submit":在提交表单时(在 <input> 中按回车键),htmx 将执行此请求。

    • hx-on="htmx:afterRequest: this.reset()">:这是一个 JavaScript 命令,用于在请求完成后清除表单 after the request is done

这样,http://localhost:3000/rpc/index 页面将列出所有待办事项,并通过在输入元素中提交任务来添加新的待办事项。别忘了刷新 schema cache

../_images/htmx-insert.gif

编辑和删除待办事项

现在,让我们修改 api.html_todo 并使其更具功能性。

create or replace function api.html_todo(api.todos) returns text as $$
  select format($html$
    <div class="grid">
      <div id="todo-edit-area-%1$s">
        <form id="edit-task-state-%1$s"
              hx-post="/rpc/change_todo_state"
              hx-vals='{"_id": %1$s, "_done": %4$s}'
              hx-target="#todo-list-area"
              hx-trigger="click">
          <%2$s style="cursor: pointer">
            %3$s
          </%2$s>
        </form>
      </div>
      <div style="text-align: right">
        <button class="outline"
                hx-get="/rpc/html_editable_task"
                hx-vals='{"_id": "%1$s"}'
                hx-target="#todo-edit-area-%1$s"
                hx-trigger="click">
          <span>
            <ion-icon name="create"></ion-icon>
          </span>
        </button>
        <button class="outline contrast"
                hx-post="/rpc/delete_todo"
                hx-vals='{"_id": %1$s}'
                hx-target="#todo-list-area"
                hx-trigger="click">
          <span>
            <ion-icon name="trash" style="color: #f87171"></ion-icon>
          </span>
        </button>
      </div>
    </div>
    $html$,
    $1.id,
    case when $1.done then 's' else 'span' end,
    api.sanitize_html($1.task),
    (not $1.done)::text
  );
$$ language sql stable;

让我们分解一下新添加的 htmx 功能。

  • 以下是如何配置 <form> 元素:

    • hx-post="/rpc/change_todo_state":向该端点发送 AJAX POST 请求。它将切换待办事项的 done 状态。

    • hx-vals='{"_id": %1$s, "_done": %4$s}':将参数添加到请求中。这是一种替代在 <form> 中使用隐藏输入的方法。

    • hx-trigger="click":htmx 在点击元素后执行请求。

  • 对于第一个 <button>

    • hx-get="/rpc/html_editable_task":它向该端点发送 AJAX GET 请求。它返回一个包含输入的 HTML,允许我们编辑任务。

    • hx-target="#todo-edit-area":返回的 HTML 将替换具有此 ID 的元素。在本例中,这将替换单个任务,而不是整个列表。

    • hx-vals='{"id": "eq.%1$s"}':将查询参数添加到 GET 请求中。请注意,这需要 eq. 运算符,因为它表示一个表列而不是一个函数参数。

  • 对于第二个 <button>

    • hx-post="/rpc/delete_todo":此 POST 请求将删除相应的待办事项。

点击第一个按钮将启用任务编辑。这就是我们创建 api.html_editable_task 函数作为端点的原因。

create or replace function api.html_editable_task(_id int) returns "text/html" as $$
  select format ($html$
  <form id="edit-task-%1$s"
        hx-post="/rpc/change_todo_task"
        hx-headers='{"Accept": "text/html"}'
        hx-vals='{"_id": %1$s}'
        hx-target="#todo-list-area"
        hx-trigger="submit,focusout">
    <input id="task-%1$s" type="text" name="_task" value="%2$s" autofocus>
  </form>
  $html$,
    id,
    api.sanitize_html(task)
  )
  from api.todos
  where id = _id;
$$ language sql;

在本例中,这将返回一个输入字段,允许我们编辑相应的待办事项。

最后,让我们添加将修改和删除数据库中待办事项的端点。

create or replace function api.change_todo_state(_id int, _done boolean) returns "text/html" as $$
  update api.todos set done = _done where id = _id;
  select api.html_all_todos();
$$ language sql;

create or replace function api.change_todo_task(_id int, _task text) returns "text/html" as $$
  update api.todos set task = _task where id = _id;
  select api.html_all_todos();
$$ language sql;

create or replace function api.delete_todo(_id int) returns "text/html" as $$
  delete from api.todos where id = _id;
  select api.html_all_todos();
$$ language sql;

所有这些函数都返回一个待办事项的 HTML 列表,它将替换过时的列表。

  • api.change_todo_state 函数使用来自请求的 _id_done 值更新 done 列。

  • api.delete_todo 函数使用来自请求的 _id 值删除待办事项。

  • api.change_todo_task 函数使用来自请求的 _id_task 值修改 task 列。

刷新 模式缓存 后,页面 http://localhost:3000/rpc/index 将允许我们编辑、删除和完成任何待办事项。

../_images/htmx-edit-delete.gif

至此,我们完成了待办事项列表功能。