Skip to content

Commit 7f8b237

Browse files
authored
Add onDocumentPatch callback and allow specifying event dispatch phase (#4043)
Relates to: https://elixirforum.com/t/extending-liveview-with-view-transition-api-support/73136 This commit adds a generic dom hook that runs before the view is patched: ```javascript let liveSocket = new LiveSocket("/live", Socket, { dom: { onDocumentPatch(start) { // do something // then trigger the patch start(); } } }) ``` This can be used to call [document.startViewTransition](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition). To be able to customize what kind of view transition to perform, this commit also introduces a new optional fourth parameter on `Phoenix.LiveView.push_event/3`. By default, events are only dispatched after the view is patched, but sometimes it is useful to run some code before patching the DOM: ```elixir def mount(_params, _session, socket) do {:ok, socket |> assign(...) |> push_event("start-view-transition", %{type: "page"}, dispatch: :before)} end ``` This event will be dispatched before performing the initial mount patch.
1 parent 517afcb commit 7f8b237

File tree

4 files changed

+75
-8
lines changed

4 files changed

+75
-8
lines changed

assets/js/phoenix_live_view/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ export interface LiveSocketOptions {
146146
query: string,
147147
defaultQuery: () => Element[],
148148
) => Element[];
149+
/**
150+
* When defined, called with a start callback that needs to be called
151+
* to perform the actual patch. Failing to call the start callback causes
152+
* the page to become stuck.
153+
*
154+
* This can be used to delay patches in order to perform view transitions,
155+
* for example:
156+
*
157+
* ```javascript
158+
* let liveSocket = new LiveSocket("/live", Socket, {
159+
* dom: {
160+
* onDocumentPatch(start) {
161+
* document.startViewTransition(start);
162+
* }
163+
* }
164+
* })
165+
* ```
166+
*
167+
* It is strongly advised to call start as quickly as possible.
168+
*/
169+
onDocumentPatch?: (start: () => void) => void;
149170
/**
150171
* Called immediately before a DOM patch is applied.
151172
*/

assets/js/phoenix_live_view/view.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,36 @@ export default class View {
316316
applyDiff(type, rawDiff, callback) {
317317
this.log(type, () => ["", clone(rawDiff)]);
318318
const { diff, reply, events, title } = Rendered.extract(rawDiff);
319-
callback({ diff, reply, events });
320-
if (typeof title === "string" || (type == "mount" && this.isMain())) {
321-
window.requestAnimationFrame(() => DOM.putTitle(title));
319+
320+
// Events are either [event, payload] or [event, payload, true]
321+
// where the optional third element (true) indicates that the event should
322+
// be dispatched before the DOM patch. This is useful in combination with
323+
// the onDocumentPatch dom callback.
324+
const ev = events.reduce(
325+
(acc, args) => {
326+
if (args.length === 3 && args[2] == true) {
327+
acc.pre.push(args.slice(0, -1));
328+
} else {
329+
acc.post.push(args);
330+
}
331+
return acc;
332+
},
333+
{ pre: [], post: [] },
334+
);
335+
336+
this.liveSocket.dispatchEvents(ev.pre);
337+
338+
const update = () => {
339+
callback({ diff, reply, events: ev.post });
340+
if (typeof title === "string" || (type == "mount" && this.isMain())) {
341+
window.requestAnimationFrame(() => DOM.putTitle(title));
342+
}
343+
};
344+
345+
if ("onDocumentPatch" in this.liveSocket.domCallbacks) {
346+
this.liveSocket.triggerDOM("onDocumentPatch", [update]);
347+
} else {
348+
update();
322349
}
323350
}
324351

lib/phoenix_live_view.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,8 +779,17 @@ defmodule Phoenix.LiveView do
779779
)
780780
```
781781
782+
## Specifying the dispatch phase
783+
784+
By default, events pushed with `push_event/3` are only dispatched after
785+
the LiveView is patched. In some cases, handling an event before the LiveView
786+
is patched can be useful though. To do this, the `dispatch` option can be passed
787+
as fourth argument:
788+
789+
{:noreply, push_event(socket, "scores", %{points: 100, user: "josé"}, dispatch: :before)}
790+
782791
"""
783-
defdelegate push_event(socket, event, payload), to: Phoenix.LiveView.Utils
792+
defdelegate push_event(socket, event, payload, opts \\ []), to: Phoenix.LiveView.Utils
784793

785794
@doc ~S"""
786795
Allows an upload for the provided name.

lib/phoenix_live_view/utils.ex

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,22 @@ defmodule Phoenix.LiveView.Utils do
267267
@doc """
268268
Annotates the changes with the event to be pushed.
269269
270-
Events are dispatched on the JavaScript side only after
270+
By default, events are dispatched on the JavaScript side only after
271271
the current patch is invoked. Therefore, if the LiveView
272-
redirects, the events won't be invoked.
272+
redirects, the events won't be invoked. If the `dispatch: :before` option
273+
is passed, this event will be dispatched before patching the DOM.
273274
"""
274-
def push_event(%Socket{} = socket, event, %{} = payload) do
275-
update_in(socket.private.live_temp[:push_events], &[[event, payload] | &1 || []])
275+
def push_event(%Socket{} = socket, event, %{} = payload, opts) do
276+
opts = Keyword.validate!(opts, [:dispatch])
277+
dispatch_phase = Keyword.get(opts, :dispatch, :after)
278+
279+
case dispatch_phase do
280+
:after ->
281+
update_in(socket.private.live_temp[:push_events], &[[event, payload] | &1 || []])
282+
283+
:before ->
284+
update_in(socket.private.live_temp[:push_events], &[[event, payload, true] | &1 || []])
285+
end
276286
end
277287

278288
@doc """

0 commit comments

Comments
 (0)