<!-- LLM_VERSION_INFO
FORMAT: text/markdown
CONTENT_TYPE: article
ORIGINAL_URL: https://fly.io/phoenix-files/server-triggered-js
ALTERNATE_VERSION: phoenix-files/server-triggered-js/index.html (text/html)
EXTRACTION_DATE: 2026-04-18T22:12:19.283Z

This is the markdown version with text-only content (images converted to alt-text).
For rich formatting with images, request the HTML version at: phoenix-files/server-triggered-js/index.html
-->

# Author

**Name**  
Berenice Medel  
@bemesa21[@bemesa21](https://twitter.com/bemesa21)
  
Image by [Annie Ruygt](https://annieruygtillustration.com/)

It’s always frustrating when we click something—and it looks like nothing’s happening. The default solution to this is to throw up a spinner while the server is chewing on a request, to help users resist the temptation to keep clicking. UX win!

We can do this on the client, and poll the server repeatedly, asking “Are you done yet?” But polling adds waiting time as well as traffic and load on the server. The server knows what it’s doing, and it knows when it’s done. We should get the server to show and hide the “Please wait while I get that for you…” spinner.

With LiveView, we have the tools!

## Problem

How can we create a loader that the **server** makes appear and disappear? And can we make that into a reusable component?

## Solution

Today we’ll create a loader component that appears asynchronously when we make a request to an external API that may take time to respond. For that we’ll apply an interesting trick; we’ll trigger JS commands from the server side!

### Defining a loader component

Before we start, I want to mention that my abilities developing CSS and HTML are not the best, so I used the amazing spinner designed and developed by Vasili Savitski and [Epicmax](http://epicmax.co/ ""); you can find many others [here](https://epic-spinners.epicmax.co/ "").

We package up the spinner into a `loader` function component that we can reuse:

```elixir
def loader(assigns) do
  ~H"""
  <div class="hidden h-full bg-slate-100" id={@id}>
    <div class="flex justify-center items-center h-full">
      <div class="flower-spinner">
        <div class="dots-container">
          <div class="bigger-dot">
            <div class="smaller-dot"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
  """
end
```

The spinner itself is just a collection of HTML elements within our component container, and its own CSS takes care of the fancy positioning and animation of its nested elements to create the pretty spinny pattern.

We use Tailwind classes to match the loader container size to its parent HTML element, and to center the spinner within its parent. The `hidden` class makes the spinner invisible by default.

Let’s render our spinner (without the `hidden` class) to see how it looks:

```elixir
def render(assigns) do
  ~H"""
    <.loader id="my_spinner"/>
  """
end
```

### Showing and hiding the loader

Now, how do we make our loader appear and disappear? [JS commands](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html "")!

```elixir
defp show_loader(js \ %JS{}, id) do
  JS.show(js,
    to: "##{id}",
    transition: {"ease-out duration-300", "opacity-0", "opacity-100"}
  )
end

defp hide_loader(js \ %JS{}, id) do
  JS.hide(js,
    to: "##{id}",
    transition: {"ease-in duration-300", "opacity-100", "opacity-0"}
  )
end
```

We use the `JS.show` and `JS.hide` commands, each one with a simple transition that changes the opacity of the loader’s container.

The commands are encapsulated inside the `show_loader/2` and `hide_loader/2` functions just for simplicity, as we’ll use them later.

Using the commands we defined above, we can show and hide our loader on the client side just by using [phx-bindings](https://hexdocs.pm/phoenix_live_view/bindings.html "") like `phx_click`:

```elixir
<button phx-click={show_loader("my_spinner")}>  
  Without the server!  
</button>
```

However, sometimes only the server (and the logic we define) knows when the processing has finished and the loader can be hidden again. This is where we apply the most important trick of this recipe!

### Triggering a JS command from the server side

The idea here is to push an event to the client from the server side each time we want to show or hide our loader, and have the event’s JS listener trigger the JS command we want. Neither the server nor the listener really needs to know exactly what should happen on the client when this event arrives! So here’s how we break it up:

- The loader’s outer HTML element has `data-*` attributes that store the JS Commands or functions that should be invoked when we start waiting, and when we’re done waiting.
- The server pushes an event whose payload consists of 1) an element id to target and 2) the name of a data attribute, like `data-ok-done`.
- The listener executes the JS indicated by the specified `data-*` attribute on the target element.

This is a highly-reusable pattern! Let’s use it:

First we specify the JS commands we want to trigger by adding them as attributes of our loader main container:

```elixir
def loader(assigns) do
  ~H"""
  <div
    class="hidden h-full bg-slate-100" id={@id}
    data-plz-wait={show_loader(@id)}
    data-ok-done={hide_loader(@id)}
  >
    .
    .
    .
  </div>
  """
end
```

The `JS.show` command we defined in the `show_loader/2` function is embedded inside the `data-plz-wait` attribute (the same happens with the `data-ok-done` attribute). In both cases, we pass the identifier of our loader as a parameter.

In our `app.js` file we add an event listener for the server-pushed event `js-exec`:

Note that when we push an event from the server using `Phoenix.LiveView.push_event/3` (as we’ll do later), the event name is dispatched in the browser with the `phx:` prefix.

```javascript
window.addEventListener("phx:js-exec", ({detail}) => {
    document.querySelectorAll(detail.to).forEach(el => {
        liveSocket.execJS(el, el.getAttribute(detail.attr))
    })
  })
```

This listener is a generic solution: we can trigger any JS command (we can even execute a set of JS commands!) just by embedding them inside an HTML attribute.

Let’s see how it works:

The listener function receives a `detail` object, which has two attributes: `to` and `attr`. `to` contains the identifier of one or more HTML elements, and `attr` is the name of the HTML attribute that embeds the JS command we want to trigger.

For each element matching the `to` identifier, we trigger the JS command contained in the element’s HTML attribute `attr`.

Finally, we can trigger our `js-exec` event by adding it to the socket and pushing it to the client by using the [push_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_event/3 "") function:

```elixir
push_event(socket, "js-exec", %{
  to: "#my_spinner",
  attr: "data-ok-done"
})
```

We send a map with the details that the listener is waiting for: the identifier of our spinner, and the name of the attribute that embeds the JS command we want to trigger.

This way we push the `js-exec` event to the client, and the listener receives the event and triggers the command embedded in the `data-ok-done` attribute.

### Loader in action

Speaking of limited skills, it’s difficult for me to choose colors that look good together, so an API like [this one](https://palett.es/api "") that generates a random color palette is very useful.

For our example, when we click a button, we send a request to the [Palett API](https://palett.es/api "") and display the generated colors in an interesting way:

We’ll define a new LiveView for our example. We won’t include _all_ the content here. You can check out the full [repo](https://github.com/bemesa21/phoenix_files/pull/1 "") to see everything. In this recipe we’ll go over the most important details.

The trick to using our loader lies in just a couple of functions:

```elixir
def handle_event("create_palette", _value, socket) do
  send(self(), :run_request)

socket = push_event(socket, "js-exec", %{
    to: "#my_spinner",
    attr: "data-plz-wait"
  })

{:noreply, socket}
end

def handle_info(:run_request, socket) do
  socket =
    socket
    |> assign(:colors, get_colors())
    |> push_event("js-exec", %{to: "#my_spinner", attr: "data-ok-done"})

{:noreply, socket}
end
```

When the `create_palette` event is received from the button, we send the `:run_request` message to our own component and add the `js-exec` event to the socket just before returning the `{:noreply, socket}` tuple. This way, we process the `:run_request` message while the spinner is displayed and it stays there.

On the other hand, the `handle_info` callback is in charge of asynchronously calling the API and adding the event to the socket to hide the spinner. Once the colors are fetched from the API, no matter how long it took to respond, the loader is hidden.

### Discussion

We created a reusable loader, and with only 4 lines of JavaScript, got the server to display it using JS commands! I can say that it took me more effort to think and come up with a fun example than to create the solution.

You can build on this. Maybe you have a background job that builds an invoice on demand. If you use Phoenix PubSub, then your far-away server can notify the waiting LiveView when the job is done, and your spinner can vanish as the invoice or link appears. The possibilities are myriad. I’ll be keeping an eye out for other solutions that involve triggering JS commands with server events!
