# Flashy
[![Hex](https://img.shields.io/hexpm/v/flashy.svg)](https://hex.pm/packages/flashy)
[![Hexdocs](https://img.shields.io/badge/-docs-green)](https://hexdocs.pm/flashy)
Flashy is a small library that extends LiveView's flash support to function and live components.
[2023-10-22 20-43-38.webm](https://github.com/sezaru/flashy/assets/279828/ac1784ab-1126-4625-8097-81d8fc40adb1)
## Installation
First add `Flashy` to your list of dependencies in `mix.exs`:
``` elixir
def deps do
[
{:flashy, "~> 0.1.0"}
]
end
```
Now, inside `assets/js/app.js`, add `flashy` hooks:
``` javascript
import FlashyHooks from "flashy"
// if you don't have any other hooks:
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: FlashyHooks})
// if you have other hooks:
const hooks = {
MyHook: {
// ...
},
...FlashHooks
}
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks})
```
Now, inside `assets/tailwind.config.js`:
``` javascript
...
module.exports = {
content: [
"./js/**/*.js",
"../lib/flashy_example_web.ex",
"../lib/flashy_example_web/**/*.*ex",
"../deps/flashy/**/*.*ex", // <-- Add this line
],
...
```
Now go to your web file `lib/<your_app>_web.ex` and add the following to `html_helpers` function:
``` elixir
defp html_helpers do
quote do
...
# Add Flash notifications functionality
import Flashy
end
end
```
Finally, you need to update your `lib/<your_app>_web/components/layouts/app.html.heex`:
Replace the line:
``` elixir
<.flash_group flash={@flash} />
```
With:
``` elixir
<Flashy.Container.render flash={@flash} />
```
### Disconnected notifications
Now we need to, at least, implement the disconnected notification. `Flashy` doesn't come with any pre-defined disconnected notification design, so you need to implement it yourself.
Here is an example of one implementation using `PetalComponents` `alert` component:
``` elixir
defmodule MyProjectWeb.Components.Notifications.Disconnected do
@moduledoc false
use MyProjectWeb, :html
use Flashy.Disconnected
import PetalComponents.Alert
attr :key, :string, required: true
def render(assigns) do
~H"""
<Flashy.Disconnected.render key={@key}>
<.alert with_icon color="danger" heading="We can't find the internet">
Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
</.alert>
</Flashy.Disconnected.render>
"""
end
end
```
Now you need to set the following config so `Flashy` knows what disconnected component we should use, to do that, in your `config/config.exs` add the following:
``` elixir
config :flashy,
disconnected_module: MyProjectWeb.Components.Notifications.Disconnected
```
Now we are all set, `Flashy` is ready to be used.
## Adding normal notifications
Above we setup a disconnected component which is mandatory, but you probably want to have a "normal" notification to use for simple messages.
`Flashy` ships with a base implementation for a notification like that, it supports timed auto-hide with progress bar and showing or not a close button.
To implement it you just need to define how to render its body, similar to how we did with the disconnected component. Here is an example using `PetalComponents` `alert` component:
``` elixir
defmodule MyProjectWeb.Components.Notifications.Normal do
@moduledoc false
use MyProjectWeb, :html
use Flashy.Normal, types: [:info, :success, :warning, :danger]
import PetalComponents.Alert
attr :key, :string, required: true
attr :notification, Flashy.Normal, required: true
def render(assigns) do
~H"""
<Flashy.Normal.render key={@key} notification={@notification}>
<.alert
with_icon
close_button_properties={close_button_properties(@notification.options, @key)}
color={color(@notification.type)}
class="relative overflow-hidden"
>
<span><%= Phoenix.HTML.raw(@notification.message) %></span>
<.progress_bar :if={@notification.options.dismissible?} id={"#{@key}-progress"} />
</.alert>
</Flashy.Normal.render>
"""
end
attr :id, :string, required: true
defp progress_bar(assigns) do
~H"""
<div id={@id} class="absolute bottom-0 left-0 h-1 bg-black/10" style="width: 0%" />
"""
end
defp color(type), do: to_string(type)
defp close_button_properties(%{closable?: true}, key),
do: ["phx-click": JS.exec("data-hide", to: "##{key}")]
defp close_button_properties(%{closable?: false}, _), do: nil
end
```
Note that you can set any `types` you want to the normal component, you just need to add it to the `types` list when calling `use Flashy.Normal`:
``` elixir
use Flashy.Normal, types: [:info, :fatal, :some_other_type]
```
## Adding a entirely custom notification
You can also create 100% custom notifications for your needs, for example, `Flashy` supports live components when you need to store state or handle events, here I will show a custom notification that will how a form inside with a text input field.
The idea with this notification would be to allow you to create a notification with business logic, for example, if you are creating a chat application, you can have a notification that will allow users to reply to it directly from the notificatio itself.
Here is the implementation:
``` elixir
defmodule MyProjectWeb.Components.Notifications.Custom do
@moduledoc false
alias Flashy.{Component, Helpers}
use MyProjectWeb, :live_component
use TypedStruct
import PetalComponents.{Alert, Input, Button}
typedstruct enforce: true do
field :question, String.t()
field :target_module, module
field :target_id, String.t()
field :component, Component.t()
end
@spec new(String.t(), module, String.t()) :: t
def new(question, target_module, target_id) do
struct!(__MODULE__,
question: question,
target_module: target_module,
target_id: target_id,
component: Component.new(&live_render/1)
)
end
attr :key, :string, required: true
attr :notification, __MODULE__, required: true
attr :rest, :global
def live_render(%{key: key} = assigns) do
assigns = assign(assigns, id: key)
~H"<.live_component module={__MODULE__} {assigns} />"
end
def update(assigns, socket) do
socket = socket |> assign(assigns) |> assign(form: to_form(%{}))
{:ok, socket}
end
def handle_event("send_answer", %{"answer" => answer}, socket) do
%{id: id, notification: %{target_module: module, target_id: target_id}} = socket.assigns
send_update(module, id: target_id, answer: answer)
socket = push_event(socket, "js-exec", %{to: "##{id}", attr: "data-hide"})
{:noreply, socket}
end
def render(assigns) do
~H"""
<div
id={@id}
class={Helpers.notification_classes()}
phx-mounted={Helpers.show_notification(@key)}
data-hide={Helpers.hide_notification(@key)}
data-show={Helpers.show_notification(@key)}
{@rest}
>
<.alert with_icon color="info" class="relative overflow-hidden">
<.form for={@form} phx-submit="send_answer" phx-target={@myself}>
<div class="flex flex-col gap-2">
<div><%= Phoenix.HTML.raw(@notification.question) %></div>
<.input field={@form[:answer]} />
<.button type="submit" label="Answer" />
</div>
</.form>
</.alert>
</div>
"""
end
end
defimpl Flashy.Protocol, for: MyProjectWeb.Components.Notifications.Custom do
def module(notification), do: notification.component.module
def function_name(notification), do: notification.component.function_name
end
```
The main takeaway here is that you always need to generate a struct which implements the `Flashy.Protocol`, this is how `Flashy` know which component it needs to call to render.
## Usage
Now that we have `Flashy` installed with some notifications, to use it is pretty simple, here are some examples:
Showing a `info` normal notification:
``` elixir
alias MyProjectWeb.Components.Notifications.Normal
put_notification(socket, Normal.new(:info, "My <i>cool</i> notification"))
```
Flashy supports stacked notifications as-well, so you can do something like this:
``` elixir
alias MyProjectWeb.Components.Notifications.Normal
socket
|> put_notification(Normal.new(:info, "My <i>cool</i> notification"))
|> put_notification(Normal.new(:info, "My another <i>cool</i> notification"))
|> put_notification(Normal.new(:danger, "Fatal error notification"))
```
When using normal notifications, you can also set if they are dimissable and how much time it will be visible:
``` elixir
alias MyProjectWeb.Components.Notifications.Normal
# This option means the notification will never auto-hide,
# the user will need to close it via the close button
options_1 = Flashy.Normal.Options.new(dismissible?: false)
# This option means the notification will not show the close button
options_2 = Flashy.Normal.Options.new(closable?: false)
# This option means you can set how much time the notification will show
# before it auto-hides
options_3 = Flashy.Normal.Options.new(dismiss_time: :timer.seconds(2))
socket
|> put_notification(Normal.new(:info, "My <i>cool</i> notification", options_1))
|> put_notification(Normal.new(:info, "My <i>cool</i> notification", options_2))
|> put_notification(Normal.new(:info, "My <i>cool</i> notification", options_3))
```
Finally, we, of course, can also create notifications with our own custom notifications:
``` elixir
alias MyProjectWeb.Components.Notifications.Custom
put_notification(socket, Custom.new("How are you today?", __MODULE__, id))
```
## More examples
You can check how the library works by going to our [examples project](https://github.com/sezaru/flashy_example) to see it working in practice.