Skip to content

Repository files navigation

Live Toast

tests badgetests badge

Live Toast is a drop-in replacement for the flash system in Phoenix/LiveView.

Features

  • 📄 Stackable toast messages: The flash system limits you to one flash per type. No longer!
  • 📸 Replaces your flash messages: One drop in component to continue to render your flash messages in the same style as the newer toast system.
  • 💅 Beautiful by design: Based on the look of the wonderful Sonner library from React.
  • ⚙️ Highly configurable: Looks good out of the box, but can be changed in pretty much any way you want.
  • 🌍 Simple asset delivery:LiveToast simply ships Tailwind classes and lets your project bundle them up. No CSS drop-in required.

Installation

Add live_toast to your list of dependencies in the mix.exs of your Phoenix app:

defdepsdo[{:live_toast,"~> 0.10.0"}]end

Next open up your app.js and import/setup the hook.

If you have a package.json file at the top of assets, you can add this to it:

"dependencies": {
"live_toast": "file:../deps/live_toast",
},

And then import and set up the bare module:

import{createLiveToastHook}from'live_toast'letliveSocket=newLiveSocket('/live',Socket,{hooks: {LiveToast: createLiveToastHook()}})

Or you can import the file directly:

// this path would be relative to where your app.js happens to be.import{createLiveToastHook}from'../deps/live_toast'letliveSocket=newLiveSocket('/live',Socket,{hooks: {LiveToast: createLiveToastHook()}})

Then, add '../deps/live_toast/lib/**/*.*ex' to your list of paths Tailwind will look for class names, in your tailwind.config.js:

// assets/tailwind.config.jsmodule.exports={content: ['./js/**/*.js','../lib/your_app_web.ex','../lib/your_app_web/**/*.*ex','../deps/live_toast/lib/**/*.*ex',]}

Your particular file will look different but all you need to do is make sure the last line is there.

Note for Umbrella Apps: If you're using an umbrella application, your paths above may look different. You'll probably have an extra folder in there, so the line you need to add would be more like "../../../deps/live_toast/lib/**/*.*ex"

Finally, replace your <.flash_group /> component with the new <LiveToast.toast_group />. It's most likely in your app.html.heex:

<!-- Remove this! --><.flash_groupflash={@flash}/><!-- And replace it with this: --><LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}toasts_sync={assigns[:toasts_sync]}/><%=@inner_content %>

Those three options, flash, connected, and toasts_sync are all required. The library will not function properly if you do not pass them in.

Note: As far as I can tell in my testing, this usage of assigns in the layout has no negative impact on change tracking.

And you're done! Note that it's very important to set connected based on whether we're in a LiveView or not. This controls toast/flash display on non-LiveView pages.

Usage

LiveToast will hijack the usual display of your flash messages, so they will continue to work as normal. You can continue to use flashes as normal, if you want to.

However, one of the reasons to not use flash messages, is the Phoenix flash system only allows one message for each kind of flash. The toast pattern, alternatively, generally allows for multiple messages displayed to the user at at time.

From a LiveView, you can now use send_toast:

Note: Please reference the Configuration section below for the available options.

defmoduleYourApp.SomeLiveViewdodefhandle_event("submit",_payload,socket)dooptions=[title: "Status"]# you do some thing with the payload, then you want to show a toast, so:LiveToast.send_toast(:info,"Upload successful.",options){:noreply,socket}endend

Note:LiveToast is the top-level module, so there's no need to alias or import anything.

send_toast/3 returns the toast UUID. You can use that UUID to dismiss the toast later from the server with dismiss_toast:

defmoduleYourApp.SomeLiveViewdodefhandle_event("start_upload",_payload,socket)douuid=LiveToast.send_toast(:info,"Upload started.",duration: 0){:noreply,assign(socket,upload_toast_uuid: uuid)}enddefhandle_event("finish_upload",_payload,socket)doLiveToast.dismiss_toast(socket.assigns.upload_toast_uuid){:noreply,socket}endend

If your toast group uses a custom id, pass the same value as :container_id.

Custom toast components can also dismiss themselves with the composable client-side action dismiss:

<buttontype="button"phx-click={LiveToast.dismiss()}>
Dismiss
</button>

Both dismissal paths run the exit animation before removing the toast.

Browser JavaScript

Import addToast when browser JavaScript needs to request a toast. The mounted toast host validates the requested kind and renders the toast server-side, so it continues to use the host's configured styles, custom component function, animation, and stacking behavior.

