Skip to content

Commit

Permalink
Flash message display helper (#13)
Browse files Browse the repository at this point in the history
* Helper to render flash message displays in a consistent way
  • Loading branch information
jeremyowensboggs authored Sep 16, 2021
1 parent 247d171 commit 6d24125
Show file tree
Hide file tree
Showing 3 changed files with 313 additions and 0 deletions.
84 changes: 84 additions & 0 deletions lib/components/flash_message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
defmodule CrunchBerry.Components.FlashMessage do
@moduledoc """
Function to render flash message blocks in a consistent manner
Uses layout for flash messages that was established in ecom_api, and makes
it available to inject into components and layouts
Flash messages are local to a component, so we can't package this up as a
live_view component, because then the component would only display flash messages for itself.
The assigns in the argument are *mandatory*
e.g.
```
<%= CrunchBerry.Components.FlashMessage.render_flash(%{flash: @flash, myself: @myself}) %>
```
Or, if you are using the live_helper
```
<%= render_flash(assigns) %>
```
## N.B. Using in a stateless component
Note well, in order for change tracking to work in a stateless component,
you must pass the flash into the assigns. See the test `describe "in a stateless component" do`
for a full example, but in a nutshell:
If StateslessComponentFixture is using the `render_flash/1` then you need to pass in flash to the
component for change tracking to work
```
<%= live_component StatelessComponentFixture, flash: @flash %>`
```
"""
import Phoenix.LiveView.Helpers

def render_flash(assigns) do
~H"""
<%= render_flash_block(%{flash: @flash, myself: @myself, color: "red", type: :error}) %>
<%= render_flash_block(%{flash: @flash, myself: @myself, color: "green", type: :info}) %>
"""
end

defp render_flash_block(assigns) do
~H"""
<%= if live_flash(@flash, @type) do %>
<div class={"bg-true-#{@color}-50 pt-3 pb-1 px-4"}>
<div class="flex">
<div class="flex-shrink-0">
<span class={"material-icons text-#{@color}-1"}>check_circle</span>
</div>
<div class="ml-2 mt-0.5">
<p class={"text-sm font-medium text-#{@color}-1"}>
<%= live_flash(@flash, @type) %>
</p>
</div>
<div class="ml-auto pl-3">
<div class="-my-1.5">
<%= render_button(%{color: @color, myself: @myself, type: @type}) %>
</div>
</div>
</div>
</div>
<% end %>
"""
end

defp render_button(assigns) do
if Map.get(assigns, :myself) do
~H"""
<button type="button" phx-click="lv:clear-flash" phx-value-key={@type} phx-target={@myself}
class={"text-2xl inline-flex px-1.5 pb-0.5 rounded-md text-#{@color}-1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-#{@color}-50 focus:ring-#{@color}-1"}>
&times;
</button>
"""
else
~H"""
<button type="button" phx-click="lv:clear-flash" phx-value-key={@type}
class={"text-2xl inline-flex px-1.5 pb-0.5 rounded-md text-#{@color}-1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-#{@color}-50 focus:ring-#{@color}-1"}>
&times;
</button>
"""
end
end
end
36 changes: 36 additions & 0 deletions lib/components/live_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule CrunchBerry.Components.LiveHelpers do
alias CrunchBerry.Components.Modal
alias CrunchBerry.Components.Pagination
alias CrunchBerry.Components.TypeAhead
alias CrunchBerry.Components.FlashMessage

@doc """
Renders a component inside the `CrunchBerry.Components.Modal` component.
Expand Down Expand Up @@ -198,4 +199,39 @@ defmodule CrunchBerry.Components.LiveHelpers do
def live_type_ahead(opts) do
live_component(TypeAhead, opts)
end

@doc """
Renders markup to display flash messages from `put_live_flash/3`
Assigns:
This function must be called in a particular way every time:
Inside of a stateful component -
```
<%= render_flash(assigns) %>
```
## N.B. Using in a stateless component
Note well, in order for change tracking to work in a stateless component,
you must pass the flash into the assigns. See the test `describe "in a stateless component" do`
for a full example, but in a nutshell:
If StateslessComponentFixture is using the `render_flash/1` then you need to pass in flash to the
component for change tracking to work
```
<%= live_component StatelessComponentFixture, flash: @flash %>`
```
"""
@spec render_flash(any) :: Phoenix.LiveView.Rendered.t()
def render_flash(assigns) do
case assigns do
# When called inside of a stateless component
%{flash: _flash, myself: _myself} ->
FlashMessage.render_flash(assigns)

# Handle the case when called outside of a stateful component
%{flash: _flash} ->
assigns = Map.put(assigns, :myself, nil)
FlashMessage.render_flash(assigns)
end
end
end
193 changes: 193 additions & 0 deletions test/components/flash_message_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
defmodule Optimizer.Api.UserLive.UploadSalesCSV.FlashMessageTest do
use ExUnit.Case
import Phoenix.ConnTest
import Phoenix.LiveViewTest

@endpoint CrunchBerry.TestEndpoint

setup do
conn = build_conn()
socket = %Phoenix.LiveView.Socket{}
start_supervised!(CrunchBerry.TestEndpoint)
{:ok, socket: socket, conn: conn}
end

describe "Renders in a view" do
defmodule LiveViewFixture do
use Phoenix.LiveView
import CrunchBerry.Components.LiveHelpers

def mount(_params, _session, socket) do
{:ok, socket}
end

def render(assigns) do
~H"""
<div id="flash-content">
<%= render_flash(assigns) %>
</div>
<button phx-click="push-me" phx-value="button">Click</button>
"""
end

def handle_event(event, _params, socket) do
socket = Phoenix.LiveView.put_flash(socket, :info, "Info flash message")
{:noreply, assign(socket, message: event)}
end
end

test "Flash renders", %{conn: conn} do
# Start out without a flash message
{:ok, view, html} = live_isolated(conn, LiveViewFixture)
refute html =~ "flash message"

# click to create the flash message
click_html =
view
|> element("button", "Click")
|> render_click()

# Verify that it is there
assert click_html =~ "flash message"

dismissed_html =
view
|> element("#flash-content button")
|> render_click()

refute dismissed_html =~ "flash message"
end
end

defmodule ComponentFixture do
use Phoenix.HTML
use Phoenix.LiveComponent
import CrunchBerry.Components.LiveHelpers

def render(assigns) do
~H"""
<div>
<div id="flash-content">
<%= render_flash(assigns) %>
</div>
<button phx-click="push-me" phx-value="button" phx-target={@myself}>Click</button>
</div>
"""
end

def handle_event(event, _params, socket) do
socket = Phoenix.LiveView.put_flash(socket, :info, "Info flash message")
{:noreply, assign(socket, message: event)}
end
end

describe "in a stateful component" do
defmodule ComponentViewFixture do
use Phoenix.LiveView

def mount(_params, _session, socket) do
{:ok, socket}
end

def render(assigns) do
~H"""
<div>
<%= live_component ComponentFixture, id: "test-fixture" %>
</div>
"""
end

def handle_event(event, _params, socket) do
socket = Phoenix.LiveView.put_flash(socket, :info, "Info flash message")
{:noreply, assign(socket, message: event)}
end
end

test "renders correctly", %{conn: conn} do
# Start out without a flash message
{:ok, view, html} = live_isolated(conn, ComponentViewFixture)
refute html =~ "flash message"

# click to create the flash message
click_html =
view
|> element("button", "Click")
|> render_click()

# Verify that it is there
assert click_html =~ "flash message"

dismissed_html =
view
|> element("#flash-content button")
|> render_click()

refute dismissed_html =~ "flash message"
end
end

describe "in a stateless component" do
defmodule StatelessComponentFixture do
use Phoenix.HTML
use Phoenix.LiveComponent
import CrunchBerry.Components.LiveHelpers

def render(assigns) do
~H"""
<div>
<div id="flash-content">
<%= render_flash(assigns) %>
</div>
<button phx-click="push-me" phx-value="button">Click</button>
</div>
"""
end
end

defmodule ViewStatelessComponentFixture do
use Phoenix.LiveView

def mount(_params, _session, socket) do
{:ok, socket}
end

def render(assigns) do
~H"""
<div>
<%= live_component StatelessComponentFixture, flash: @flash %>
</div>
"""
end

def handle_event(event, _params, socket) do
socket = Phoenix.LiveView.put_flash(socket, :error, "stateless flash error message")
{:noreply, assign(socket, message: event)}
end
end

test "renders correctly", %{conn: conn} do
# Start out without a flash message
{:ok, view, html} = live_isolated(conn, ViewStatelessComponentFixture)
refute html =~ "stateless flash error message"

# click to create the flash message
click_html =
view
|> element("button", "Click")
|> render_click()

# Verify that it is there
assert click_html =~ "stateless flash error message"

dismissed_html =
view
|> element("#flash-content button")
|> render_click()

refute dismissed_html =~ "stateless flash error message"
end
end
end

0 comments on commit 6d24125

Please sign in to comment.