Skip to content

Instantly share code, notes, and snippets.

@conradfr
Last active June 7, 2023 13:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save conradfr/50c250ea5fddcf12979f6ca5282a3af1 to your computer and use it in GitHub Desktop.
Save conradfr/50c250ea5fddcf12979f6ca5282a3af1 to your computer and use it in GitHub Desktop.
LiveView Boostrap v5 toast LiveView
One possible implementation of Bootstrap toasts:
Against just making it a Live Component (see gist: https://gist.github.com/conradfr/a2f072edfb889bfd169bed22421e2ac6)
- Pros : You can ask for a toast to show and navigate to another LiveView, the toast is then able to be displayed without being unmouted by the navigation and therefore not showing
- Cons: Makes it harder to communicate between the LiveView and your toast container.
1/ The toast container is a separate LiveView from your "main" LiveView
2/ A unique id (page_id) is generated by the javascript and transmitted to all LiveViews.
3/ The Toast LiveView register itself to a registry using page_id
4/ The regular LiveView uses page_id and the registry to send a message to the Toast LiveView.
For how to use, check:
- example_from_another_hook.js
- example.ex
const Hooks = {};
Hooks.BsToast = BsToastHook;
let liveSocket = new LiveSocket('/live', Socket, {
hooks: Hooks,
params: {
page_id: Math?.floor(Math.random() * 100000000000000000000)
}
});
# add to children in start/2
{Registry, [keys: :unique, name: MyAppApiRegistry]}
defmodule MyAppWeb.BsToastLive do
use MyAppWeb, :live_view
@status [:success, :error]
@impl true
def render(assigns) do
~H"""
<div
id="bs-toast-container"
phx-hook="BsToast"
phx-update="stream"
class="toast-container position-absolute end-0 p-3"
>
<div
:for={{id, toast} <- @streams.toasts}
id={id}
role="alert"
aria-live="assertive"
aria-atomic="true"
class={"toast text-white #{if toast.type == :success, do: "bg-success"}#{if toast.type == :error, do: "bg-warning"}"}
>
<div class="d-flex justify-content-center align-items-center p-3">
<div class="toast-icon">
<i class={"bi #{if toast.type == :success, do: "bi-check-circle-fill"}#{if toast.type == :error, do: "bi-x-circle-fill"}"}>
</i>
</div>
<div class="toast-body flex-fill py-0 text-center align-middle">
<%= toast.message %>
</div>
</div>
</div>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
process_name = get_toast_process_name(socket)
Registry.register(MyAppRegistry, process_name, :toast)
end
{:ok,
socket
|> stream(:toasts, []), layout: false}
end
@impl true
def handle_info({:display_toast, message, status}, socket) when is_binary(message) and status in @status do
toast = Toast.new(message, status)
{:noreply,
socket
|> stream_insert(:toasts, toast)
|> push_event("show_toast", %{id: "toasts-" <> toast.id})}
end
@impl true
def handle_info({:display_toast, _message, _status}, socket) do
{:noreply, socket}
end
@impl true
def handle_event("toast_closed", %{"id" => id} = _params, socket) do
{:noreply, stream_delete_by_dom_id(socket, :toasts, id)}
end
# we get the unique id from the app.js so both liveviews have the same
defp get_toast_process_name(socket) do
"bs_toast_" <> get_page_id_from_socket(socket)
end
def get_page_id_from_socket(socket) do
with %{} = params <- Phoenix.LiveView.get_connect_params(socket) do
params
|> Map.get("page_id")
|> Integer.to_string()
else
_ -> nil
end
end
end
const TOAST_DURATION = 3500;
const BsToastHook = {
mounted() {
this.handleEvent('show_toast', ({ id }) => {
const toastElem = document.getElementById(id);
if (toastElem) {
const toast = new bootstrap.Toast(toastElem, {delay: TOAST_DURATION});
toast.show();
setTimeout(
() => {
toast.hide();
this.pushEventTo(this.el, 'toast_closed', { id });
},
TOAST_DURATION + 500
);
}
});
}
};
export default BsToastHook;
defmodule MyAppWeb.ExampleLive do
use MyAppWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div>
<button phx-click="send_toast">Send toast</button>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(
page_id: get_page_id_from_socket(socket)
)
end
@impl true
def handle_event("send_toast", _params, socket) do
with pid when pid != nil <- get_pid_of_toast_lv(socket.assigns.page_id) do
Process.send(pid, {:display_toast, "Great Toast !", :success}, [])
else
_ -> :error
end
{:noreply, socket}
end
def get_pid_of_toast_lv(id) do
case Registry.lookup(MyAppRegistry, "bs_toast_" <> id) do
[] -> nil
[{pid, _}] -> pid
end
end
end
const AnotherHook = {
mounted() {
this.pushEventTo('#bs-toast-container', 'display_toast', { status: 'success', message: 'I love toast!' });
}
}
<%= live_render(@conn, MyAppWeb.BsToastLive) %>
defmodule Toast do
use Ecto.Schema
@primary_key false
embedded_schema do
field(:id, :string)
field(:message, :string)
field(:type, Ecto.Enum, values: [:success, :error])
end
def new(message, type \\ :success) when is_binary(message) do
%Toast{
id: System.unique_integer([:positive]) |> Integer.to_string(),
message: message,
type: type
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment