From 80f5aa7c0b5284c60e236e6e1f547bf3e5281d7c Mon Sep 17 00:00:00 2001 From: Churchill Aboge Date: Fri, 11 Sep 2026 22:41:26 +0300 Subject: [PATCH] fix: keep the flash map intact in the put_toast sync path --- CHANGELOG.md | 6 ++ demo/test/demo_web/live/send_toast_test.exs | 73 +++++++++++++++++++++ lib/live_toast/live_component.ex | 2 +- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b950db0..78cf6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/demo/test/demo_web/live/send_toast_test.exs b/demo/test/demo_web/live/send_toast_test.exs index 09b53d8..4b694c9 100644 --- a/demo/test/demo_web/live/send_toast_test.exs +++ b/demo/test/demo_web/live/send_toast_test.exs @@ -4,6 +4,9 @@ defmodule DemoWeb.SendToastTest do import Phoenix.LiveViewTest + alias Phoenix.LiveView.Lifecycle + alias Phoenix.LiveView.Socket + defmodule TestLive do @moduledoc false @@ -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""" + + """ + end + end + defmodule CenteredPositionLive do @moduledoc false @@ -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) diff --git a/lib/live_toast/live_component.ex b/lib/live_toast/live_component.ex index 171f12c..963e3f2 100644 --- a/lib/live_toast/live_component.ex +++ b/lib/live_toast/live_component.ex @@ -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