Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

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

## [v0.10.0] (unreleased)

Note: this release includes breaking changes involving translation. The library no longer calls `gettext` functions on
dynamic strings passed to `put_toast` and `send_toast`.

### Changed

- [Translation behavior](https://github.com/srcrip/live_toast/pull/78): translation of LiveToast's built-in connection notices is now opt-in; configure `gettext_backend` to enable it. `send_toast/3`, `put_toast/4`, and custom connection-notification copy now render the supplied message and title unchanged, so translate application-owned copy before passing it to LiveToast.
- [TypeScript compatibility](https://github.com/srcrip/live_toast/pull/77): LiveToast no longer augments the global `HTMLElement` interface, and the package now ships and exposes its TypeScript declarations.

## [v0.1.0] (2024-03-06)

🚀 Initial release.
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,19 +316,23 @@ classes; use a custom component function for the icon, title, body, action, and

### Default custom toast component

Pass `toast_component_fn` to `LiveToast.toast_group` when all programmatic toasts in that host should use the same
custom component. A `component:` passed to an individual `send_toast/3` call takes precedence. This host default does
not change Phoenix flash or connection-notice rendering.
Pass `toast_component_fn` to `LiveToast.toast_group` when programmatic toasts and Phoenix flashes in that host should
use the same custom component. A `component:` passed to an individual `send_toast/3` call takes precedence for that
toast. Connection notices continue to use their default content or the corresponding slot.

```heex
<LiveToast.toast_group
flash={@flash}
connected={assigns[:socket] != nil}
toasts_sync={assigns[:toasts_sync]}
toast_component_fn={&MyAppWeb.Components.NotificationToast.render/1}
flash_group_id="phoenix-flashes"
/>
```

`flash_group_id` optionally adds an addressable container around only the Phoenix flashes. Programmatic toasts remain
outside that container.

```elixir
LiveToast.send_toast(:info, "The message was sent.", title: "Sent")
```
Expand Down
13 changes: 13 additions & 0 deletions demo/lib/demo_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ defmodule DemoWeb.Layouts do
]
end

attr(:kind, :atom, required: true)
attr(:title, :string, default: nil)
attr(:body, :any, required: true)

def demo_toast_component_fn(assigns) do
~H"""
<div data-demo-toast-component={@kind}>
<p :if={@title} class="text-sm font-semibold">{@title}</p>
<p class="text-sm">{@body}</p>
</div>
"""
end

attr(:notice, :map, required: true)

def connection_notice(assigns) do
Expand Down
3 changes: 3 additions & 0 deletions demo/lib/demo_web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<LiveToast.toast_group
toast_class_fn={&demo_toast_class_fn/1}
group_class_fn={&demo_group_class_fn/1}
toast_component_fn={
if assigns[:live_action] == :customization, do: &demo_toast_component_fn/1
}
flash={@flash}
kinds={[:info, :error, :warn]}
flash_duration={3000}
Expand Down
16 changes: 15 additions & 1 deletion demo/lib/demo_web/live/tabs/customization.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
A custom component renders the toast content inside the LiveToast shell. Use it to control the icon, title, body, actions, and application-specific markup while LiveToast continues to manage timing, animation, and dismissal. Pass it to an individual toast, or define
<code class="text-rose-600 bg-zinc-50 rounded px-1">toast_component_fn</code>
on <code class="text-rose-600 bg-zinc-50 rounded px-1">toast_group</code>
to use it for every programmatic toast in that host.
to use it for every programmatic toast and Phoenix flash in that host.
</p>
<pre class="overflow-x-auto rounded-md bg-zinc-950 p-4 text-sm text-zinc-100"><code>&lt;LiveToast.toast_group
flash=&#123;@flash&#125;
connected=&#123;assigns[:socket] != nil&#125;
toasts_sync=&#123;assigns[:toasts_sync]&#125;
toast_component_fn=&#123;&amp;notification/1&#125;
/&gt;</code></pre>
<p>
<.link
class="text-blue-700 hover:text-blue-500 underline"
href="https://github.com/srcrip/live_toast/blob/main/demo/lib/demo_web/components/layouts.ex"
>
View the demo component
</.link>
</p>
</div>
70 changes: 70 additions & 0 deletions demo/test/demo_web/live/send_toast_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,31 @@ defmodule DemoWeb.SendToastTest do
end
end

defmodule FlashComponentLive do
@moduledoc false

use Phoenix.LiveView

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

def handle_event("show_flash", _params, socket) do
{:noreply, Phoenix.LiveView.put_flash(socket, :info, "Rendered from a live flash.")}
end

def render(assigns) do
~H"""
<LiveToast.toast_group
flash={@flash}
toasts_sync={assigns[:toasts_sync]}
connected={assigns[:socket] != nil}
toast_component_fn={&DemoWeb.SendToastTest.flash_component/1}
flash_group_id="live-flashes"
/>
<button phx-click="show_flash">Show Flash</button>
"""
end
end

defmodule ClientToastLive do
@moduledoc false

Expand Down Expand Up @@ -262,6 +287,24 @@ defmodule DemoWeb.SendToastTest do
"""
end

defp dead_flash_component_host(assigns) do
~H"""
<LiveToast.toast_group
flash={@flash}
connected={false}
toasts_sync={[]}
toast_component_fn={&flash_component/1}
flash_group_id="dead-flashes"
/>
"""
end

def flash_component(assigns) do
~H"""
<p data-flash-renderer="custom" data-kind={@kind}>{@body}</p>
"""
end

describe "LiveToast.send_toast/3" do
test "renders info toast", %{conn: conn} do
{:ok, view, _html} = live_isolated(conn, TestLive)
Expand Down Expand Up @@ -344,6 +387,33 @@ defmodule DemoWeb.SendToastTest do
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)

assert html =~ ~s(id="live-flashes")
refute html =~ ~s(data-flash-renderer="custom")

view
|> element("button", "Show Flash")
|> render_click()

assert render(view) =~ "Rendered from a live flash."
assert has_element?(view, "#live-flashes [data-kind=info]")
end

test "uses the host component in a dead host" do
html =
render_component(&dead_flash_component_host/1,
flash: %{"error" => "Rendered from a dead flash."}
)

assert html =~ ~s(id="dead-flashes")
assert html =~ ~s(data-flash-renderer="custom")
assert html =~ "Rendered from a dead flash."
end
end

describe "client toast API" do
test "renders a client toast with supported options", %{conn: conn} do
{:ok, view, _html} = live_isolated(conn, ClientToastLive)
Expand Down
10 changes: 9 additions & 1 deletion lib/live_toast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,12 @@ defmodule LiveToast do

attr(:toast_component_fn, :any,
default: nil,
doc: "default component function used to render programmatic toasts"
doc: "default component function used to render programmatic toasts and Phoenix flashes"
)

attr(:flash_group_id, :string,
default: nil,
doc: "optional id for a container around Phoenix flashes"
)

attr :toasts_sync, :list, required: true, doc: "toasts that get synchronized when calling `put_toast`"
Expand Down Expand Up @@ -332,6 +337,7 @@ defmodule LiveToast do
client_error_delay={@client_error_delay}
toast_class_fn={@toast_class_fn}
toast_component_fn={@toast_component_fn}
flash_group_id={@flash_group_id}
group_class_fn={@group_class_fn}
connection_notifications={@connection_notifications}
client_error={@client_error}
Expand All @@ -345,7 +351,9 @@ defmodule LiveToast do
corner={@corner}
flash_duration={@flash_duration}
toast_class_fn={@toast_class_fn}
toast_component_fn={@toast_component_fn}
group_class_fn={@group_class_fn}
flash_group_id={@flash_group_id}
client_error_delay={@client_error_delay}
connection_notifications={@connection_notifications}
client_error={@client_error}
Expand Down
49 changes: 37 additions & 12 deletions lib/live_toast/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ defmodule LiveToast.Components do
doc: "function to override the toast classes"
)

attr(:toast_component_fn, :any,
default: nil,
doc: "component function used to render Phoenix flash content"
)

attr(:id, :string, default: nil, doc: "optional id for the flash-only container")

attr(:connection_notifications, :any,
default: %{},
doc: "false disables connection-state notifications; a map overrides their copy and kind"
Expand All @@ -150,18 +157,10 @@ defmodule LiveToast.Components do
assigns = assign(assigns, :connection_notifications, connection_notifications(assigns.connection_notifications))

~H"""
<.toast
:for={level <- @kinds}
data-component="flash"
corner={@corner}
toast_class_fn={@toast_class_fn}
flash_duration={@flash_duration}
duration={0}
kind={level}
title={String.capitalize(to_string(level))}
phx-update="ignore"
flash={@f}
/>
<div :if={@id} id={@id} style="display: contents">
<.flash_toasts {assigns} />
</div>
<.flash_toasts :if={!@id} {assigns} />
<%= if @connection_notifications do %>
<.toast
data-component="flash"
Expand Down Expand Up @@ -224,6 +223,23 @@ defmodule LiveToast.Components do
"""
end

defp flash_toasts(assigns) do
~H"""
<.toast
:for={level <- @kinds}
data-component="flash"
corner={@corner}
toast_class_fn={@toast_class_fn}
component={@toast_component_fn}
flash_duration={@flash_duration}
duration={0}
kind={level}
title={String.capitalize(to_string(level))}
flash={@f}
/>
"""
end

defp duration_value(:infinity), do: "Infinity"
defp duration_value(duration), do: duration

Expand All @@ -247,6 +263,13 @@ defmodule LiveToast.Components do
doc: "function to override the toast classes"
)

attr(:toast_component_fn, :any,
default: nil,
doc: "component function used to render Phoenix flash content"
)

attr(:flash_group_id, :string, default: nil, doc: "optional id for the flash-only container")

attr(:client_error_delay, :integer, default: 3000, doc: "adds a delay before the disconnected client error is shown")

attr(:flash_duration, :integer, default: 0, doc: "if provided clears flash after provided milliseconds")
Expand All @@ -269,6 +292,8 @@ defmodule LiveToast.Components do
corner={@corner}
flash_duration={@flash_duration}
toast_class_fn={@toast_class_fn}
toast_component_fn={@toast_component_fn}
id={@flash_group_id}
client_error_delay={@client_error_delay}
connection_notifications={@connection_notifications}
client_error={@client_error}
Expand Down
2 changes: 2 additions & 0 deletions lib/live_toast/live_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ defmodule LiveToast.LiveComponent do
corner={@corner}
flash_duration={@flash_duration}
toast_class_fn={@toast_class_fn}
toast_component_fn={@toast_component_fn}
id={@flash_group_id}
client_error_delay={@client_error_delay}
connection_notifications={@connection_notifications}
client_error={@client_error}
Expand Down
Loading