使用Htmx提供HTML内容#
- 作者:
本How-to说明了一种返回HTML内容并使用 htmx library 来处理AJAX请求。Htmx期望得到一个HTML响应,并使用它替换DOM中的元素(请参阅 htmx introduction 在文档中)。

警告
这是一个概念验证,展示了使用这两种技术可以实现什么。我们正在努力 plmustache 这将进一步改进本How-to的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可以识别您的Web浏览器发出的请求(使用 Accept: text/html
头),并返回原始的HTML文档文件。
create domain "text/html" as text;
创建一个HTML响应#
让我们创建一个函数,该函数返回一个基本的HTML文件,使用 Pico CSS 用于设置样式和 Ionicons 稍后显示一些图标。
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/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;
Web浏览器将在以下位置打开该网页 http://localhost:3000/rpc/index
。

列出和创建待办事项#
现在,让我们显示已插入到数据库中的待办事项列表。为此,我们还需要一个函数来帮助我们清理任务中可能出现的HTML内容。
create or replace function api.sanitize_html(text) returns text as $$
select replace(replace(replace(replace(replace($1, '&', '&'), '"', '"'),'>', '>'),'<', '<'), '''', ''')
$$ 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>
。The 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/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>
。这将确保正文中的所有htmx元素都发送此头,否则PostgREST不会将其识别为HTML。也有一个
<form>
使用htmx库的元素。让我们来分析一下:hx-post="/rpc/add_todo"
:将AJAX POST请求发送到/rpc/add_todo
终结点,并使用_task
从<input>
元素。hx-target="#todo-list-area"
:从请求返回的HTML内容将进入内部<div id="todo-list-area"></div>
(这是待办事项列表)。hx-trigger="submit"
:htmx将在提交表单时执行此请求(在<input>
)。hx-on="htmx:afterRequest: this.reset()">
:这是一个清除表单的Java脚本命令 after the request is done 。
有了这个, http://localhost:3000/rpc/index
页面列出了所有的待办事项,并通过在输入元素中提交任务来添加新的待办事项。别忘了刷新 schema cache 。

编辑和删除To-Dos#
现在,让我们修改一下 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
函数更新done
列使用_id
以及_done
来自请求的值。这个
api.delete_todo
函数删除待办事项。_id
来自请求的值。这个
api.change_todo_task
函数修改task
列使用_id
以及_task
来自请求的值。
刷新后 schema cache ,页面位于 http://localhost:3000/rpc/index
将允许我们编辑、删除和完成任何待办事项。

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