查看源代码 绑定

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-clickphx-click-away
表单事件phx-changephx-submitphx-feedback-forphx-feedback-groupphx-disable-withphx-trigger-actionphx-auto-recover
焦点事件phx-blurphx-focusphx-window-blurphx-window-focus
按键事件phx-keydownphx-keyupphx-window-keydownphx-window-keyupphx-key
滚动事件phx-viewport-topphx-viewport-bottom
DOM 修补phx-mountedphx-updatephx-remove
JS 交互phx-hook
生命周期事件phx-connectedphx-disconnected
速率限制phx-debouncephx-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-blurphx-focus 绑定绑定到发出这些事件的 DOM 元素,例如

<input name="email" phx-focus="myfocus" phx-blur="myblur"/>

要检测页面本身何时获得焦点或失去焦点,可以使用 phx-window-focusphx-window-blur。如果所考虑的元素(通常是没有任何 tabindex 的 div)无法获得焦点,则可能需要这些窗口级事件。与其他绑定一样,可以在绑定元素上提供 phx-value-*,这些值将作为有效负载的一部分发送。例如

<div class="container"
    phx-window-focus="page-active"
    phx-window-blur="page-inactive"
    phx-value-page="123">
  ...
</div>

按键事件

onkeydownonkeyup 事件通过 phx-keydownphx-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-keyupphx-keydown 不支持输入。而是使用表单绑定,如 phx-changephx-submit 等。

注意:某些浏览器功能(如自动填充)可能会触发没有 "key" 字段的按键事件,该字段存在于发送到服务器的值映射中。出于这个原因,我们建议始终为 LiveView 按键绑定提供一个后备 catch-all 事件处理程序。默认情况下,绑定元素将是事件监听器,但可以使用 phx-window-keydownphx-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-debouncephx-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-clickphx-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-connectedphx-disconnected 绑定,以便使用 JS 命令对连接生命周期事件做出反应。例如,在 LiveView 失去连接时显示元素,并在连接恢复时隐藏它

<div id="status" class="hidden" phx-disconnected={JS.show()} phx-connected={JS.hide()}>
  Attempting to reconnect...
</div>

phx-connectedphx-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-topphx-viewport-bottom 绑定允许您检测容器的第一个子元素何时到达视窗的顶部,或最后一个子元素何时到达视窗的底部。这对于无限滚动很有用,您希望在用户上下滚动并到达视窗的顶部或底部时,发送下一个结果集或上一个结果集的分页事件。

通常,应用程序在执行无限滚动时会在容器的上面和下面添加填充,以在加载结果时允许平滑滚动。结合 Phoenix.LiveView.stream/3phx-viewport-topphx-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 重置为分页第一页。