Pushing Events: with and without JS.push

Image by Annie Ruygt

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 ContentLives 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 ContentLives 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>

Fly ❤️ Elixir

Fly is an awesome place to run your Elixir apps. It’s really easy to get started. You can be running in minutes.

Deploy your Elixir app today!

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.