Flash message to disappear after 5 seconds?

Is there a way to have a flash message disappear (fade out) after a few seconds, please? Can’t find anything in docs nor on internets. Feels like this could be easy in LiveView, maybe I’m just missing the obvious. Thank you.

4 Likes

You can achieve that with something like this:

Send a message to self when you use put_flash

Process.send_after(self(), :clear_flash, 5000)

Then, in handle_info:

def handle_info(:clear_flash, socket) do
  {:noreply, clear_flash(socket)}
end
5 Likes

That will remove the flash immediately, right? I use some css to accomplish fading out though it is for a different case than flash messages.

@keyframes fade-out {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

.fade-out {
  animation-name: fade-out;
}

.animated {
  animation-duration: 1s;
  animation-fill-mode: both;
}
5 Likes

It appears to me, that the process that put the flash message is dead after it e.g. creates a record. Maybe the send needs to go to different process?

That’s true. If you program the sending to a pid, then push_redirect from the live component, the live view will be shut down and a new one created in its place, thus the old process won’t exist anymore. Changing to push_patch should work.

Also, forgot to mention that you also need to make sure the socket is connected before sending the message:

if connected?(socket), do: ...

@baldwindavid, it will just be removed from the DOM. You could chain together 2 events: 1st fade-out the element (add a css class), then remove it altogether. Worth mentioning that animations are best kept for Javascript, rather than Liveview. You could have a hook on the flash message that will remove itself in 5 seconds with animation effects and everything.

1 Like

Bingo! I’ve combined both of your solutions, and tweaked it a bit. I kept push_redirect rather than push_patch as I was “returning” to index and with push_patch my index of items was not updated.

My workaround is that I actually put the Process.send_after into my mount function, because that’s where I always get back to – so if there’s any new flash message displayed, it gets cleared after 5s. Not sure it’s super robust solution, but this way I need the clear function in one place only, and it works nicely.

And I applied the css animation to nicely fade out the flash, no need for javascript, nor desirable for this simple animation, I think.

6 Likes

Hey guys. This thread was very helpful. Many thanks. I see many other threads out there asking this question, and this one seems to be the closest to a clean solution.

I wanted to revisit this topic given more recent Phoenix & LiveView releases. Frankly, I am baffled that such a standard UI behavior expectation is still so hard to find a clean solution for in Phoenix/LiveView. If I am missing that solution, please point me to it.

I just created a fresh Phoenix app and ran mix phx.gen.auth to create a typical starting point to see how Flash messages currently behave. (FYI, it looks like my generator runs created a Phoenix 1.7.10 app running LiveView 0.19.5).

Alas, it appears that nothing has changed. Out of the box, Flash message behavior remains static. The Flash box appears, and it just stays there–until the user clicks the “x” in the box to dismiss it.

The (partial) Solution So Far

Adapting the advice in this thread history, I have been able to get the Flash box to go away after a specified number of milliseconds. In my example app, I have a (generated) LiveView scaffolding for an Alerts table and its LiveViews, so I have a MyApp_web/live/alert_live/index.ex and a MyApp_web/live/form_component.ex that are involved in making updates to Alert records via LiveView interactions.

My list of alerts is driven by this logic in MyApp_web/live/alert_live/index.ex:

defp apply_action(socket, :index, _params) do
    Process.send_after(self(), :clear_flash, 3000) #Clear the flash in 3 seconds
    socket
    |> assign(:page_title, "Listing Alerts")
    |> assign(:alert, socket.assigns.current_user.id)
  end

Because each CRUD action on an Alert gets us back here, I have added the Process.send_after(self(), :clear_flash, 3000) in this function as shown above.

That :clear_flash handle_event function is also in MyApp_web/live/alert_live/index.ex:

 @impl true
  def handle_info(:clear_flash, socket) do
    {:noreply, clear_flash(socket)}
  end

This works. I’m not (yet) using CSS to create a graceful fade-out; the Flash box disappears abruptly after 3 seconds. It is not slick, but it is a huge improvement.

But How Do We Generalize This?

I haven’t wired this up for all the other places CRUD activities take place (like User Settings, for example). This solution will require repeating this logic in all the places put_flash is invoked. To say the most, that is very not DRY.

I asked my AI intern for suggestions, and that advice included writing JavaScript to create a more generalized solution. That strikes me as antithetical to the LiveView vision–particularly for UI behavior users expect–and behavior that is common to most of the other web frameworks out there.

  • To put it bluntly, the out-of-the-box Flash behavior is half-baked/not ready for prime time.

We need to do better than this. I need a solution that is credible to SaaS users, and our community needs a solution that is credible to UI framework adopters.

I’m not just whining here; I am eager to do the work required to help create, document and publicize a generalized solution.

Thanks in advance for your help.

2 Likes

But How Do We Generalize This?

I haven’t wired this up for all the other places CRUD activities take place (like User Settings, for example). This solution will require repeating this logic in all the places put_flash is invoked. To say the most, that is very not DRY.

To keep it dry, maybe have a look at attach_hook/4 in conjunction with on_mount?

To give an example, I have something like this:

  def on_mount(:subscribe_to_runs, _params, _session, socket) do
     # ... 

    socket =
      socket
      |> attach_hook(
        :hide_flash,
        :handle_info,
        &hide_flash/2
      )

    {:cont, socket}
  end

  defp hide_flash(:hide_flash, socket) do
    {:halt, clear_flash(socket)}
  end

And this is mounted to live_session in router.ex

    live_session :app,
      on_mount: [
        {HanekawaWeb.SubscribeToRunsHook, :subscribe_to_runs}
      ] do
      live "/pipelines", PipelineListLiveView
      live "/pipelines/:id", PipelineLiveView
      live "/runs", RunListLiveView
       # ...
    end

Now the logic to hide the flash is in one place.

I do agree that more could be done for flash though!

2 Likes

This looks promising! I’ll give it a shot and report back.

HUGE thanks!

Hey @GumptionWare maybe you will be interested in my library which supports what you want to do and much more: Flashy - A small library to extend LV's flash notifications - #17 by sezaru

Thank @sezaru!! Our team will check that out tomorrow. It looks pretty complete.

Hey @sezaru, I am stuck on getting this error when I try to compile.

error: undefined function put_notification/2 (expected MinderyWeb.UserSessionController to define such a function or for it to be imported, but none are available)

I can see from your repo that this is defined in flashy.ex, but my import in app.js is apparently not finding it–or maybe my mix deps.get is not picking it up. (I have verified both multiple times).

I’m probably making a dumb mistake/subtle typo somewhere. Anything obvious jump out at you?

Did you went through the installation steps GitHub - sezaru/flashy: Flashy is a small library that extends LiveView's flash support to function and live components.? In this specific case it seems you are missing the import Flashy in your lib/<your_app>_web.ex file

UPDATE:

We resolved the undefined function put_notification issue. (We were missing the import Flashy in both places in MyAppWeb.ex.)

I think we are getting close. We are now figuring out how to change the icons displaying for each type.

1 Like

UPDATE:

We have Flashy functionality working, but we are figuring out why our configuration results in the Petal Alert components getting rendered for us much differently than they do in the flashy_demo project.

All that aside, many thanks for creating this. This is exactly the kind of thing I was saying we all need for our Phoenix projects.

1 Like

Not sure what is being rendered differently, but one thing that can affect it is what css you have applied in tags above it in the app.html.heex and root.html.heex.

If that is the case, you can always customize the Flashy.Container css to adapt to your needs.

Thank you @sezaru. As a Phoenix newbie, I’m struggling with adapting my Liveview tests for using Flashy. For example:

...
  refute get_session(conn, :user_token)
  assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
...

I’m getting ** (FunctionClauseError) no function clause matching in Kernel.=~/2 against that Phoenix.Flash.get(conn.assigns.flash, :info) =~ ... line, which makes sense, because our conn.assigns no longer has a flash.

What I cannot discern from the Flashy docs is how to retrieve the value from the put_notification that replaced the former call to put_flash.

So, the way flash mechanism work is that it is a key/value structure.

In other words, that Phoenix.Flash.get(conn.assigns.flash, :info) is searching for a flash with the key :info on it, internally, the function just does a simple Map.get .

This would work with the default flash that comes with phoenix since you create a flash giving it a key like :info, :error, etc and a string as a value.

That’s why you can’t have more than one flash message per key.

Flashy leverages the key/value to store strings as the key and a struct that implements the Flashy.Notification.Protocol as the value.

You can look how that works in this file https://github.com/sezaru/flashy/blob/master/lib/flashy.ex

If you want to test that, i guess you could mock the :erlang.unique_integer/1 function for the test, that way you would have control to how the key is generated during the test.

Or you can also just traverse the conn.assigns.flash map and find the notification there.

Thanks, I have abstracted those dynamic values you assign to key (flashy-N) for my testing. I will share some examples once I work through the variations in my current suite of tests.