查看源代码 绑定
Phoenix 支持 DOM 元素绑定以实现客户端-服务器交互。例如,要对按钮点击做出反应,您将渲染该元素
<button phx-click="inc_temperature">+</button>
然后在服务器端,所有 LiveView 绑定都由 handle_event
回调处理,例如
def handle_event("inc_temperature", _value, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
绑定 | 属性 |
---|---|
参数 | phx-value-* |
点击事件 | phx-click ,phx-click-away |
表单事件 | phx-change ,phx-submit ,phx-feedback-for ,phx-feedback-group ,phx-disable-with ,phx-trigger-action ,phx-auto-recover |
焦点事件 | phx-blur ,phx-focus ,phx-window-blur ,phx-window-focus |
按键事件 | phx-keydown ,phx-keyup ,phx-window-keydown ,phx-window-keyup ,phx-key |
滚动事件 | phx-viewport-top ,phx-viewport-bottom |
DOM 修补 | phx-mounted ,phx-update ,phx-remove |
JS 交互 | phx-hook |
生命周期事件 | phx-connected ,phx-disconnected |
速率限制 | phx-debounce ,phx-throttle |
静态跟踪 | phx-track-static |
点击事件
phx-click
绑定用于将点击事件发送到服务器。当任何客户端事件(如 phx-click
点击)被推送时,发送到服务器的值将根据以下优先级选择
在
Phoenix.LiveView.JS.push/3
中指定的:value
,例如<div phx-click={JS.push("inc", value: %{myvar1: @val1})}>
任何数量的可选
phx-value-
前缀属性,例如<div phx-click="inc" phx-value-myvar1="val1" phx-value-myvar2="val2">
将发送以下参数映射到服务器
def handle_event("inc", %{"myvar1" => "val1", "myvar2" => "val2"}, socket) do
如果使用
phx-value-
前缀,服务器有效负载也将包含"value"
(如果元素的 value 属性存在)。有效负载还将包含客户端事件的任何其他用户定义元数据。例如,以下
LiveSocket
客户端选项将为所有点击发送坐标和altKey
信息let liveSocket = new LiveSocket("/live", Socket, { params: {_csrf_token: csrfToken}, metadata: { click: (e, el) => { return { altKey: e.altKey, clientX: e.clientX, clientY: e.clientY } } } })
phx-click-away
事件在元素外部发生点击事件时触发。这对于隐藏切换的容器(如下拉菜单)很有用。
焦点和模糊事件
焦点和模糊事件可以使用 phx-blur
和 phx-focus
绑定绑定到发出这些事件的 DOM 元素,例如
<input name="email" phx-focus="myfocus" phx-blur="myblur"/>
要检测页面本身何时获得焦点或失去焦点,可以使用 phx-window-focus
和 phx-window-blur
。如果所考虑的元素(通常是没有任何 tabindex 的 div
)无法获得焦点,则可能需要这些窗口级事件。与其他绑定一样,可以在绑定元素上提供 phx-value-*
,这些值将作为有效负载的一部分发送。例如
<div class="container"
phx-window-focus="page-active"
phx-window-blur="page-inactive"
phx-value-page="123">
...
</div>
按键事件
onkeydown
和 onkeyup
事件通过 phx-keydown
和 phx-keyup
绑定得到支持。每个绑定都支持 phx-key
属性,该属性为特定按键触发事件。如果没有提供 phx-key
,则任何按键都会触发该事件。推送时,发送到服务器的值将包含所按的 "key"
,以及任何用户定义的元数据。例如,按下 Escape 键如下所示
%{"key" => "Escape"}
要捕获其他用户定义的元数据,可以在 LiveSocket
构造函数中为 keydown 事件提供 metadata
选项。例如
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
metadata: {
keydown: (e, el) => {
return {
key: e.key,
metaKey: e.metaKey,
repeat: e.repeat
}
}
}
})
要确定按下了哪个键,您应该使用 key
值。可以在 MDN 或通过 按键事件查看器 上找到可用选项。
注意:phx-keyup
和 phx-keydown
不支持输入。而是使用表单绑定,如 phx-change
、phx-submit
等。
注意:某些浏览器功能(如自动填充)可能会触发没有 "key"
字段的按键事件,该字段存在于发送到服务器的值映射中。出于这个原因,我们建议始终为 LiveView 按键绑定提供一个后备 catch-all 事件处理程序。默认情况下,绑定元素将是事件监听器,但可以使用 phx-window-keydown
或 phx-window-keyup
提供窗口级绑定,例如
def render(assigns) do
~H"""
<div id="thermostat" phx-window-keyup="update_temp">
Current temperature: <%= @temperature %>
</div>
"""
end
def handle_event("update_temp", %{"key" => "ArrowUp"}, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", %{"key" => "ArrowDown"}, socket) do
{:ok, new_temp} = Thermostat.dec_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", _, socket) do
{:noreply, socket}
end
使用防抖和节流限制事件速率
除 phx-blur
绑定(立即触发)外,所有事件都可以使用 phx-debounce
和 phx-throttle
绑定在客户端进行速率限制。
速率限制和防抖事件具有以下行为
phx-debounce
- 接受整数超时值(以毫秒为单位),或"blur"
。提供整数时,将延迟指定毫秒数后发出事件。提供"blur"
时,将在用户模糊字段后延迟发出事件。省略值时,将使用默认值 300ms。防抖通常用于输入元素。phx-throttle
- 接受一个整数超时值,以毫秒为单位对事件进行节流。与防抖不同,节流会立即发出事件,然后以每次提供的超时时间限制一次速率。省略值时,将使用默认值 300ms。节流通常用于限制点击、鼠标和键盘操作的速率。
例如,要避免在字段失去焦点之前验证电子邮件,同时在用户更改字段后最多每 2 秒验证用户名
<form phx-change="validate" phx-submit="save">
<input type="text" name="user[email]" phx-debounce="blur"/>
<input type="text" name="user[username]" phx-debounce="2000"/>
</form>
并且要将音量调高点击的速率限制为每秒一次
<button phx-click="volume_up" phx-throttle="1000">+</button>
同样,您可以节流按下并保持的 keydown
<div phx-window-keydown="keydown" phx-throttle="500">
...
</div>
除非需要按下并保持的键,否则通常更好的方法是使用 phx-keyup
绑定,它只在松开键时触发,从而实现自限制。但是,phx-keydown
对于游戏和其他需要持续按下某个键的用例很有用。在这种情况下,应始终使用节流。
防抖和节流的特殊行为
对于表单和 keydown 绑定,将执行以下专门的行为
当触发
phx-submit
或针对不同输入的phx-change
时,将重置现有输入的任何当前防抖或节流计时器。phx-keydown
绑定仅针对键重复进行节流。连续的唯一按键将调度按下的按键事件。
JS 命令
LiveView 绑定通过 Phoenix.LiveView.JS
模块支持 JavaScript 命令接口,该接口允许您指定在触发 phx-
绑定事件(如 phx-click
、phx-change
等)时在客户端执行的实用操作。命令组合在一起,使您可以推送事件、向元素添加类、元素进出转换等等。有关完整用法,请参阅 Phoenix.LiveView.JS
文档。
为了举一个关于可能性的小例子,假设您想要显示和隐藏页面上的模态窗口,而无需往返服务器来渲染内容
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.show(to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.hide(to: "#modal", transition: "fade-out")}>
hide modal
</button>
<button phx-click={JS.toggle(to: "#modal", in: "fade-in", out: "fade-out")}>
toggle modal
</button>
或者,如果您的 UI 库依赖于类来执行显示或隐藏
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.add_class("show", to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
命令组合在一起。例如,您可以将事件推送到服务器,并立即在客户端隐藏模态窗口
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.push("modal-closed") |> JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
将命令提取到它们自己的函数中也很有用
alias Phoenix.LiveView.JS
def hide_modal(js \\ %JS{}, selector) do
js
|> JS.push("modal-closed")
|> JS.remove_class("show", to: selector, transition: "fade-out")
end
<button phx-click={hide_modal("#modal")}>hide modal</button>
Phoenix.LiveView.JS.push/3
命令特别强大,它允许您自定义推送到服务器的事件。例如,假设您从熟悉的 phx-click
开始,该 phx-click
在点击时将消息推送到服务器
<button phx-click="clicked">click</button>
现在想象您想自定义 "clicked"
事件被推送时发生的事情,例如哪个组件应该被定位、哪个元素应该接收 CSS 加载状态类等等。这可以通过 JS 推送命令的选项来实现。例如
<button phx-click={JS.push("clicked", target: @myself, loading: ".container")}>click</button>
有关所有支持选项,请参阅 Phoenix.LiveView.JS.push/3
。
DOM 修补
可以使用 phx-update
标记容器,以配置 DOM 的更新方式。支持以下值
replace
- 默认操作。用内容替换元素stream
- 支持流操作。流用于管理 UI 中的大型集合,而无需在服务器上存储该集合ignore
- 无论新内容更改如何,都忽略对 DOM 的更新。这对于与执行自身 DOM 操作的现有库的客户端交互很有用
使用 phx-update
时,必须始终在容器中设置唯一的 DOM ID。如果使用“stream”,也必须为每个子元素设置 DOM ID。当插入包含容器中已存在 ID 的流元素时,LiveView 将使用新内容替换现有元素。有关更多信息,请参阅 Phoenix.LiveView.stream/3
。
当您需要与另一个 JS 库集成时,通常会使用“ignore”行为。从服务器到元素内容和属性的更新将被忽略,但数据属性除外。从服务器到数据属性的更改、添加和删除将与被忽略的元素合并,该元素可用于将数据传递给 JS 处理程序。
要对元素被挂载到 DOM 中做出反应,可以使用 phx-mounted
绑定。例如,要在挂载时为元素设置动画
<div phx-mounted={JS.transition("animate-ping", time: 500)}>
如果在初始页面渲染时使用 phx-mounted
,它只会在建立初始 WebSocket 连接后才会被调用。
要对元素被从 DOM 中移除做出反应,可以使用 phx-remove
绑定,该绑定可以包含要执行的 Phoenix.LiveView.JS
命令。 phx-remove
命令仅对被移除的父元素执行。它不会级联到子元素。
生命周期事件
LiveView 支持 phx-connected
和 phx-disconnected
绑定,以便使用 JS 命令对连接生命周期事件做出反应。例如,在 LiveView 失去连接时显示元素,并在连接恢复时隐藏它
<div id="status" class="hidden" phx-disconnected={JS.show()} phx-connected={JS.hide()}>
Attempting to reconnect...
</div>
phx-connected
和 phx-disconnected
仅在 LiveView 容器内操作时才执行。对于静态模板,它们将不起作用。
LiveView 特定事件
The lv:
事件前缀支持 LiveView 特定的功能,这些功能由 LiveView 处理,无需调用用户的 handle_event/3
回调。目前,支持以下事件:
lv:clear-flash
– 当发送到服务器时,清除闪存。如果提供了phx-value-key
,则将从闪存中删除该特定键。
例如
<p class="alert" phx-click="lv:clear-flash" phx-value-key="info">
<%= Phoenix.Flash.get(@flash, :info) %>
</p>
加载状态和错误
所有 phx-
事件绑定在推送时应用自己的 CSS 类。例如,以下标记
<button phx-click="clicked" phx-window-keydown="key">...</button>
在点击时,将接收 phx-click-loading
类,在按下键时,将接收 phx-keydown-loading
类。CSS 加载类将一直保持,直到客户端收到推送事件的确认。
在表单的情况下,当将 phx-change
发送到服务器时,发出更改的输入元素将接收 phx-change-loading
类,以及父表单标签。以下事件将接收 CSS 加载类
phx-click
-phx-click-loading
phx-change
-phx-change-loading
phx-submit
-phx-submit-loading
phx-focus
-phx-focus-loading
phx-blur
-phx-blur-loading
phx-window-keydown
-phx-keydown-loading
phx-window-keyup
-phx-keyup-loading
此外,以下类将应用于 LiveView 的父容器
"phx-connected"
- 在视图已连接到服务器时应用"phx-loading"
- 在视图未连接到服务器时应用"phx-error"
- 在服务器上发生错误时应用。请注意,如果与服务器的连接丢失,此类将与"phx-loading"
一起应用。
有关导航相关的加载状态(自动和手动),请参阅 phx-page-loading
,如 JavaScript 交互性:实时导航事件 中所述。
滚动事件和无限流分页
phx-viewport-top
和 phx-viewport-bottom
绑定允许您检测容器的第一个子元素何时到达视窗的顶部,或最后一个子元素何时到达视窗的底部。这对于无限滚动很有用,您希望在用户上下滚动并到达视窗的顶部或底部时,发送下一个结果集或上一个结果集的分页事件。
通常,应用程序在执行无限滚动时会在容器的上面和下面添加填充,以在加载结果时允许平滑滚动。结合 Phoenix.LiveView.stream/3
,phx-viewport-top
和 phx-viewport-bottom
允许创建仅在 DOM 中保留少量实际元素的无限虚拟化列表。例如
def mount(_, _, socket) do
{:ok,
socket
|> assign(page: 1, per_page: 20)
|> paginate_posts(1)}
end
defp paginate_posts(socket, new_page) when new_page >= 1 do
%{per_page: per_page, page: cur_page} = socket.assigns
posts = Blog.list_posts(offset: (new_page - 1) * per_page, limit: per_page)
{posts, at, limit} =
if new_page >= cur_page do
{posts, -1, per_page * 3 * -1}
else
{Enum.reverse(posts), 0, per_page * 3}
end
case posts do
[] ->
assign(socket, end_of_timeline?: at == -1)
[_ | _] = posts ->
socket
|> assign(end_of_timeline?: false)
|> assign(:page, new_page)
|> stream(:posts, posts, at: at, limit: limit)
end
end
我们的 paginate_posts
函数获取一页帖子,并确定用户是否正在翻页到上一页或下一页。根据分页方向,流要么被追加到,要么被追加到,其 at
分别为 0
或 -1
。我们还将流的 limit
设置为 per_page
的三倍,以允许 UI 中有足够的帖子显示为无限列表,但足够小以保持 UI 性能。我们还设置了一个 @end_of_timeline?
赋值以跟踪用户是否已到达结果的末尾。最后,我们更新 @page
赋值和帖子流。然后,我们可以将容器连接起来以支持视窗事件
<ul
id="posts"
phx-update="stream"
phx-viewport-top={@page > 1 && "prev-page"}
phx-viewport-bottom={!@end_of_timeline? && "next-page"}
phx-page-loading
class={[
if(@end_of_timeline?, do: "pb-10", else: "pb-[calc(200vh)]"),
if(@page == 1, do: "pt-10", else: "pt-[calc(200vh)]")
]}
>
<li :for={{id, post} <- @streams.posts} id={id}>
<.post_card post={post} />
</li>
</ul>
<div :if={@end_of_timeline?} class="mt-5 text-[50px] text-center">
🎉 You made it to the beginning of time 🎉
</div>
这里没有太多内容,但这就是重点!这段 UI 代码片段驱动着具有双向无限滚动的完全虚拟化列表。我们使用 phx-viewport-top
绑定将 "prev-page"
事件发送到 LiveView,但前提是用户已超出第一页。加载负数页结果没有意义,因此在这些情况下我们完全删除绑定。接下来,我们将 phx-viewport-bottom
连接起来以发送 "next-page"
事件,但前提是我们尚未到达时间线的末尾。最后,我们有条件地应用一些 CSS 类,这些类根据当前的分页设置顶部和底部填充为视窗高度的两倍,以实现平滑滚动。
为了完成我们的解决方案,我们只需要在 LiveView 中处理 "prev-page"
和 "next-page"
事件即可
def handle_event("next-page", _, socket) do
{:noreply, paginate_posts(socket, socket.assigns.page + 1)}
end
def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, paginate_posts(socket, 1)}
end
def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, paginate_posts(socket, socket.assigns.page - 1)}
else
{:noreply, socket}
end
end
此代码只是调用我们定义的第一个步骤的 paginate_posts
函数,使用当前页或下一页来驱动结果。请注意,我们在 "prev-page"
事件中匹配了一个特殊的 "_overran" => true
参数。当用户“超过”了视窗顶部或底部时,视窗事件会发送此参数。想象一下,用户通过许多页结果向上滚动,但抓住了滚动条并立即返回到页面顶部。这意味着我们的 <ul id="posts">
容器被视窗顶部超过,我们需要将 UI 重置为分页第一页。