Skip to content

Commit

Permalink
Add blocking: true to JS.dispatch
Browse files Browse the repository at this point in the history
Relates to: #3516

When integrating external animation libraries like motion.dev, the existing
JS functions are not sufficient. Instead, the third party library needs to
be triggered via JS. This has the downside of not being able to block the DOM
until the animation is complete, which prevents this from working when
elements are removed using `phx-remove`.

This commit introduces a new `blocking: true` option to `JS.dispatch/3`,
which injects a `done` function into the event's `detail` object.

Using this with motion could look like this:

```elixir
def render(assigns) do
  ~H"""
  <div :if={@show} phx-remove={JS.dispatch("motion:rotate", blocking: true)}>
    ...
  </div>
  """
end
```

```javascript
const { animate } = Motion

window.addEventListener("motion:rotate", (e) => {
  animate(e.target, { rotate: [0, 360] }, { duration: 1 }).then(() => {
    if (e.detail.done) {
      e.detail.done()
    }
  })
})
```

It is still necessary to block the DOM while the remove animation is
running, as the remove can happen because of a navigation, where the
animation would otherwise not run as the whole LiveView is just replaced.
  • Loading branch information
SteffenDE committed Jan 4, 2025
1 parent 751fe00 commit 340efb6
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 3 deletions.
8 changes: 7 additions & 1 deletion assets/js/phoenix_live_view/js.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ let JS = {
})
},

exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, {event, detail, bubbles}){
exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, {event, detail, bubbles, blocking}){
detail = detail || {}
detail.dispatcher = sourceEl
if(blocking){
const promise = new Promise((resolve, _reject) => {
detail.done = resolve
})
liveSocket.asyncTransition(promise)
}
DOM.dispatchEvent(el, event, {detail, bubbles})
},

Expand Down
16 changes: 15 additions & 1 deletion assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ export default class LiveSocket {
this.transitions.after(callback)
}

asyncTransition(promise){
this.transitions.addAsyncTransition(promise)
}

transition(time, onStart, onDone = function(){}){
this.transitions.addTransition(time, onStart, onDone)
}
Expand Down Expand Up @@ -993,6 +997,7 @@ export default class LiveSocket {
class TransitionSet {
constructor(){
this.transitions = new Set()
this.promises = new Set()
this.pendingOps = []
}

Expand All @@ -1001,6 +1006,7 @@ class TransitionSet {
clearTimeout(timer)
this.transitions.delete(timer)
})
this.promises.clear()
this.flushPendingOps()
}

Expand All @@ -1022,9 +1028,17 @@ class TransitionSet {
this.transitions.add(timer)
}

addAsyncTransition(promise){
this.promises.add(promise)
promise.then(() => {
this.promises.delete(promise)
this.flushPendingOps()
})
}

pushPendingOp(op){ this.pendingOps.push(op) }

size(){ return this.transitions.size }
size(){ return this.transitions.size + this.promises.size }

flushPendingOps(){
if(this.size() > 0){ return }
Expand Down
14 changes: 13 additions & 1 deletion lib/phoenix_live_view/js.ex
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ defmodule Phoenix.LiveView.JS do
with the client event. The details will be available in the
`event.detail` attribute for event listeners.
* `:bubbles` – A boolean flag to bubble the event or not. Defaults to `true`.
* `:blocking` - A boolean flag to block the UI until the event handler calls `event.detail.done()`.
The done function is injected by LiveView and *must* be called eventually to unblock the UI.
This is useful to integrate with third party JavaScript based animation libraries.
## Examples
Expand All @@ -245,7 +248,7 @@ defmodule Phoenix.LiveView.JS do

@doc "See `dispatch/2`."
def dispatch(%JS{} = js, event, opts) do
opts = validate_keys(opts, :dispatch, [:to, :detail, :bubbles])
opts = validate_keys(opts, :dispatch, [:to, :detail, :bubbles, :blocking])
args = [event: event, to: opts[:to]]

args =
Expand Down Expand Up @@ -284,6 +287,15 @@ defmodule Phoenix.LiveView.JS do
args
end

args =
case Keyword.get(opts, :blocking) do
true ->
Keyword.put(args, :blocking, opts[:blocking])

_ ->
args
end

put_op(js, "dispatch", args)
end

Expand Down

0 comments on commit 340efb6

Please sign in to comment.