Phoenix LiveView 0.19 发布

发布于 2023 年 5 月 29 日,作者 Chris McCord


LiveView 0.19.0 发布了!此版本包含期待已久的动态表单功能、新的流基元,并弥合了您希望 LiveView 能够实现的功能差距。这是我们在 LiveView 1.0 之前计划的最后一个主要版本。

开源 TodoTrek 演示应用程序

正如我在 ElixirConfEU 主题演讲 中演示并承诺的那样,我正在开源 TodoTrek 应用程序,这是一个类似 Trello 的应用程序,展示了新的流功能,例如拖放、无限滚动、动态表单等等。

以下是 TodoTrek 的实际操作

现在进入功能!

增强了带有重置和限制的流接口

LiveView 0.18 中的流引入了在客户端处理大型集合的强大方法,而无需在服务器内存中存储集合。流允许开发人员在 UI 上向集合中追加、前置或任意插入和更新项目。这为许多类似 SPA 的用例打开了大门,例如实时时间线和无限馈送,但缺少几个基元。

通过 LiveView 0.19,流弥合了这些基元的差距。流可以使用 :limit 选项在 UI 上进行限制。这允许开发人员指示 UI 在将新项目插入流时保留前 N 个项目或集合中的 N 个项目。这对许多情况至关重要,例如防止客户端被 DOM 中过多的数据淹没。结合新的视口绑定,可以轻松实现虚拟化的无限列表,我们将在稍后看到这一点。

除了流限制之外,流现在还支持 :reset 选项,该选项在更新流时会清除客户端上的流容器。这对于任何需要清除结果或重置列表的情况都很有用,例如搜索自动完成或传统分页。

只需一个简单的 stream(socket, :posts, posts, limit: 10)stream(socket, :posts, [], reset: true),您现在就可以完成复杂的客户端集合处理,而无需考虑它。但这只是可能的开始。

带有拖放的嵌套流

LiveView 0.19 支持嵌套流,这允许仅用几行代码实现拖放。想象一下一个 Trello 板,其中有命名的列表,这些列表包含待办事项。您可以拖放重新排序列表本身,或列表内的待办事项。您还可以将待办事项拖放到不同的列表中。所有这些现在在 LiveView 中都是可能的,并且在客户端和服务器上只需要极少的代码。对于客户端的拖放本身,您可以引入自己的库并使用十行代码进行集成。例如,以下是 TodoTrek 用于处理待办事项和列表的拖放代码

import Sortable from "../vendor/sortable"
Hooks.Sortable = {
  mounted(){
    let group = this.el.dataset.group
    let sorter = new Sortable(this.el, {
      group: group ? {name: group, pull: true, put: true} : undefined,
      animation: 150,
      dragClass: "drag-item",
      ghostClass: "drag-ghost",
      onEnd: e => {
        let params = {old: e.oldIndex, new: e.newIndex, to: e.to.dataset, ...e.item.dataset}
        this.pushEventTo(this.el, this.el.dataset["drop"] || "reposition", params)
      }
    })
  }
}

在这里,我们导入了 sortable.js,然后将其连接为一个 phx-hook。现在,任何流容器都可以使用以下标记将流连接到拖放

<div id="todos" phx-update="stream" phx-hook="Sortable">
  ...
</div>

当一个项目被放置时,LiveView 将收到一个“重新定位”事件,其中包含新的和旧的索引,以及任何存在的数据属性。

用于虚拟化、无限滚动的视口绑定

LiveView 0.19 引入了两个用于处理视口事件的新 phx 绑定 – phx-viewport-topphx-viewport-bottom。当容器的第一个子元素到达视口顶部或最后一个子元素到达视口底部时,会触发这些事件。将这两个事件与流限制结合起来,使您可以执行“虚拟化”的无限滚动,其中只有少数项目存在于 DOM 中,而用户却体验到一个大型或无限的项目集。在 LiveView 0.19 之前,这种功能要求用户逃离复杂的 JavaScript 钩子,但现在不再需要了。

使用新的 inputs_for 实现动态表单

现在,使用 inputs_for 动态添加和删除输入得到支持,它通过为插入和删除渲染复选框来实现。诸如 Ecto 之类的库或自定义参数过滤可以检查参数并处理添加或删除的字段。这可以与 Ecto.Changeset.cast/3:sort_param:drop_param 选项结合使用。例如,假设一个父 Ecto.Schema 带有一个 :emails has_manyembeds_many 关联。要从嵌套的 inputs_for 中转换用户输入,只需配置选项即可

schema "lists" do
  field :title, :string

  embeds_many :emails, EmailNotification, on_replace: :delete do
    field :email, :string
    field :name, :string
  end
end

def changeset(list, attrs) do
  list
  |> cast(attrs, [:title])
  |> cast_embed(:emails,
    with: &email_changeset/2,
    sort_param: :emails_sort,
    drop_param: :emails_drop
  )
end

在这里,我们看到 :sort_param:drop_param 选项的实际操作。

注意:使用这些选项时,has_manyembeds_many 上的 on_replace: :delete 是必需的。

当 Ecto 看到表单中指定的排序或删除参数时,它将根据它们在表单中出现的顺序对子项进行排序,添加它以前从未见过的子项,或者如果删除参数指示这样做,则删除子项。

此类模式和关联的标记如下所示

<.inputs_for :let={ef} field={@form[:emails]}>
  <input type="hidden" name="list[emails_sort][]" value={ef.index} />
  <.input type="text" field={ef[:email]} placeholder="email" />
  <.input type="text" field={ef[:name]} placeholder="name" />
  <label>
    <input type="checkbox" name="list[emails_drop][]" value={ef.index} class="hidden" />
    delete
  </label>
</.inputs_for>

<label class="block cursor-pointer">
  <input type="checkbox" name="list[emails_sort][]" class="hidden" />
  add more
</label>

我们使用 inputs_for:emails 关联渲染输入,该关联包含每个子项的电子邮件地址和姓名输入。在嵌套的输入中,我们渲染了一个隐藏的 list[emails_sort][] 输入,该输入设置为给定子项的索引。这告诉 Ecto 的转换操作如何对现有子项进行排序,或者在何处插入新子项。接下来,我们照常渲染电子邮件和姓名输入。然后,我们渲染一个包含“删除”文本的标签,以及一个名为 list[emails_drop][] 的隐藏复选框输入,该输入包含子项的索引作为其值。与以前一样,这告诉 Ecto 在复选框被选中时删除此索引处的子项。将复选框和文本内容包装在标签中,使标签中的任何点击内容都将选中和取消选中复选框。

最后,在 inputs_for 之外,我们渲染另一个带有无值 list[emails_sort][] 复选框的标签,并附带“添加更多”文本。Ecto 将未知排序参数视为新子项并构建一个新的子项。

这种方法的一大优势是它允许 LiveView 和传统视图使用相同的表单和转换基元。

弥合了可能的差距

0.19 中的功能允许轻松实现以前被限制为单页应用程序的各种用例。考虑一下像 Slack 一样具有无限滚动历史记录的聊天消息,或像 Twitter 一样的双向社交时间线,或像 Trello 一样具有嵌套拖放重新排序功能的待办事项应用程序。现在,无需考虑客户端上必要的细节即可实现这一切。此版本使我们能够专注于发布 LiveView 1.0。敬请关注!

编码愉快!

–Chris