Live view client side redirect via a form

I have a form that has a phx-submit and in the submit handler it calls push_redirect. This works but the problem is all this handler does is go back to the client to tell it to do a redirect which triggers topbar (some might use NProgress instead) showing twice.

E.g. click form submit button → topbar loading while submit handler is triggered → client receives response to redirect → topbar loading again while redirecting.

It’s not a major issue but feels a little janky from a ux perspective. Is there alternative approach or a way to avoid this?

Can you share the code for handle_event and maybe also the topbar template so we can see what’s going on?

Here’s my code which is a live component.

defmodule MyAppWeb.SearchFormLive do
  use MyAppWeb, :live_component

  def mount(socket) do
    categories =
      MyApp.Categories.list_categories()
      |> Enum.map(fn category ->
        {category.name, category.slug}
      end)

    {:ok, assign(socket, categories: categories, temporary_assigns: [caregories: []])}
  end

  def render(assigns) do
    ~L"""
    <form phx-submit="search" phx-target="<%= @myself %>" phx-hook="SearchSelect">
      <input type="hidden" name="url" />
      <select id="category" name="category">
        <option disabled selected value>Choose a category</option>
        <%= options_for_select(@categories, "") %>
      </select>

      <button type="submit">
        Search
      </button>
    </form>
    """
  end

  def handle_event("search", %{"category" => category, "url" => url}, socket) do
    case String.starts_with?(url, "/search") do
      true ->
        socket = push_patch(socket, to: Routes.search_path(socket, :category, category))
        {:noreply, socket}

      false ->
        socket = push_redirect(socket, to: Routes.search_path(socket, :category, category))
        {:noreply, socket}
    end
  end
end

And here’s the topbar setup which is the default for live view:

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

The other approach I thought of was to not use a form and to instead just have a link that I manually add the required phx attributes to make it live redirect. Then use a live view hook to update the href based on the selected option i.e.

// live view component render function

def render(assigns) do
    <div phx-hook="Find">
      <select id="category" name="category">
        <option disabled selected value>Choose a category</option>
        <%= options_for_select(@categories, "") %>
      </select>

      <a href="/search" data-phx-link="redirect" data-phx-link-state="push">
        <button>Find</button>
      </a>
    </div>
end

// live view hook
Hooks.Find = {
  mounted() {
    const selectEl = this.el.querySelector('select')
    const aEl = this.el.querySelector('a')

    selectEl.addEventListener('change', e => {
      aEl.href = `/search/${e.target.value}`
    })
  }
}

Not sure if this is relevant to your original issue or not, but I think

{:ok, assign(socket, categories: categories, temporary_assigns: [caregories: []])}

should be

{:ok,
  assign(socket, categories: categories),
  temporary_assigns: [categories: []]
}

i.e. the temporary assigns as third element of the :ok tuple, as opposed to a third argument to assign/2

For reference: DOM patching and temporary assigns

oh right good pick up, thanks! Unfortunately thats not related to the issue.

You can hack something to disallow topbar to be triggered by a specific DOM element, something like this in app.js can work.

// Show progress bar on live navigation and form submits (except the chat one)
window.addEventListener("phx:page-loading-start", info => {
    if(info.detail.kind != "initial" && info.detail.kind != "error") {
        if(info.detail.target.id != "chat-input") {
            topbar.show()
        }
    } else {
        topbar.show()
    }
})