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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ Releasing new bundle (meant to compile it in `v0.6.1`).
- Readme fixes in #32
- Fix earlier weirdness with gettext usage in 0.7.0 in #34 (thanks @neilberkman)
- Fix docs source ref in #39 (thanks @Flo0807)

## [v0.9.0] (unreleased)

- Made display of the disconnect error delayed and configurable in #19 (thanks @lardcanoe)
88 changes: 57 additions & 31 deletions assets/js/live_toast/live_toast.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { animate } from 'motion'
import type { ViewHook } from 'phoenix_live_view'
import { animate } from "motion"
import type { ViewHook } from "phoenix_live_view"

function isHidden(el: HTMLElement | null) {
if (el === null) {
Expand All @@ -10,26 +10,26 @@ function isHidden(el: HTMLElement | null) {
}

function isFlash(el: HTMLElement) {
return el.dataset.component === 'flash'
return el.dataset.component === "flash"
}

// number of flashes that aren't hidden
function flashCount() {
let num = 0

if (!isHidden(document.getElementById('server-error'))) {
if (!isHidden(document.getElementById("server-error"))) {
num += 1
}

if (!isHidden(document.getElementById('client-error'))) {
if (!isHidden(document.getElementById("client-error"))) {
num += 1
}

if (!isHidden(document.getElementById('flash-info'))) {
if (!isHidden(document.getElementById("flash-info"))) {
num += 1
}

if (!isHidden(document.getElementById('flash-error'))) {
if (!isHidden(document.getElementById("flash-error"))) {
num += 1
}

Expand All @@ -56,7 +56,7 @@ declare global {

function doAnimations(
this: ViewHook,
delayTime: number,
animationDelayTime: number,
maxItems: number,
elToRemove?: HTMLElement
) {
Expand Down Expand Up @@ -99,14 +99,14 @@ function doAnimations(

const toast = ts[i]

let direction = ''
let direction = ""

if (
toast.dataset.corner === 'bottom_left' ||
toast.dataset.corner === 'bottom_center' ||
toast.dataset.corner === 'bottom_right'
toast.dataset.corner === "bottom_left" ||
toast.dataset.corner === "bottom_center" ||
toast.dataset.corner === "bottom_right"
) {
direction = '-'
direction = "-"
}

// Calculate the translateY value with gap
Expand All @@ -122,17 +122,17 @@ function doAnimations(

// also if this item moved past the max limit, disable click events on it
if (toast.order >= max) {
toast.classList.remove('pointer-events-auto')
toast.classList.remove("pointer-events-auto")
} else {
toast.classList.add('pointer-events-auto')
toast.classList.add("pointer-events-auto")
}

const keyframes = { y: [`${direction}${val}px`], opacity: [opacity] }

// if element is entering for the first time, start below the fold
if (toast.order === 0 && lastTS.includes(toast) === false) {
const val = toast.offsetHeight + gap
const oppositeDirection = direction === '-' ? '' : '-'
const oppositeDirection = direction === "-" ? "" : "-"
keyframes.y.unshift(`${oppositeDirection}${val}px`)

keyframes.opacity.unshift(0)
Expand All @@ -142,9 +142,14 @@ function doAnimations(

const duration = animationTime / 1000

// as of right now this is not exposed to end users, but
// it's 'plumbed out' if we want to make it so in the future
const delayTime = Number.parseInt(this.el.dataset.delay || "0") / 1000

animate(toast, keyframes, {
duration,
easing: [0.22, 1.0, 0.36, 1.0]
easing: [0.22, 1.0, 0.36, 1.0],
delay: delayTime
})
toast.order += 1

Expand All @@ -156,9 +161,9 @@ function doAnimations(
// also what about elements moving down when you close one?
window.setTimeout(() => {
if (toast.order > max) {
this.pushEventTo('#toast-group', 'clear', { id: toast.id })
this.pushEventTo("#toast-group", "clear", { id: toast.id })
}
}, delayTime + removalTime)
}, animationDelayTime + removalTime)

lastTS = ts
}
Expand All @@ -167,14 +172,14 @@ function doAnimations(
async function animateOut(this: ViewHook) {
const val = (this.el.order - 2) * 100 + (this.el.order - 2) * gap

let direction = ''
let direction = ""

if (
this.el.dataset.corner === 'bottom_left' ||
this.el.dataset.corner === 'bottom_center' ||
this.el.dataset.corner === 'bottom_right'
this.el.dataset.corner === "bottom_left" ||
this.el.dataset.corner === "bottom_center" ||
this.el.dataset.corner === "bottom_right"
) {
direction = '-'
direction = "-"
}

const animation = animate(
Expand All @@ -183,10 +188,10 @@ async function animateOut(this: ViewHook) {
{
opacity: {
duration: 0.2,
easing: 'ease-out'
easing: "ease-out"
},
duration: 0.3,
easing: 'ease-out'
easing: "ease-out"
}
)

Expand All @@ -206,27 +211,48 @@ export function createLiveToastHook(duration = 6000, maxItems = 3) {
animate(this.el, keyframes, { duration: 0 })
},
mounted(this: ViewHook) {
this.el.addEventListener("show-error", async _event => {
const delayTime = Number.parseInt(this.el.dataset.delay || "0")
await new Promise(resolve => setTimeout(resolve, delayTime))

// todo: in the future use this to execute the data-disconnected command
// https://elixirforum.com/t/can-we-use-liveview-js-commands-inside-a-hook/67324/8

// const command = this.el.getAttribute('data-disconnected')
// this.liveSocket.execJS(this.el, command)

// (don't want to do this quite yet because 1.0 is pretty new)
// also repeat this on hide.

this.el.style.display = "flex"
})

this.el.addEventListener("hide-error", async _event => {
this.el.style.display = "none"
})

// for the special flashes, check if they are visible, and if not, return early out of here.
if (['server-error', 'client-error'].includes(this.el.id)) {
if (["server-error", "client-error"].includes(this.el.id)) {
if (isHidden(document.getElementById(this.el.id))) {
return
}
}

window.addEventListener('phx:clear-flash', e => {
this.pushEvent('lv:clear-flash', {
window.addEventListener("phx:clear-flash", e => {
this.pushEvent("lv:clear-flash", {
key: (e as CustomEvent<{ key: string }>).detail.key
})
})

window.addEventListener('flash-leave', async event => {
window.addEventListener("flash-leave", async event => {
if (event.target === this.el) {
// animate this flash sliding out
doAnimations.bind(this, duration, maxItems, this.el)()
await animateOut.bind(this)()
}
})

// begin actually showing the toast through this call to the animation function
doAnimations.bind(this)(duration, maxItems)

// skip the removal code if this is a flash
Expand All @@ -245,7 +271,7 @@ export function createLiveToastHook(duration = 6000, maxItems = 3) {
// animate this element sliding down, opacity to 0, with delay time
await animateOut.bind(this)()

this.pushEventTo('#toast-group', 'clear', { id: this.el.id })
this.pushEventTo("#toast-group", "clear", { id: this.el.id })
}, durationOverride + removalTime)
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/live_toast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ defmodule LiveToast do

attr :toasts_sync, :list, required: true, doc: "toasts that get synchronized when calling `put_toast`"

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

@doc """
Renders a group of toasts and flashes.

Expand All @@ -259,6 +261,7 @@ defmodule LiveToast do
corner={@corner}
toast_class_fn={@toast_class_fn}
group_class_fn={@group_class_fn}
client_error_delay={@client_error_delay}
flash={@flash}
kinds={@kinds}
/>
Expand Down
24 changes: 20 additions & 4 deletions lib/live_toast/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ defmodule LiveToast.Components do
attr(:rest, :global, doc: "the arbitrary HTML attributes to add to the flash container")
attr(:target, :any, default: nil, doc: "the target for the phx-click event")

attr(:delay, :integer,
default: 0,
doc: "adds a delay before being shown. not exposed by default, used only for 'client-error' and 'server-error'"
)

attr(:duration, :integer,
default: 6000,
doc: "the time in milliseconds before the message is automatically dismissed"
Expand Down Expand Up @@ -50,6 +55,7 @@ defmodule LiveToast.Components do
role="alert"
phx-hook="LiveToast"
data-duration={@duration}
data-delay={@delay}
data-corner={@corner}
class={@toast_class_fn.(assigns)}
{@rest}
Expand Down Expand Up @@ -108,6 +114,8 @@ defmodule LiveToast.Components do

attr(:f, :map, required: true, doc: "the map of flash messages")

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

attr(:corner, :atom,
values: [:top_left, :top_center, :top_right, :bottom_left, :bottom_center, :bottom_right],
default: :bottom_right,
Expand Down Expand Up @@ -142,9 +150,12 @@ defmodule LiveToast.Components do
id="client-error"
kind={:error}
title={Utility.translate("We can't find the internet")}
delay={@client_error_delay}
phx-update="ignore"
phx-disconnected={Utility.show(".phx-client-error #client-error")}
phx-connected={Utility.hide("#client-error")}
phx-disconnected={Utility.show_error(".phx-client-error #client-error")}
phx-connected={Utility.hide_error("#client-error")}
data-disconnected={Utility.show(".phx-client-error #client-error")}
data-connected={Utility.hide("#client-error")}
hidden
>
{Utility.translate("Attempting to reconnect")}
Expand All @@ -159,8 +170,11 @@ defmodule LiveToast.Components do
kind={:error}
title={Utility.translate("Something went wrong!")}
phx-update="ignore"
phx-disconnected={Utility.show(".phx-server-error #server-error")}
phx-connected={Utility.hide("#server-error")}
phx-disconnected={Utility.show_error(".phx-server-error #server-error")}
phx-connected={Utility.hide_error("#server-error")}
data-disconnected={Utility.show(".phx-server-error #server-error")}
data-connected={Utility.hide("#server-error")}
delay={@client_error_delay}
hidden
>
{Utility.translate("Hang in there while we get back on track")}
Expand Down Expand Up @@ -189,6 +203,8 @@ defmodule LiveToast.Components do
doc: "function to override the toast classes"
)

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

# Used to render flashes-only on regular non-LV pages.
@doc false
def flash_group(assigns) do
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 @@ -77,6 +77,8 @@ defmodule LiveToast.LiveComponent do
{:ok, socket}
end

# todo: if someone really wants it, we can implement the internally used `delay` option here.

@impl Phoenix.LiveComponent
def render(assigns) do
~H"""
Expand Down
8 changes: 8 additions & 0 deletions lib/live_toast/utility.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ defmodule LiveToast.Utility do
"""
end

def show_error(js \\ %JS{}, selector) do
JS.dispatch(js, "show-error", to: selector)
end

def hide_error(js \\ %JS{}, selector) do
JS.dispatch(js, "hide-error", to: selector)
end

def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
Expand Down