Skip to content

Instantly share code, notes, and snippets.

@mayel
Last active July 21, 2022 21:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mayel/690a4e27d394888d8ff59c9e0fefe32f to your computer and use it in GitHub Desktop.
Save mayel/690a4e27d394888d8ff59c9e0fefe32f to your computer and use it in GitHub Desktop.
reusable modal in Surface/LiveView
defmodule Bonfire.UI.Common.OpenModalLive do
@moduledoc """
A button that opens a **modal**
"""
use Bonfire.UI.Common.Web, :stateful_component
alias Bonfire.UI.Common.ReusableModalLive
@doc "The title of the modal. Only used if no title slot is passed."
prop title_text, :string
@doc "The classes of the title of the modal"
prop title_class, :css_class, default: "font-bold text-base"
@doc "The classes of the close/cancel button on the modal. Only used if no close_btn slot is passed."
prop cancel_btn_class, :css_class, default: "btn btn-outline btn-sm normal-case"
@doc "Force modal to be open"
prop show, :boolean, default: false
prop form_opts, :any, default: []
@doc "Optional prop to hide the actions at the bottom of the modal"
prop no_actions, :boolean, default: false
@doc "The classes of the title of the modal"
prop reusable_modal_id, :string, default: "modal"
@doc """
Additional attributes to add onto the modal wrapper
"""
prop opts, :keyword, default: []
@doc """
Slots for the contents of the modal, title, buttons...
"""
slot default
slot open_btn
slot action_btns
slot cancel_btn
slot title
def open() do
set(show: true)
end
def close() do
set(show: false)
end
def set(assigns) when is_list(assigns) do
send_update(ReusableModalLive, Keyword.put(assigns, :id, e(assigns, :reusable_modal_id, "modal")))
end
def set(assigns) when is_map(assigns) do
send_update(ReusableModalLive, Map.put(assigns, :id, e(assigns, :reusable_modal_id, "modal")))
end
# Default event handlers
def handle_event("open", _, socket) do
socket = socket
|> assign(show: true)
set(socket.assigns) # copy all of this component's assigns to the reusable modal (including slots!)
{:noreply, socket}
end
def handle_event("close", _, socket) do
close()
{:noreply, socket}
end
end
<button
:on-click={"open"}
type="button"
>
<#slot name="open_btn">
<button class={@open_btn_class} type="button">
{@open_btn_text || "Open modal"}
</button>
</#slot>
</button>
defmodule Bonfire.UI.Common.ReusableModalLive do
use Bonfire.UI.Common.Web, :stateful_component
@moduledoc """
A reusable **modal**. This component is meant to be included only once, typically in the app's live layout.
"""
@doc "The title of the modal. Only used if no title slot is passed."
prop title_text, :string, default: nil
@doc "The classes of the title of the modal"
prop title_class, :css_class, default:
@doc "The classes of the close/cancel button on the modal. Only used if no close_btn slot is passed."
prop cancel_btn_class, :css_class, default: nil
@doc "Force modal to be open"
prop show, :boolean, default: false
prop form_opts, :any, default: []
@doc "Optional prop to hide the actions at the bottom of the modal"
prop no_actions, :boolean, default: false
@doc """
Additional attributes to add onto the modal wrapper
"""
prop opts, :keyword, default: []
@doc """
Slots for the contents of the modal, title, buttons...
"""
slot default
slot open_btn
slot action_btns
slot cancel_btn
slot title
def mount(socket) do
# need this because ReusableModalLive when called in the HEEX layout doesn't set Surface defaults
{:ok, socket
|> assign(
title_text: nil,
title_class: nil,
cancel_btn_class: nil,
show: false,
form_opts: [],
no_actions: false,
opts: []
)
}
end
def handle_event("close", _, socket) do
{:noreply, assign(socket, show: false)}
end
end
<div
id={@id}
class={"modal max-h-[100%] ",
"modal-closed": !@show,
"modal-open": @show}
:attrs={@opts}>
<form {...@form_opts}>
<div
phx-click="close"
phx-target={"##{@id}"}
class="fixed inset-0 transition-opacity bg-slate-600/60 backdrop-blur-md"
aria-hidden="true">
</div>
<div class="relative border-4 modal-box border-base-content border-opacity-20 max-w-[100%] min-w-[320px] min-h-[100px]">
<h3 class={@title_class || "font-bold text-base"}>
<#slot name="title">{@title_text}</#slot>
</h3>
<div class="absolute top-0 right-0 block pt-5 pr-4">
<button
phx-click="close"
phx-target={"##{@id}"}
type="button"
class="normal-case btn btn-ghost btn-circle btn-sm">
<span class="sr-only">{l "Close"}</span>
<Outline.XIcon class="w-4 h-4" />
</button>
</div>
<div class="mt-2" data-id="modal-contents">
<#slot></#slot>
</div>
<div
:if={!@no_actions}
class="modal-action">
<#slot name="action_btns"></#slot>
<div :on-click="close">
<#slot name="cancel_btn">
<button class={@cancel_btn_class || "btn btn-outline btn-sm normal-case"} type="button">{l "Cancel"}</button>
</#slot>
</div>
</div>
</div>
</form>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment