From 5829b6f9d363cf511b154be2bda8e0b9419204dc Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Sun, 15 Dec 2024 16:35:04 +0100 Subject: [PATCH] stream_insert update_only aka stream_update Fixes #2690. Relates to: https://elixirforum.com/t/add-stream-update-to-liveview-streams/68107 Work in progress, no tests yet. --- assets/js/phoenix_live_view/dom_patch.js | 9 +++++++-- lib/phoenix_live_view.ex | 6 +++++- lib/phoenix_live_view/engine.ex | 4 +++- lib/phoenix_live_view/live_stream.ex | 10 +++++----- lib/phoenix_live_view/test/dom.ex | 3 ++- test/phoenix_live_view/live_stream_test.exs | 14 +++++++++++--- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/assets/js/phoenix_live_view/dom_patch.js b/assets/js/phoenix_live_view/dom_patch.js index 09eb786d6..73620cdc7 100644 --- a/assets/js/phoenix_live_view/dom_patch.js +++ b/assets/js/phoenix_live_view/dom_patch.js @@ -125,6 +125,11 @@ export default class DOMPatch { } }, onBeforeNodeAdded: (el) => { + // don't add update_only nodes if they did not already exist + if(this.getStreamInsert(el)?.updateOnly && !this.streamComponentRestore[el.id]){ + return false + } + DOM.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom) this.trackBefore("added", el) @@ -278,8 +283,8 @@ export default class DOMPatch { liveSocket.time("morphdom", () => { this.streams.forEach(([ref, inserts, deleteIds, reset]) => { - inserts.forEach(([key, streamAt, limit]) => { - this.streamInserts[key] = {ref, streamAt, limit, reset} + inserts.forEach(([key, streamAt, limit, updateOnly]) => { + this.streamInserts[key] = {ref, streamAt, limit, reset, updateOnly} }) if(reset !== undefined){ DOM.all(container, `[${PHX_STREAM_REF}="${ref}"]`, child => { diff --git a/lib/phoenix_live_view.ex b/lib/phoenix_live_view.ex index bc1e2a3ac..314f015e5 100644 --- a/lib/phoenix_live_view.ex +++ b/lib/phoenix_live_view.ex @@ -1843,6 +1843,9 @@ defmodule Phoenix.LiveView do here as well in order to be enforced. See `stream/4` for more information on limiting streams. + * `:update_only` - A boolean to only update the item in the stream. If the item does not + exist on the client, it will not be inserted. Defaults to `false`. + ## Examples Imagine you define a stream on mount with a single item: @@ -1892,8 +1895,9 @@ defmodule Phoenix.LiveView do def stream_insert(%Socket{} = socket, name, item, opts \\ []) do at = Keyword.get(opts, :at, -1) limit = Keyword.get(opts, :limit) + update_only = Keyword.get(opts, :update_only, false) - update_stream(socket, name, &LiveStream.insert_item(&1, item, at, limit)) + update_stream(socket, name, &LiveStream.insert_item(&1, item, at, limit, update_only)) end @doc """ diff --git a/lib/phoenix_live_view/engine.ex b/lib/phoenix_live_view/engine.ex index 847178504..f3399368d 100644 --- a/lib/phoenix_live_view/engine.ex +++ b/lib/phoenix_live_view/engine.ex @@ -84,7 +84,9 @@ defmodule Phoenix.LiveView.Comprehension do @doc false def __annotate__(comprehension, %Phoenix.LiveView.LiveStream{} = stream) do - inserts = for {id, at, _item, limit} <- stream.inserts, do: [id, at, limit] + inserts = + for {id, at, _item, limit, update_only} <- stream.inserts, do: [id, at, limit, update_only] + data = [stream.ref, inserts, stream.deletes] if stream.reset? do diff --git a/lib/phoenix_live_view/live_stream.ex b/lib/phoenix_live_view/live_stream.ex index a88bef155..a48024b90 100644 --- a/lib/phoenix_live_view/live_stream.ex +++ b/lib/phoenix_live_view/live_stream.ex @@ -25,7 +25,7 @@ defmodule Phoenix.LiveView.LiveStream do # with manually calling stream_insert multiple times, as stream_insert prepends. items_list = for item <- items, reduce: [] do - items -> [{dom_id.(item), -1, item, opts[:limit]} | items] + items -> [{dom_id.(item), -1, item, opts[:limit], opts[:update_only]} | items] end %LiveStream{ @@ -64,10 +64,10 @@ defmodule Phoenix.LiveView.LiveStream do %{stream | deletes: [dom_id | stream.deletes]} end - def insert_item(%LiveStream{} = stream, item, at, limit) do + def insert_item(%LiveStream{} = stream, item, at, limit, update_only) do item_id = stream.dom_id.(item) - %{stream | inserts: [{item_id, at, item, limit} | stream.inserts]} + %{stream | inserts: [{item_id, at, item, limit, update_only} | stream.inserts]} end defimpl Enumerable, for: LiveStream do @@ -81,7 +81,7 @@ defmodule Phoenix.LiveView.LiveStream do # before rendering; we also remove duplicates to only use the most recent # inserts, which, as the items are reversed, are first {inserts, _} = - for {id, _, _, _} = insert <- stream.inserts, reduce: {[], MapSet.new()} do + for {id, _, _, _, _} = insert <- stream.inserts, reduce: {[], MapSet.new()} do {inserts, ids} -> if MapSet.member?(ids, id) do # skip duplicates @@ -106,7 +106,7 @@ defmodule Phoenix.LiveView.LiveStream do defp do_reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &do_reduce(list, &1, fun)} defp do_reduce([], {:cont, acc}, _fun), do: {:done, acc} - defp do_reduce([{dom_id, _at, item, _limit} | tail], {:cont, acc}, fun) do + defp do_reduce([{dom_id, _at, item, _limit, _update_only} | tail], {:cont, acc}, fun) do do_reduce(tail, fun.({dom_id, item}, acc), fun) end diff --git a/lib/phoenix_live_view/test/dom.ex b/lib/phoenix_live_view/test/dom.ex index 55f97bc8a..5497897b2 100644 --- a/lib/phoenix_live_view/test/dom.ex +++ b/lib/phoenix_live_view/test/dom.ex @@ -502,7 +502,8 @@ defmodule Phoenix.LiveViewTest.DOM do streamInserts = Enum.reduce(streams, %{}, fn %{ref: ref, inserts: inserts}, acc -> - Enum.reduce(inserts, acc, fn [id, stream_at, limit], acc -> + # TODO: support update_only in LiveViewTest + Enum.reduce(inserts, acc, fn [id, stream_at, limit, _update_only], acc -> Map.put(acc, id, %{ref: ref, stream_at: stream_at, limit: limit}) end) end) diff --git a/test/phoenix_live_view/live_stream_test.exs b/test/phoenix_live_view/live_stream_test.exs index 0c628cd8f..8e7964586 100644 --- a/test/phoenix_live_view/live_stream_test.exs +++ b/test/phoenix_live_view/live_stream_test.exs @@ -13,12 +13,20 @@ defmodule Phoenix.LiveView.LiveStreamTest do test "default dom_id" do stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], []) - assert stream.inserts == [{"users-2", -1, %{id: 2}, nil}, {"users-1", -1, %{id: 1}, nil}] + + assert stream.inserts == [ + {"users-2", -1, %{id: 2}, nil, nil}, + {"users-1", -1, %{id: 1}, nil, nil} + ] end test "custom dom_id" do stream = LiveStream.new(:users, 0, [%{name: "u1"}, %{name: "u2"}], dom_id: &"u-#{&1.name}") - assert stream.inserts == [{"u-u2", -1, %{name: "u2"}, nil}, {"u-u1", -1, %{name: "u1"}, nil}] + + assert stream.inserts == [ + {"u-u2", -1, %{name: "u2"}, nil, nil}, + {"u-u1", -1, %{name: "u1"}, nil, nil} + ] end test "default dom_id without struct or map with :id" do @@ -31,7 +39,7 @@ defmodule Phoenix.LiveView.LiveStreamTest do test "inserts are deduplicated (last insert wins)" do assert stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], []) - stream = LiveStream.insert_item(stream, %{id: 2, updated: true}, -1, nil) + stream = LiveStream.insert_item(stream, %{id: 2, updated: true}, -1, nil, false) stream = %{stream | consumable?: true} assert Enum.to_list(stream) == [{"users-1", %{id: 1}}, {"users-2", %{id: 2, updated: true}}] end