Loading Indicators for Events With JS.push

Problem

Phoenix/LiveView apps involve a lot of interaction between the client and the server. We want to customize how we indicate to users that our UI is waiting for a server response to some event. Specifically, we'd like to see a general-purpose loading animation triggered, or we'd like to apply a visual cue to a specific part of our app.

Solution

Depending on the desired effect, we can use the JS.push command to customize one (or both) of two approaches built into LiveView: a loading animation, such as LiveView's default progress bar, or the temporary loading classes applied to elements when a phx- event is pushed.

Let's explore how, using LiveBeats!

Setup

Our real-life example uses the JS.push API to send an event with a payload and a target, much as we did in an earlier post.

In LiveBeats we can listen to our favorite playlist and browse different friends' profiles at the same time, without interrupting the music. The secret behind the scenes is that we have two different LiveViews (PlayerLive and ProfileLive) that work independently and communicate by pushing events.

ProfileLive shows the profile and the playlist; PlayerLive takes care of playing whichever playlist we tell it to play.

The "Listen" button is part of ProfileLive. When we click it, we want the player, PlayerLive, to start playing the playlist currently displayed in the profile.

Here's how we achieve that with JS.push:

<.button phx-click={JS.push(
  "switch_profile", 
  value: %{user_id: @profile.user_id}, 
  target: "#player")}
>
  Listen
</.button>

The "Listen" button sends an event called switch_profile to the server. We define a payload that contains the user ID of the profile owner, and finally, we specify that the event should be handled by the component whose identifier is #player.

On the PlayerLive side, we need a handle_event callback to execute all the logic to switch to the current playlist:

def handle_event("switch_profile", %{"user_id" => user_id}, socket) do
  {:noreply, switch_profile(socket, user_id)}
end

We're ready to play music!

Triggering a Page Loading Animation

We've set up our profile-player communication. Now we want some visual feedback on the page while the server applies its changes.

By default, LiveView displays the topbar progress indicator at the top of the page when we navigate across our application, as well as on form submits. Can we trigger topbar when our custom events are being processed, too?

The secret is in the phx:page-loading-start and phx:page-loading-stop events. If we take a look in our app.js file, we find a listener for each of those events:

window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

The top bar is shown and hidden when events phx:page-loading-start and phx:page-loading-stop are emitted, respectively.

With this in mind: if we can emit those events, we can trigger the topbar animation. And that's what the page_loading option of JS.push is for!

This option takes a boolean value. If true, phx:page-loading-start is emitted when the event is pushed, and phx:page-loading-stop when the server has finished all its processing and responded with an acknowledgement.

We can extend the LiveBeats "Listen" button to use this and see the topbar:

JS.push(
      "switch_profile", 
      value: %{user_id: @profile.user_id}, 
      target: "#player",
      page_loading: true
)

This is an artificial example, because in general the LiveBeats profile change is so fast that adding a loading indicator would make it feel slower!

Using Temporary Loading Classes

The topbar animation makes our playlist change look the same as a full page reload. What if we want to indicate that just the player is waiting for an action to finish?

Whenever an event is sent using a LiveView phx- binding, the emitting element is given a temporary loading class (e.g. phx-click-loading) that can be used to apply some effect in CSS.

The JS.push loading option specifies further elements to receive this loading class.

JS.push(
      "switch_profile", 
      value:%{user_id: @profile.user_id}, 
      target: "#player",
      loading: "#player"
)

Now we can apply a fade effect to the #player that ends when the server responds with an acknowledgement for the switch_profile event.

We are indicating that the player is executing an action, but there isn't an entire page reloading.

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!  

Discussion

JS.push is a difficult utility to summarize.

We can simply use its API as an alternative syntax to send events to the back end. We can compose it with the client-side LiveView JS commands, to coordinate optimistic client-side effects. Today, we explored the unique loading-state options of JS.push, which allow us more control over how an app, or some part of it, indicates that it's awaiting a server response to a given event.

Put together, JS.push is a toolkit to push events and extend our creative options for how the front-end interacts and communicates with the user.