Allow calling DOM methods and supporting meta key bindings

I have two proposals - not sure if I should split them into different threads but I think they’re related:

  1. Add a new JS.call/2 function that allows us to invoke native DOM methods.
  2. Support meta key bindings (e.g. cmd + k) either through phx-key or by adding a new phx-meta-key binding.

Calling DOM methods

Today, to invoke native DOM methods (e.g. dialog.showModal(), dialog.close(), etc.) in LiveView, we must either create a custom hook or dispatch an event.

Proposal

Introduce a new function:

JS.call("method_name", to: "#selector", args: [...])

This would allow us to easily call DOM methods without having to write any JavaScript. A common use case would be opening a dialog element:

<dialog id="my_dialog">...</dialog>

<button phx-click={JS.call("show_modal", to: "#my_dialog")}>
  open dialog
</button>

Meta key bindings

Similarly, Phoenix LiveView has some useful key bindings like phx-keydown, phx-keyup, etc. When working on complex UIs, we often need to provide keyboard shortcuts that use meta keys.

Today, we can either have a custom hook or a combination of phx-window-keydown and handle_event/3 to accomplish this.

Proposal

Either extend phx-key or add a new phx-meta-key binding for shortcuts:

<dialog id="my_dialog">...</dialog>

<button phx-meta-key="k" phx-window-keydown={JS.call("show_modal", to: "#my_dialog")}>
  open dialog
</button>

The example above would allow to open the dialog element using cmd+k or ctrl+k without the need to write any JavaScript nor handle_event/3 callbacks.


I think those additions would simplify a large class of UI behaviors and further reduce the need for custom hooks or imperative JS for native interactions. Happy to help with a PR or implementation sketch if needed.

2 Likes

I feel life call has been proposed before and Chris gave the solution of adding a custom event to JS.dispatch to that would dynamically call any function. I’m having trouble finding the post. I’d be pretty happy to have JS.call, though. I wouldn’t snake-ify the JS function names, though. Far less confusing and snake case if valid JS and while super uncommon, you never know.

Also, if we’re adding meta why not shift as well? Perhaps that’s the resistance?

1 Like

Oh, that was my bad. I didn’t mean to use snake case. It was supposed to use the JS method name (showModal). I can’t edit it anymore, though. But it would be JS.call("showModal", to: "#my_dialog").

But all decisions are about priorities, right? For example, why do we have JS.focus and not JS.blur? Or why do we have a phx-keydown binding and not phx-scroll or phx-dblclick?

If you think about it, even those could be generic. For example, we could use phx-scroll (or add a dom prefix for clarity) and it would add an event listener to it:

<.tab_bar
  phx-dom-window-scroll={JS.hide()}
  phx-dom-window-scrollend={JS.show()}
 />

<button phx-dom-pointerdown="move" phx-dom-pointerup="release">
  Hold to move 
</button>

<.card phx-dom-dblclick="rename_file">
  My file
</.card>
1 Like

On the calling DOM method side, Liveview in its current state enables you to write DOM access abstractions in userland without extending liveview’s current API surface.

I have written about a POC a few months ago here : What if LiveView gave DOM access to Elixir ?. This toy used hooks to trigger the commands but I now use JS.dispatch as @sodapopcan suggested.

My use case was querying and triggering layout operations, so what I needed was an abstraction allowing to run a single call, batches of calls, and in-order sequences of calls that make use of previous calls results.

A functional DSL to compose operations elixir-side would obviously feel nice, but this adds API complexity quickly so I’m not sure how I feel about Liveview embarking that kind of thing. The declarative power of function composition is great but the impedance mismatch with the imperative DOM API is high.

At its core, you could always have something like JS.dispatch("dom_op", to: "#your_element", detail: %{fun: :scroll_by, args: [10, 10]}).

If you convert a name to camel case, check if the name exists as a function or property on the target, and accordingly perform a read or call, you can cover 95% of use cases without explicitly spelling out every function.

Thanks for sharing. Your use case is definitely more complex than mine—nice work on the creative solution.

My idea was more about solving simpler things like opening/closing dialogs and handling keyboard shortcuts (cmd/ctrl), which I’ve had to implement in pretty much every app I’ve worked on.

It’d be great to have a way to handle that without needing a custom hook or turning a plain function component into a LiveComponent.

I would actually like this most of all so it I can render_dblclick() in LiveView tests.

2 Likes