import{addToast}from'live_toast'addToast('info','Copied to clipboard.',{title: 'Copied',duration: 3_000,metadata: {has_icon: false}})

The browser API accepts serializable title, duration, and metadata options. Use duration: 'infinity' for a persistent toast.

Or you can use the helper function, put_toast, similar to how you may use put_flash:

defmoduleYourApp.SomeLiveViewdodefhandle_event("submit",_payload,socket)dosocket=socket|>LiveToast.put_toast(:info,"Upload successful."){:noreply,socket}endend

put_toast can take a Phoenix.LiveView.Socket or a Plug.Conn, so you can use the same thing in your live and non-live pages.

defmoduleYourApp.SomeControllerdodefcreate(conn,_params)doconn|>LiveToast.put_toast(:info,"Upload successful.")|>render(:whatever)endend

Configuration

Setting the corner

You can change where toasts are anchored by passing the corner setting to toast_group. Supported positions are :top_left, :top_center, :top_right, :bottom_left, :bottom_center, and :bottom_right. The default is :bottom_right.

<LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}corner={:top_center}toasts_sync={assigns[:toasts_sync]}/>

Internationalization

LiveToast can translate its built-in connection notices. Enable this by adding the following to your config.exs:

config:live_toast,gettext_backend: MyApp.Gettext

Without this configuration, LiveToast renders its built-in connection notices in English.

send_toast/3, put_toast/4, and custom connection-notification copy render the message and title you provide unchanged. Translate application-owned copy before passing it to LiveToast:

LiveToast.send_toast(:error,MyApp.Gettext.dgettext("errors","Failed to send the email"))

You have to create a live_toast.po file, inside the priv/gettext/<language>/LC_MESSAGES/ folder for each language you want to support.

For example, if you want to support spanish, you would create the file live_toast.po in the priv/gettext/es/LC_MESSAGES/ folder, with the following content:

msgid ""msgstr ""
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid"We can't find the internet"msgstr"Nosotros no podemos encontrar internet"msgid"Attempting to reconnect"msgstr"Intentando reconectar"msgid"Something went wrong!"msgstr"¡Algo salió mal!"msgid"Hang in there while we get back on track"msgstr"Aguanta mientras volvemos a la normalidad"

Function Options

send_toast takes a number of arguments to control it's behavior. They are currently:

  • kind: The 'level' of this toast. The component function can receive this and modify behavior based on severity. the toast_class_fn also receives it, and it can be used there to modify styles, for example, making :info toasts green and :error toasts red.
  • body: The primary text of the message.
  • title: The optional title of the toast displayed at the top.
  • icon: An optional function component that renders next to the title. You can use this with the default toast to display an icon.
  • action: An optional function component that renders to the side. You can use this with the default toast to display an action, like a button.
  • duration: How long the toast stays visible in milliseconds. Timed toasts pause while hovered or while keyboard focus is inside the toast, then resume with their remaining duration. Set this to 0 or :infinity to keep a toast visible until it is dismissed. A custom component can include a [data-live-toast-remaining] element when it needs LiveToast to display the remaining whole seconds.
  • component: Use this to totally override rendering of the toast. This is expected to be a function component that will receive all of the above options. See this part of the demo as an example.
  • metadata: An application-defined map passed unchanged to a custom component function. Use this for component-specific presentation or behavior without adding library-level options. LiveToast does not interpret metadata.

Custom rendering and classes

The component option and toast_component_fn render the content inside a LiveToast-managed toast shell. The shell continues to provide the hook, timing attributes, enter and exit animations, stacking, and dismiss button. Use toast_class_fn/1 to change that outer shell's classes; use a custom component function for the icon, title, body, action, and other toast content.

Default custom toast component

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.

<LiveToast.toast_groupflash={@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.

LiveToast.send_toast(:info,"The message was sent.",title: "Sent")

Custom component metadata

Custom component functions receive metadata alongside the standard toast assigns such as kind, title, body, uuid, duration, and dismissible. This gives an application a stable extension point for renderer-specific options.

For example, a component can show its semantic icon by default and let individual toasts opt out:

defnotification_toast(assigns)do~H"""<div><span:if={Map.get(@metadata,:has_icon,true)}aria-hidden="true">*</span><p:if={@title}data-part="title">{@title}</p><p>{@body}</p></div>"""endLiveToast.send_toast(:info,"The message was sent.",title: "Sent",component: &notification_toast/1,metadata: %{has_icon: false})

Persistent toasts

Pass duration: 0 or duration: :infinity to disable automatic expiry. Persistent toasts can still be replaced with the same UUID and dismissed manually or programmatically.

uuid=LiveToast.send_toast(:info,"Export ready",duration: :infinity)LiveToast.dismiss_toast(uuid)

Connection-state notifications

Set connection_notifications={false} if you prefer not to show LiveToast's built-in connection failure notices. This removes both client and server notices, including their connection-event wiring.

<LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}toasts_sync={assigns[:toasts_sync]}connection_notifications={false}/>

