Coder Social home page Coder Social logo

flashy's Introduction

Flashy

Hex Hexdocs

Flashy is a small library that extends LiveView's flash support to function and live components.

2023-10-22.20-43-38.webm

Installation

First add Flashy to your list of dependencies in mix.exs:

def deps do
  [
    {:flashy, "~> 0.2.7"}
  ]
end

Now, inside assets/js/app.js, add flashy hooks:

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: {
        // ...
    },
    ...FlashyHooks
}

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks})

Now, inside assets/tailwind.config.js:

...

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:

  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:

<.flash_group flash={@flash} />

With:

<Flashy.Container.render flash={@flash} />

Flashy notification hook

Flashy also has a hook that can be added to LiveViews to make it easier to send notifications from a LiveComponent to a LiveView when no navigation is happening.

First, we need to add the hook to a LiveView live_session:

live_session :my_session, on_mount: [Flashy.Hook] do
  ...
end

Now, inside any LiveComponent, we can send a notification to the LiveView like this:

notification = Notification.Normal.new(:success, "some notification")

socket = send_notification(socket, notification)

Using with controllers

If you want to also use Flashy with controllers outside LiveView, first you need to go to your web file lib/<your_app>_web.ex and add the following to controller function:

defp controller do
  quote do
    ...

    # Add Flash notifications functionality
    import Flashy
  end
end

Now, inside any Controller, we can send a notification like this:

def index(conn, _) do
  notification = Notifications.Normal.new(:info, "some notification")

  conn |> put_notification(notification) |> render(:index)
end

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:

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:

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:

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:

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:

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:

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:

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:

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:

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 to see it working in practice.

Customizing CSS

By default Flashy notifications will show-up on the top right of the screen. But sometimes your requirements can be different, maybe you want the notifications to show-up on the left side, maybe the way Flashy default CSS doesn't work well with your project CSS, etc.

Flashy uses PhxComponentHelpers to customize CSS, you can check the library here: https://hexdocs.pm/phx_component_helpers/PhxComponentHelpers.html

Flashy allows to fully customize the CSS, as an example, I will show how to move the notifications to the left.

The first place we can change CSS is to the Flashy.Container component, you can update your lib/<your_app>_web/components/layouts/app.html.heex file to:

<Flashy.Container.render
  flash={@flash}
  class="!right-0 left-0 !items-end items-start"
/>

With this change, we are replacing right-0 with left-0 and items-end with items-start.

Now, we want to customize our components, first, we will want to create a custom transition that will move from the left to right, let's create a module to add that since they are used by all notification components:

defmodule MyProjectWeb.Components.Notifications.Helpers do
  @moduledoc false

  alias Phoenix.LiveView.JS

  def hide_notification(key) do
    JS.hide(
      to: "##{key}",
      transition: {"ease-in duration-300", "translate-x-0", "translate-x-[-100%]"},
      time: 300
    )
    |> JS.push("lv:clear-flash", value: %{key: key})
  end

  def show_notification(key) do
    JS.show(
      to: "##{key}",
      transition: {"ease-in duration-300", "translate-x-[-100%]", "translate-x-0"},
      time: 300
    )
  end
end

Now let's start changing our components, let's start with the Disconnected one.

First we add the alias to our new helper:

alias MyProjectWeb.Components.Notifications.Helpers

Then, inside the render function, we change the way we call Flashy disconnected render:

<Flashy.Disconnected.render
  key={@key}
  class="!pr-3 pl-3 !translate-x-full translate-x-[-100%]"
  hide_action={Helpers.hide_notification(@key)}
  show_action={Helpers.show_notification(@key)}
>

What we are doing here is customize the CSS and the JS actions.

For the CSS, we replaced !pr-3 with pl-3 and translate-x-full with translate-x-[-100$].

For the JS actions, we are using the ones for our helper instead of Flashy built-in ones.

Now, let's do the same for the Normal component:

alias FlashyExampleWeb.Components.Notifications.Helpers

...

<Flashy.Normal.render
  key={@key}
  notification={@notification}
  class="!pr-3 pl-3 !translate-x-full translate-x-[-100%]"
  hide_action={Helpers.hide_notification(@key)}
  show_action={Helpers.show_notification(@key)}
>

It is exactly the same changes are the Disconnected component above.

Finally, we will also update our Custom component.

On that one we are importing Flashy built-in helpers, se we will replace that with ours:

alias FlashyExampleWeb.Components.Notifications.Helpers

alias Flashy.Component

Now we just need to update the component CSS as-well.

In this case we are not using PhxComponentHelpers, so we will just implement the full class directly:

<div
  id={@id}
  class={"pointer-events-auto pl-3 select-none drop-shadow flex items-center translate-x-[-100%] hidden"}
  phx-mounted={Helpers.show_notification(@key)}
  data-hide={Helpers.hide_notification(@key)}
  data-show={Helpers.show_notification(@key)}
  {@rest}
>

After these changes, your notifications will show up on the left:

2023-10-23.20-23-15.webm

flashy's People

Contributors

sezaru avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

flashy's Issues

Unknown hook found

I followed the instructions, but when running, I get errors in the console for unknown hook found for both FlashHook and DisconnectedNotificationHook. I temporarily fixed it by creating the hooks in my own code and importing them, but I assume I should not have to?

I'm using Elixir 1.15.3, Phoenix 1.7.10 and Phoenix Liveview 0.20.1

Import as a module

The project I'm working on has some large dependencies, so I've enabled splitting in esbuild and set type="module" for app.js

When trying to import Flashy I am receiving the following error:

[FlashHook is undefined](flashy.min.js:22 Uncaught ReferenceError: FlashHook is not defined)

To replicate, just change app.js type to module. After looking through the JS, it seems that line 5 here is missing a const.

After adding the const, I am able to see the flash notifications but I am also seeing unknown hook found for "FlashHook" in the console. Any thoughts on how to fix this?

Support for AlpineJS

It would be helpful to extend the library to be able to select either the current implementation or AlpineJS for all client-side timer processing to relieve the need for extra server interactions. I had a similar requirement in my "multi-select" component to be able to support both AlpineJS and Phoenix's JS events and it works really nicely. Also you may want to copy from that project the mix install task, which automates the required modifications of source code during installation of the library.

Persist notifications across routes

Hi,

The title says it all. Currently notifications go away when the route is changed. I'd like the notifications to stay until they expire even if the user changes the route.

Is this possible?

Thanks,
Theron

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.