rhcarvalho

rhcarvalho

Delaying LiveView.JS commands (avoid quick flash of "Trying to reconnect")

Hello! There are moments in the lifecycle of a LiveView app that the client disconnects and briefly reconnects (e.g.: tab woke up from sleep, or app was redeployed causing a quick socket reconnection).

In those cases, there’s a very fast “flash” of the flash message (by default a red-colored flash message) that disappears before it could ever be read.

So I’m looking into ways to avoid this brief appearance of the disconnected state. Instead, I would like to wait for say 2 seconds and if we’re still offline then show the flash message.

This is the standard flash_group implementation in core components in a new app:

  def flash_group(assigns) do
    ~H"""
    <div id={@id}>
      <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
      <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
      <.flash
        id="client-error"
        kind={:error}
        title={gettext("We can't find the internet")}
        phx-disconnected={show(".phx-client-error #client-error")}
        phx-connected={hide("#client-error")}
        hidden
      >
        <%= gettext("Attempting to reconnect") %>
        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
      </.flash>

      <.flash
        id="server-error"
        kind={:error}
        title={gettext("Something went wrong!")}
        phx-disconnected={show(".phx-server-error #server-error")}
        phx-connected={hide("#server-error")}
        hidden
      >
        <%= gettext("Hang in there while we get back on track") %>
        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
      </.flash>
    </div>
    """
  end

The show and hide helpers call JS.show and JS.hide.

Is there a way to delay a call to JS.show and perhaps cancel it if phx-connected fires in the meantime?

Do I need to resort to a Hook for greater client-side control of this behavior?

Marked As Solved

rhcarvalho

rhcarvalho

I found a simpler solution that seems to work as intended. It consists of simply adding animation-delay to the appropriate element permanently and unrelated to the transition classes used in show/2, no JavaScript involved.

  1. Update the CoreComponents.flash/1 component to take extra classes:
   attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
+
+  attr :extra_classes, :string,
+    default: nil,
+    doc: "the extra CSS classes to add to the flash container"
+
   attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
       class={[
         "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
         @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
-        @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
+        @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900",
+        @extra_classes
       ]}
       {@rest}
     >
  1. Update only the client-error flash in CoreComponents.flash_group/1:
         id="client-error"
         kind={:error}
         title="We can't find the internet"
+        extra_classes="delay-[3s]"
         phx-disconnected={show(".phx-client-error #client-error")}
         phx-connected={hide("#client-error")}
         hidden
12
Post #5

Also Liked

mjrusso

mjrusso

There’s a related issue with using LiveView apps on mobile that these solutions don’t address.

Scenario:

  • LiveView-based web app is loaded in mobile browser (or embedded web view in a mobile app)
  • User backgrounds the browser or mobile app
  • After some time (depends on OS and a number of factors, but say at least a minute or so), user returns to browser or app

Assuming that the OS hasn’t unloaded the web page entirely, what you’ll usually see is a quick flash of “trying to reconnect”. The delay doesn’t help (it actually makes it worse because it also delays the hide); I think what’s happening is that the browser is killing the tab’s networking, which causes the “trying to reconnect” flash to render (while the tab isn’t visible to the user), and when you come back later, it’s already on screen.

I think the Page Visibility API can help here. LiveView already has bindings for blur and focus; it’s reasonable to consider including built-in bindings for Document.visibilityState (and changing the logic to only show the flash if the page is visible, and the socket is disconnected).

FWIW, my solution for now is much less complicated: simply make this particular flash look different than the other flashes (just a loading spinner, with “Connecting…” text), and neutral colours that don’t suggest any sort of error:

# A custom flash notice for communicating that the client has disconnected.
def flash(%{id: "client-error"} = assigns) do
  ~H"""
  <div
    id={@id}
    role="alert"
    class="fixed top-2 right-2 z-50 mt-1 mr-2 w-fit rounded-md bg-slate-300 p-3 text-slate-900 shadow-md"
    {@rest}
  >
    <p class="flex items-center gap-1.5 text-sm leading-6">
      <span class="font-semibold">
        <.icon name="hero-arrow-path" class="mr-2 h-5 w-5 animate-spin" />
        <%= gettext("Connecting...") %>
      </span>
    </p>
  </div>
  """
steffend

steffend

Phoenix Core Team

I’d try using JS.dispatch Phoenix.LiveView.JS — Phoenix LiveView v1.2.5 to send a custom event that then uses liveSocket.execJS.

This is pseudo code, I didn’t test it, but I hope that it shows the idea:

<.flash
  id="client-error"
  kind={:error}
  title={gettext("We can't find the internet")}
  phx-disconnected={JS.dispatch("delayed-exec", detail: %{id: "client-error", target: "data-disconnected"})}
  phx-connected={JS.dispatch("clear-delayed-exec", detail: %{id: "client-error"}) |> JS.exec("data-connected")}
  data-disconnected={show(".phx-client-error #client-error")}
  data-connected={hide("#client-error")}
  hidden
>
  <%= gettext("Attempting to reconnect") %>
  <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>

And in your app.js something like:

const timeouts = {};
window.addEventListener("delayed-exec", (e) => {
  clearTimeout(timeouts[e.detail.id]);
  timeouts[e.detail.id] = setTimeout(() => {
    liveSocket.execJS(e.target, e.target.getAttribute(e.detail.target));
  }, e.detail.timeout);
});

window.addEventListener("clear-delayed-exec", (e) => {
  clearTimeout(timeouts[e.detail.id]);
});
chrismccord

chrismccord

Creator of Phoenix

<.flash delay> with <div class={["...", @delay && "delay-2s"]} with attr :delay, :boolean, default: false gets my vote

Last Post!

rhcarvalho

rhcarvalho

IIRC the approaches I suggested (and use) all come with small tradeoffs.

I know nowadays JS commands that used to be blocking can be made non-blocking and I didn’t reconsider how that would affect the possible solutions.

There were also ideas on how to make the concept of delays more general.

I think this topic would benefit from a fresh pair of eyes :eyes:

Sorry I don’t have anything more concrete to share at the moment.

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement