If you program using Phoenix and LiveViews you eventually have the sad realization
that Phoenix.LiveView.put_flash(...) doesn't have any effect when used from LiveComponent.
We can bypass this limitation by realizing that LiveViews are all just elixir processes,
GenServers and functions.
Because of that we can send a message to root LiveView and then update socket.assigns in it.
We can do that almost transparently, as LiveViews have a thing called server side hooks.
Using hooks we can add it once and have that functionality available in all LiveViews.
To start, let's create a new module for our new function that will deal with passing flashes where they should end up in:
defmodule MyAppWeb.Flash do
alias Phoenix.LiveView
def put_flash!(socket, kind, msg) do
send(socket.root_pid, {:put_flash, kind, msg})
socket
end
end
This will send a message even when called directly in root LiveView.
LiveComponent has the same pid as root_pid, so there's no real way to figure out if put_flash! got called
from LiveComponent or LiveView.
It shouldn't ever be much of a problem realistically, as flashes are mostly put after human interaction,
which are pretty slow from a computer perspective.
Now we need a hook that will listen to these messages and update socket's assigns:
defmodule MyAppWeb.LiveHook.PutFlash do
use MyAppWeb, :live_view
def on_mount(:default, _params, _session, socket) do
{:cont, attach_hook(socket, :put_flash, :handle_info, &handle_info/2)}
end
def handle_info({:put_flash, kind, msg}, socket) do
{:halt, put_flash(socket, kind, msg)}
end
def handle_info(_msg, socket), do: {:cont, socket}
end
Then we have to attach that hook to LiveViews.
To do that, we have to add it in MyAppWeb.Router when we define live route:
live_session :put_flash_live_session,
on_mount: [MyAppWeb.LiveHook.PutFlash] do
# Routes...
end
After that, for simplicity, we may as well import put_flash!(...) in LiveViews and LiveComponents.
We have to edit macros in MyAppWeb edit macro for :live_component and :live_view:
defmacro live_component do
quote do
# ...
import MyAppWeb.Flash, only: [put_flash!: 3]
end
end
defmacro live_view do
quote do
# ...
import MyAppWeb.Flash, only: [put_flash!: 3]
end
end
Reducing amount of messages sent
If, for some reason, we want/need to avoid sending messages from root LiveView to itself when not needed,
we can just use Phoenix.LiveView.put_flash(...) directly in LiveViews.
But that only works if we know whether they are/will be nested at some point.
But maybe we don't know that, or we want one function.
To deal with this case, we can change the Flash module's function to something like this:
# For LiveComponents
def put_flash!(kind, msg) do
send(self(), {:put_flash, kind, msg})
end
# For root LiveView
def put_flash!(socket, kind, msg) when socket.root_pid == self() do
LiveView.put_flash(socket, kind, msg)
end
# For nested LiveViews
def put_flash!(socket, kind, msg) do
send(socket.root_pid, {:put_flash, kind, msg})
socket
end
And modify MyAppWeb's macros accordingly.
With this, we lose one function signature for everything,
but no message is sent when using put_flash! directly in root LiveView.
Also, LiveComponent doesn't have to (well, it can't) pass socket anymore.
This is a bit over the top, as sending these messages shouldn't really be a problem.
Only real benefit may be if we really don't want to pass socket to put_flash! in LiveComponents
if it would simplify application code.