Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add blocking: true to JS.dispatch #3615

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we error if detail.done already exists? Seems like something that could already be in use. That being said, since the user has to opt-in to blocking, it's not going to break for anyone upgrading LV.

})
view.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 @@ -986,6 +990,7 @@ export default class LiveSocket {
class TransitionSet {
constructor(){
this.transitions = new Set()
this.promises = new Set()
this.pendingOps = []
}

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

Expand All @@ -1015,9 +1021,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
23 changes: 23 additions & 0 deletions assets/test/js_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,29 @@ describe("JS", () => {

JS.exec(event, "close", close.getAttribute("phx-click"), view, close)
})

test("blocking blocks DOM updates until done", done => {
let view = setupView(`
<div id="modal">modal</div>
<div id="click" phx-click='[["dispatch", {"to": "#modal", "event": "custom", "blocking": true}]]'></div>
`)
let modal = simulateVisibility(document.querySelector("#modal"))
let click = document.querySelector("#click")
let doneCalled = false

modal.addEventListener("custom", (e) => {
expect(e.detail).toEqual({done: expect.any(Function), dispatcher: click})
expect(view.liveSocket.transitions.size()).toBe(1)
view.liveSocket.requestDOMUpdate(() => {
expect(doneCalled).toBe(true)
done()
})
// now we unblock the transition
e.detail.done()
doneCalled = true
})
JS.exec(event, "click", click.getAttribute("phx-click"), view, click)
})
})

describe("exec_add_class and exec_remove_class", () => {
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 @@ -261,6 +261,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 @@ -278,7 +281,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 @@ -317,6 +320,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
Loading