Otherwise, customize the built-in client and server connection notices with connection_notifications. Each entry accepts kind, title, and body; unspecified values retain their defaults. Notices are persistent, non-dismissible, and excluded from the ordinary visible-toast limit.

Use the optional :client_error and :server_error slots when an application needs custom markup. LiveToast retains the connection-event wiring, animation, and toast shell while the slot receives id, kind, title, body, and corner through :let.

<LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}toasts_sync={assigns[:toasts_sync]}connection_notifications={%{client_error: %{kind: :info,title: "Reconnecting",body: "Your changes are safe."}}}><:client_error:let={notice}><.reconnecting_noticetitle={notice.title}body={notice.body}/></:client_error></LiveToast.toast_group>

Note that if you use more than just :info and :error in your codebase for flashes, you can augment LiveToast using some of the methods below to support that.

Custom Classes

You can define a custom toast class function, like so:

defmoduleMyModuledodeftoast_class_fn(assigns)do[# base classes"group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",# start hidden if javascript is enabled"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",# used to hide the disconnected flashesif(assigns[:rest][:hidden]==true,do: "hidden",else: "flex"),# override styles per severityassigns[:kind]==:info&&"bg-white text-black",assigns[:kind]==:error&&"!text-red-700 !bg-red-100 border-red-200"]endend

And then use it to override the default styles:

<LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}toast_class_fn={&MyModule.toast_class_fn/1}toasts_sync={assigns[:toasts_sync]}/>

If you need to change the classes of the container, there is a similar function parameter called group_class_fn. Reference the documentation and apply the override just as you would toast_class_fn/1 shown above.

Custom Severity Levels

New Phoenix projects use :info and :error as the default severity levels for flash messages, so this is likely what you're already using. If you need to add an additional severity level, like :warning, you can pass a list of these values to the kind attribute:

<LiveToast.toast_groupflash={@flash}connected={assigns[:socket]!=nil}kinds={[:info,:error,:warning]}toast_class_fn={&custom_toast_class_fn/1}toasts_sync={assigns[:toasts_sync]}/>

If this value is not set, it defaults to [:info, :error].

Setting kind will allow these new severity levels to be displayed, but it won't change how they look. To do that, you need to override toast_class_fn/1. For example:

# Note that this is just the default with one line added to handle the new `:warning` level.defcustom_toast_class_fn(assigns)do[# base classes"bg-white group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",# start hidden if javascript is enabled"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",# used to hide the disconnected flashesif(assigns[:rest][:hidden]==true,do: "hidden",else: "flex"),# override styles per severityassigns[:kind]==:info&&"text-black",assigns[:kind]==:error&&"!text-red-700 !bg-red-100 border-red-200",assigns[:kind]==:warning&&"!text-amber-700 !bg-amber-100 border-amber-200"]end

Then just make sure you've passed it to the live_group component as seen above.

JavaScript Options

You can also change some options about the LiveView hook when it is initialized. Such as:

import{createLiveToastHook}from'live_toast'// the duration for each toast to stay on screen in msconstduration=4000// how many toasts to show on screen at onceconstmaxItems=3constliveToastHook=createLiveToastHook(duration,maxItems)letliveSocket=newLiveSocket('/live',Socket,{hooks: {LiveToast: liveToastHook}})

Roadmap

Some of the stuff still to work on:

  • A11y
  • Further documentation
  • Even more configuration
  • Lots of amazing tests
  • Spring animations
  • Possibly some way to configure additional severity levels

Cutting a new GitHub release:

Mostly for my notes:

git tag -a v0.10.0 -m "LiveToast v0.10.0"
git push origin v0.10.0
gh release create v0.10.0 \
--repo srcrip/live_toast \
--verify-tag \
--title "LiveToast v0.10.0"

About

A beautiful drop-in replacement for the Phoenix Flash system.

Topics

Resources

Stars

264 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages