LiveView: how to create a loading state during form submission?

I’m creating a domain name search form. It could take up to 10 seconds after hitting “search” to get a result. I want to display a “loading” animation in the meantime, something like this:

<section class="textual">
  <div class="wrap">

    <form method="post" phx-submit="search">
      <input type="text" name="q" placeholder="mytribe.com" />
      <button class="submit">Search</button>
    </form>
    
    <%= if @loading do %>
      LOADING . . .
    <% end %>
    
    <pre>
      <%= inspect(@results, pretty: true) %>
    </pre>
  </div>
</section>

What I can’t seem to figure out is how to set the “loading” state while handling the event…

  @impl true
  def handle_event("search", %{"q" => q}, socket) do
    push_event(socket, "set_loading", true) # This does nothing, pretty sure I've misunderstood the purpose of this function

    with {:ok, %{body: %{"data" => data}}} <- Epik.check_domains([q]) do
      {:noreply, assign(socket, :results, data)}
    end
  end

Right when the “search” event is starting to be handled, I want my template to have the loading animation. Then ~10s later when I get the result, I want to display the result.

I really don’t understand how LiveView/GenServer works on a fundamental level yet, but I’m starting to think maybe this isn’t possible.

1 Like

You need to decouple a little bit… first set loading status, then run the query.

def handle_event("search", %{"q" => q}, socket) do
  send(self, {:run_search, q})
  {:noreply, assign(socket, :set_loading, true)}
end

def handle_info({:run_search, q}, socket) do
  # do the actual work
  socket
  |> assign(:set_loading, false)
  |> assign(:result, ...)
  {:noreply, socket}
end
8 Likes

Thank you SO MUCH. This worked. You’re a champion!

I’m currently using this approach for a search form in my app. I’m trying to extract my search form into a LiveComponent.

I moved the handle_event/3 function into the new LiveComponent:

def handle_event("search", %{"q" => q}, socket) do
  send(self, {:update_loading, q})
  {:noreply, socket}
end

And updated the form to target={@myself}.

The following sends a message to the parent view:

send(self, {:update_loading, q})

I can keep the handle_info/2 in the parent view, but I think the work should happen within the LiveComponent. Plus I need to include two separate handle_info/2 functions because the parent view needs the loading state.

def handle_info({:update_loading, q}, socket) do
  send(self, {run_search, q})
  socket
  |> assign(:set_loading, true)
  {:noreply, socket}

def handle_info({:run_search, q}, socket) do
  # do the actual work
  socket
  |> assign(:set_loading, false)
  |> assign(:result, ...)
  {:noreply, socket}

I tried using send_update/3 as recommended in the Managing State section of the docs.

It would look something like:

def handle_info({:update_loading, q}, socket) do
  send_update SearchComponent, id: :new, query: q
  socket
  |> assign(:set_loading, true)
  {:noreply, socket}
end

But then I think I need to “do the work” in the update callback, which seems strange. I could begin with query = nil and then do the work there if query is not nil.

Any suggestions on extracting the search form into a LiveComponent?

I would not do the business logic in the component, I would make it dumb, and pass search query to the parent.

I see the parent as the manager of the state. It’s just my way of doing it

2 Likes