Spice up your LiveView app with a cool loading spinner!

lcezermf

Luiz Cezer

Posted on December 15, 2023

Spice up your LiveView app with a cool loading spinner!

When developing applications, it's common to encounter scenarios where a user's request takes longer than expected. This delay can result from various factors like data transformation, external API calls, or slow database queries.

Fortunately, when constructing a Phoenix app that leverages the capabilities of LiveView, enhancing the user experience by incorporating spinners for such extended requests is a breeze.

The behavior I aim to illustrate is as follows:

  • The user encounters the "Load Data" button on the interface.
  • Upon clicking, we simulate an extended request by introducing a :timer.sleep(2000).
  • While the request loads the necessary data, a spinner becomes visible.

It might seem trivial, but from a user perspective, this serves as valuable feedback, helping them understand that the request might take a couple of seconds to complete.

⚠️ I will not cover the steps to start a new Phoenix LiveView project and setup aspects.

So let's start the coding.

The first step is to implement our LiveView module with the mount/3 function. This mount function will assign values to two variables in the socket posts, which will contain fake data for demonstration purposes, and loading, which will control the state of the loading spinner on the interface.

# lib/spinner_web/live/spinner_live.ex

defmodule SpinnerWeb.SpinnerLive do
  use SpinnerWeb, :live_view

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:posts, [])
      |> assign(:loading, false)

    {:ok, socket}
  end
end
Enter fullscreen mode Exit fullscreen mode

For rendering, I've created an html.heex template at the same directory level.

<!-- lib/spinner_web/live/spinner_live.html.heex -->

<div>
  <button
    phx-click="load-posts"
    class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:border-blue-300"
  >
    Load Posts
  </button>
</div>
<br /><br />
<%= if @loading do %>
  <svg
    class="animate-spin h-10 w-10 text-blue-500"
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
  >
    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
    </circle>
    <path
      class="opacity-75"
      fill="currentColor"
      d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 6.627 5.373 12 12 12v-4a7.946 7.946 0 01-6-2.709z"
    >
    </path>
  </svg>
<% end %>
Enter fullscreen mode Exit fullscreen mode

In the HTML, the crucial components are:

  • the button with a phx-click="load-posts" event assigned
  • the condition using the loading variable; this condition, along with the variable, will control when the spinner must appear.

Well, since I've set a phx-click="load-posts" event on the button, I need to implement that on the backend.

def handle_event("load-posts", _params, socket) do
  socket = assign(socket, :loading, true)

  send(self(), :load_posts)

  {:noreply, socket}
end
Enter fullscreen mode Exit fullscreen mode

Here is where the trick happens, In the first line of the function we set the loading assigned in the socket to be true and spawn a new message by using the function send/3. This function will receive an internal PID and a message, in this case :load_posts. Last but not least we return the {:noreply, socket} with assigns updated, in this case only the loading as true. At this point, the spinner will appear on the interface.

Since we are sending an internal message using send/3 in the handle_event/3 callback, we need to capture that in the LiveView module using a new callback, handle_info/2.

The new callback will primarily handle three steps:

  • Load the data we are expecting (in this case, a fake list of posts)
  • Set loading as false to remove the spinner from the interface
  • Return the new socket data with {:noreply, socket}
def handle_info(:load_posts, socket) do
    # Just to emulate the long-time request
    :timer.sleep(2000)

    socket =
      socket
      |> assign(:posts, get_posts())
      |> assign(:loading, false)

    {:noreply, socket}
  end

  defp get_posts do
    [
      %Spinner.Post{title: "First Title", description: "First Description"},
      %Spinner.Post{title: "Second Title", description: "Second Description"}
    ]
  end
Enter fullscreen mode Exit fullscreen mode

To simulate a case where the request takes a long time to run, I manually add a :timer.sleep/1 that takes 2 seconds to complete.

After these 2 seconds, the spinner will hide, and the list with loaded posts will appear on the interface. Just need to build the code for that on the HTML page.

<%= unless Enum.empty?(@posts) do %>
  <ul class="mt-4">
    <li :for={post <- @posts}>
      <h2 class="text-lg font-bold"><%= post.title %></h2>
      <p class="text-gray-600"><%= post.description %></p>
    </li>
  </ul>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Image description

Hope that was helpful!

The whole example can be found here: https://github.com/lcezermf/elixir-playground/tree/master/spinner

💖 💪 🙅 🚩
lcezermf
Luiz Cezer

Posted on December 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related