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.
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
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;
}
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.
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.
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.
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!
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
.
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.
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.
Here is a solution using hooks. It hides info flash messages after 10 seconds and leaves error flash messages open until the user closes them.
Create a file called hooks.js for your hooks.
// assets/js/hooks.js
let Hooks = {}
Hooks.HideFlash = {
mounted() {
setTimeout(() => {
this.pushEvent("lv:clear-flash", { key: this.el.dataset.key })
this.el.style.display = "none"
}, 10000)
}
}
export default Hooks
Don’t forget to add your hooks to your liveview socket.
// assets/js/app.js
// ...
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: Hooks,
})
// ...
Add the hook to the flash component.
# core_components.ex
def flash(assigns) do
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-hook={@kind == :info && "HideFlash"}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"fixed top-2 right-2 mx-2 min-w-80 max-w-xl 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"
]}
{@rest}
>
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
<%= @title %>
</p>
<pre class="mt-2 text-sm leading-5 whitespace-pre-wrap"><%= msg %></pre>
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
"""
end