Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

Note that the versions prior to `0.6.0` are very unstable.

## [Unreleased]

### Fixed

- `put_toast/4` no longer replaces the flash map with the whole assigns map when the synced flash is still present, which hid every other flash.

## [v0.11.0] (2026-09-09)

### Changed
Expand Down
73 changes: 73 additions & 0 deletions demo/test/demo_web/live/send_toast_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ defmodule DemoWeb.SendToastTest do

import Phoenix.LiveViewTest

alias Phoenix.LiveView.Lifecycle
alias Phoenix.LiveView.Socket

defmodule TestLive do
@moduledoc false

Expand Down Expand Up @@ -84,6 +87,33 @@ defmodule DemoWeb.SendToastTest do
end
end

defmodule SyncToastLive do
@moduledoc false

use Phoenix.LiveView

def mount(_params, _session, socket), do: {:ok, socket}

def handle_event("save", _params, socket) do
{:noreply, LiveToast.put_toast(socket, :success, "Saved", uuid: "saved")}
end

def handle_event("show_error", _params, socket) do
{:noreply, Phoenix.LiveView.put_flash(socket, :error, "Check the form")}
end

def render(assigns) do
~H"""
<LiveToast.toast_group
flash={@flash}
toasts_sync={assigns[:toasts_sync]}
connected={true}
kinds={[:success, :error]}
/>
"""
end
end

defmodule CenteredPositionLive do
@moduledoc false

Expand Down Expand Up @@ -392,6 +422,49 @@ defmodule DemoWeb.SendToastTest do
end
end

describe "LiveToast.put_toast/4" do
test "renders another flash while the synced flash awaits client clearing", %{conn: conn} do
{:ok, view, _html} = live_isolated(conn, SyncToastLive)

render_click(view, "save")

assert has_element?(view, "#toast-saved", "Saved")
refute has_element?(view, "#flash-success")
assert_push_event(view, "clear-flash", %{key: :success})
refute_push_event(view, "clear-flash", %{key: :success})

# Keep the success flash: do not send the hook's lv:clear-flash event.
render_click(view, "show_error")

assert has_element?(view, "#flash-error", "Check the form")
assert has_element?(view, "#toast-saved", "Saved")
refute has_element?(view, "#flash-success")
assert_push_event(view, "clear-flash", %{key: :success})
refute_push_event(view, "clear-flash", %{key: :success})
end

test "keeps the component flash map flat when removing a synced flash" do
parent_socket =
%Socket{assigns: %{__changed__: %{}, flash: %{}}}
|> LiveToast.put_toast(:success, "Saved")

{:ok, socket} =
LiveToast.LiveComponent.mount(%Socket{private: %{live_temp: %{}, lifecycle: %Lifecycle{}}})

assigns = %{
id: "toast-group",
f: parent_socket.assigns.flash,
toasts_sync: parent_socket.assigns.toasts_sync
}

{:ok, socket} = LiveToast.LiveComponent.update(assigns, socket)
assigns = put_in(assigns.f["error"], "Check the form")
{:ok, socket} = LiveToast.LiveComponent.update(assigns, socket)

assert socket.assigns.f == %{"success" => nil, "error" => "Check the form"}
end
end

describe "Phoenix flashes" do
test "uses the host component in a live host", %{conn: conn} do
{:ok, view, html} = live_isolated(conn, FlashComponentLive)
Expand Down
2 changes: 1 addition & 1 deletion lib/live_toast/live_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ defmodule LiveToast.LiveComponent do
toasts = toasts || []
toasts = [sync_toast | toasts]

new_f = put_in(assigns[:f][sync_toast_kind], nil)
new_f = Map.put(assigns[:f], sync_toast_kind, nil)
assigns = Map.put(assigns, :f, new_f)

socket
Expand Down