Spice up your LiveView app with a cool loading spinner!
Luiz Cezer
Posted on December 15, 2023
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
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 %>
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
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
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 %>
Hope that was helpful!
The whole example can be found here: https://github.com/lcezermf/elixir-playground/tree/master/spinner
Posted on December 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.