Skip to content

Instantly share code, notes, and snippets.

@conradfr
Last active May 21, 2023 15:28
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/a2f072edfb889bfd169bed22421e2ac6 to your computer and use it in GitHub Desktop.
Save conradfr/a2f072edfb889bfd169bed22421e2ac6 to your computer and use it in GitHub Desktop.
LiveView Boostrap v5 toast component
One possible implementation of Bootstrap toasts.
Using a Live Component.
Note:
If you ask for a toast to show and navigate to another LiveView the component will be unmounted and your toast will not be displayed.
For a solution using a separate LiveView check: https://gist.github.com/conradfr/50c250ea5fddcf12979f6ca5282a3af1
defmodule BsToastComponent do
use MyAppWeb, :live_component
alias Phoenix.LiveView.JS
def render(assigns) do
~H"""
<div
id={@id}
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(socket) do
{:ok,
socket
|> stream(:toasts, [])}
end
@impl true
def update(assigns, socket) when is_map_key(assigns, :toast) == false do
{:ok, assign(socket, :id, assigns.id)}
end
@impl true
def update(assigns, socket) do
with %Toast{} = toast <- Map.get(assigns, :toast) do
{:ok,
socket
|> stream_insert(:toasts, toast)
|> push_event("show_toast", %{id: "toasts-" <> toast.id})}
else
_ -> {:ok, socket}
end
end
@impl true
def handle_event("toast_closed", %{"id" => id} = _params, socket) do
{:noreply, stream_delete_by_dom_id(socket, :toasts, id)}
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;
toast = Toast.new("Great success !")
send_update(BsToastComponent, id: "bs-toast", toast: toast)
toast = Toast.new("Still great success !", :success)
send_update(BsToastComponent, id: "bs-toast", toast: toast)
toast = Toast.new("An error occurred", :error)
send_update(BsToastComponent, id: "bs-toast", toast: toast)
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