defmodule Toast do @moduledoc """ Server-rendered toast notifications for Phoenix LiveView. Toast provides a simple, server-controlled toast notification system for Phoenix LiveView applications. All state is managed on the server using LiveView streams, with a minimal JavaScript hook for animations and interactions. ## Installation Add `toast` to your dependencies in `mix.exs`: def deps do [ {:toast, "~> 0.0.2"} ] end ## Setup 1. Import the JavaScript hook in your `app.js`: import Toast from "../deps/toast/assets/js/toast.js"; let liveSocket = new LiveSocket("/live", Socket, { hooks: { Toast } }); 2. Import the CSS in your `app.css`: @import "../deps/toast/assets/css/toast.css"; 3. Add the toast container to your root layout: ## Basic Usage Send toasts from your LiveView: def handle_event("save", _params, socket) do Toast.send_toast(:success, "Changes saved!") {:noreply, socket} end ## Toast Types - `:info` - Blue informational messages - `:success` - Green success messages - `:error` - Red error messages - `:warning` - Yellow warning messages - `:loading` - Loading state with spinner - `:default` - Default neutral style ## Options Configure individual toasts with: Toast.send_toast(:success, "Message", title: "Success!", description: "Additional details", duration: 10_000, action: %{ label: "Undo", event: "undo_action" } ) See the documentation for `send_toast/3` for all available options. """ use Phoenix.Component defstruct [ :id, :type, :message, :title, :description, :duration, :action, :icon, :close_button, :position, :important ] @type t :: %__MODULE__{ id: String.t(), type: :info | :success | :error | :warning | :loading | :default, message: String.t(), title: String.t() | nil, description: String.t() | nil, duration: integer() | nil, action: map() | nil, icon: String.t() | nil, close_button: boolean(), position: atom() | nil, important: boolean() } @doc """ Renders the toast notification container. This is a convenience function component that wraps the Toast.LiveComponent. It can be used directly in your templates with the component syntax. ## Examples ## Attributes * `:id` - The ID of the toast group container (default: "toast-group") * `:flash` - The flash assigns from the socket * `:position` - Position of toasts: "top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right" (default: "bottom-right") * `:theme` - Theme: "light" or "dark" (default: "light") * `:rich_colors` - Whether to use rich colors for different toast types (default: false) * `:max_toasts` - Maximum number of toasts to display (default: 3) """ attr(:id, :string, default: "toast-group") attr(:flash, :map, required: false) attr(:position, :string, default: "bottom-right") attr(:theme, :string, default: "light") attr(:rich_colors, :boolean, default: false) attr(:max_toasts, :integer, default: 3) def toast_group(assigns) do ~H""" <.live_component module={Toast.LiveComponent} id={@id} flash={@flash} position={@position} theme={@theme} rich_colors={@rich_colors} max_toasts={@max_toasts} /> """ end @doc """ Sends a toast notification to the LiveComponent. ## Examples Toast.send_toast(:info, "Operation completed!") Toast.send_toast(:error, "Something went wrong", title: "Error", duration: 10_000) """ def send_toast(type, message, opts \\ []) do toast = build_toast(type, message, opts) Phoenix.LiveView.send_update(self(), Toast.LiveComponent, id: "toast-group", toast: toast ) end @doc """ Sends a toast notification and returns the socket for piping. This is a convenience function for use in pipelines. ## Examples socket |> assign(:user, user) |> put_toast(:success, "Profile updated!") |> push_navigate(to: ~p"/profile") """ def put_toast(socket, type, message, opts \\ []) do send_toast(type, message, opts) socket end @doc """ Updates an existing toast by ID. ## Examples Toast.update_toast("toast-123", message: "Updated message", type: :success) Toast.update_toast("toast-123", title: "New Title", duration: 10_000) """ def update_toast(toast_id, updates) do Phoenix.LiveView.send_update(self(), Toast.LiveComponent, id: "toast-group", update_toast: %{id: toast_id, updates: updates} ) end @doc """ Clears a specific toast by ID. """ def clear_toast(id) do Phoenix.LiveView.send_update(self(), Toast.LiveComponent, id: "toast-group", clear: id ) end @doc """ Clears all toasts. """ def clear_all_toasts() do Phoenix.LiveView.send_update(self(), Toast.LiveComponent, id: "toast-group", clear_all: true ) end defp build_toast(type, message, opts) do %__MODULE__{ id: opts[:id] || generate_id(), type: type, message: message, title: opts[:title], description: opts[:description], duration: opts[:duration] || 6000, action: opts[:action], icon: opts[:icon], close_button: Keyword.get(opts, :close_button, true), position: opts[:position], important: Keyword.get(opts, :important, false) } end defp generate_id do "toast-#{:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)}" end end