LiveView DOM element bindings can be used to send events to the server, as well as issue LiveView JS commands on the client.
In another post, we used client-side JS commands to show and hide content in a set of tabs, just by manipulating DOM element attributes.
JS.push
is a bit different; it has one foot on the client side and one on the server side. On its own, JS.push
provides a combined API for pushing events to the server, specifying targets and payloads, and customizing loading states. As a LiveView JS command, it’s composable with the other JS commands to coordinate more complex, optimistic client-side effects.
In its basic event-pushing functionality, JS.push
provides an alternative syntax for some things you can already do with phx-*
bindings alone.
Let’s take a closer look at how we can migrate between pure phx-*
pushes and JS.push
. Then we’ll take it one step further and combine a push with an asynchronous transition effect, by composing JS.push
with JS.transition
.
Sending a simple click event to the server
If all you want to do is send an event called clicked
to the server, you can use the phx-click
binding and call it a day.
<button phx-click="clicked">Don't panic!</button>
Here’s how we can write the exact same thing using the JS.push
API:
<button phx-click={JS.push("clicked")}>Don't panic!</button>
This looks a little bit silly. But it will work! Either way, we’re sending the clicked
event to the server, where we’d have a handle_event
callback defined that knows what to do with clicked
events.
Here’s a super-simple callback that can receive our event and print the string Handling clicked event
to the iex console.
def handle_event("clicked", _values, socket) do
IO.inspect("Handling clicked event")
{:noreply, socket}
end
A click with a payload
Say we want to send an event called clicked
, with a payload of parameter values: val1
, val2
, and val3
. We can do it with phx-click
and phx-value-*
:
<button
phx-click="clicked"
phx-value-val1="At"
phx-value-val2="the"
phx-value-val3="disco!">
Panic!
</button>
Or we can use JS.push
, passing a map of values using its value
option:
<button
phx-click={
JS.push("clicked",
value: %{val1: "At", val2: "the", val3: "disco!"})
}
>
Panic!
</button>
Either way, our handle_event
callback receives a map as its values
parameter.
The following example callback assigns values for variables val1
, val2
, and val3
from the values
map, and concatenates them into a string called content
. Finally, it uses this string to update the value of the content
assign and returns the updated socket.
def handle_event("clicked", values, socket) do
%{"val1" => val1, "val2" => val2, "val3" => val3} = values
content = val1 <> " " <> val2 <> " " <> val3
{:noreply, assign(socket, :content, content)}
end
Some part of our LiveComponent would render the contents of the content
assign; for example:
When the button is clicked, content
gets updated and our concatenated string appears in the rectangle.
Pushing the event to a specific target
We can send events to a particular LiveView or LiveComponent, by specifying a DOM selector that matches an element or elements inside it. The owner of each matching DOM element will automatically receive the event.
Imagine we’ve defined a LiveView named ContentLive
and we render two ContentLive
s with DOM IDs content1
and content2
.
<%= live_render(@socket, ContentLive, id: "content1", session: %{}})%>
<%= live_render(@socket, ContentLive, id: "content2", session: %{}})%>
And the HTML content rendered inside them is the following:
~H"""
<div class="container_content">
<h1><%= @content %></h1>
</div>
"""
Each ContentLive
will render one div with the class container_content
. We can target an event to both divs using the class selector as follows.
With phx-click
and phx-target
:
<button phx-target=".container_content" phx-click="clicked">
Panic!
</button>
With JS.push
:
<button phx-click={JS.push("clicked", target: ".container_content")}>
Panic!
</button>
Either way, both of our ContentLive
LiveViews will receive and handle the clicked
event, because they each contain an element with class container_content
. We didn’t need to specify the ContentLive
s by id.
We can target a LiveView or LiveComponent directly by id if we want to:
# pure phx-bindings way
<button phx-target="#content1" phx-click="clicked">
Panic!
</button>
# JS.push way
<button phx-click={JS.push("clicked", target: "#content1")}>
Panic!
</button>
Composing JS.push with other JS commands
In all the above scenarios, JS.push
is doing nothing more than what the phx-*
bindings are doing. We haven’t seen any compelling reason to migrate to the JS.push
syntax.
For the moment, we’re putting aside the loading-state tweaks that JS.push
enables through its loading
and page_loading
options.
But here’s a cool trick! If we’re using JS.push
to send our event, we can orchestrate client-side transition effects along with our push, simply by composing it with other LiveView JS commands!
Let’s try with the JS.transition
command:
<button phx-click={
JS.push("clicked", value: %{val1: "At", val2: "the", val3: "disco!"})
|> JS.transition("shake", to: "#content")
}
>
Panic!
</button>
This applies a transition named shake
to the div with id content
, totally client-side, at the same time the event is being processed and the content
assign is changed.
Even when JS.push
itself is replicating push behavior that can be achieved with phx-*
bindings alone, we’ve found a use-case for preferring the JS.push
syntax, and one of its raisons d'être as a LiveView JS command.