Skip to content

Instantly share code, notes, and snippets.

@mazz
Last active October 23, 2020 22:56
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 mazz/ff4af076e51e28d41a31d48ff47e86ca to your computer and use it in GitHub Desktop.
Save mazz/ff4af076e51e28d41a31d48ff47e86ca to your computer and use it in GitHub Desktop.
liveview alpine modal live component
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import "../css/app.scss"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import deps with the dep name or local files with a relative path, for example:
//
// import {Socket} from "phoenix"
// import socket from "./socket"
//
import "phoenix_html"
import {Socket} from "phoenix"
import NProgress from "nprogress"
import {LiveSocket} from "phoenix_live_view"
import {InitToast} from "./init_toast.js"
let Hooks = {}
Hooks.InitToast = InitToast
Hooks.initModal = {
mounted() {
const handleOpenCloseEvent = event => {
if (event.detail.open === false) {
this.el.removeEventListener("modal-change", handleOpenCloseEvent)
this.pushEvent("close-modal", {id: this.el.id})
}
}
this.el.addEventListener("modal-change", handleOpenCloseEvent)
}
}
Hooks.closeModal = {
mounted() {
const modalId = this.el.dataset.modalId
const el = document.getElementById(modalId)
const event = new CustomEvent('close-modal');
el.dispatchEvent(event)
}
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
params: {_csrf_token: csrfToken},
dom: {
onBeforeElUpdated(from, to){
if(from.__x){ window.Alpine.clone(from.__x, to) }
}
}
})
// Show progress bar on live navigation and form submits
window.addEventListener("phx:page-loading-start", info => NProgress.start())
window.addEventListener("phx:page-loading-stop", info => NProgress.done())
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
import "alpinejs"
defmodule ElijahWeb.InvitationRequestLive.Index do
use ElijahWeb, :live_view
alias Elijah.Accounts.Invitations
alias Elijah.Schema.InvitationRequest
@impl true
def mount(_params, %{"csrf_token" => csrf_token} = _session, socket) do
if connected?(socket), do: Invitations.subscribe()
socket = assign(socket,
conn: socket,
csrf_token: csrf_token)
{:ok, assign(socket, :invitation_requests, fetch_invitation_requests()), temporary_assigns: [invitation_requests: []]}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
# defp apply_action(socket, :edit, %{"id" => id}) do
# socket
# |> assign(:page_title, "Edit Post")
# |> assign(:invitation_request, Timeline.get_invitation_request!(id))
# end
# defp apply_action(socket, :new, _params) do
# socket
# |> assign(:page_title, "New Post")
# |> assign(:invitation_request, %Post{})
# end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Invitation Requests")
|> assign(:invitation_request, nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
invitation_request = Invitations.get_invitation!(id)
|> IO.inspect()
{:ok, _} = Invitations.delete_invitation(invitation_request)
{:noreply, assign(socket, :invitation_requests, fetch_invitation_requests())}
end
@impl true
def handle_info({:invitation_request_created, invitation_request}, socket) do
{:noreply, update(socket, :invitation_requests, fn invitation_requests -> [invitation_request | invitation_requests] end)}
end
def handle_info({:invitation_request_updated, invitation_request}, socket) do
{:noreply, update(socket, :invitation_requests, fn invitation_requests -> [invitation_request | invitation_requests] end)}
end
@impl true
def handle_info({:invitation_request_deleted, _invitation_request}, socket) do
{:noreply, assign(socket, :invitation_requests, fetch_invitation_requests())}
end
# MODAL
def handle_event("submit", %{"id" => id}, socket) do
send_update ElijahWeb.ModalComponent, id: id, action: "CLOSE"
{:noreply, socket}
end
def handle_event("open-modal", %{"id" => id}, socket) do
IO.inspect(id)
IO.inspect(socket)
send_update ElijahWeb.ModalComponent, id: id, state: "OPEN"
{:noreply, socket}
end
def handle_event("close-modal", %{"id" => id}, socket) do
:timer.sleep(300) # SO THE CSS ANIMATIONS HAVE TIME TO RUN
send_update ElijahWeb.ModalComponent, id: id, state: "CLOSED", action: nil
{:noreply, socket}
end
defp fetch_invitation_requests do
Invitations.list_invitations()
end
end
<div class="w-full card mb-20">
<div class="card-header flex">
<h5 class="mb-0 flex-1">Invitation Requests</h5>
</div>
<table class="card-body table">
<thead>
<tr>
<th>Email</th>
<th>Token</th>
<th>Invitation Sent</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for invitation_request <- @invitation_requests do %>
<tr>
<td><%= invitation_request.email %></td>
<td><%= invitation_request.token %></td>
<td><%= invitation_request.invitation_sent %></td>
<td class="w-1/12 whitespace-no-wrap">
<span class="rounded-md shadow-sm">
<button phx-click="open-modal" phx-value-id="modal-one" type="button" class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150">Open modal one</button>
</span>
<%= link "Show", to: Routes.invitation_request_path(@conn, :show, invitation_request), class: "btn btn-sm btn-link" %>
<%= link "Edit", to: Routes.invitation_request_path(@conn, :edit, invitation_request), class: "btn btn-sm btn-link" %>
<%= link "Delete", to: "#", phx_click: "delete", phx_value_id: invitation_request.id, class: "btn btn-sm btn-link", data: [confirm: "This will stop the user from redeeming the invite request. Are you sure?"] %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= live_component @socket, ElijahWeb.ModalComponent, id: "modal-one" do %>
<form>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
Username
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="username" type="text" placeholder="Username">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
Password
</label>
<input class="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="password" type="password" placeholder="******************">
<p class="text-red-500 text-xs italic">Please choose a password.</p>
</div>
<div class="flex items-center justify-between">
<button phx-click="submit" phx-value-id="modal-one" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="button">
Sign In
</button>
<a class="inline-block align-baseline text-sm text-indigo-600 hover:text-indigo-800" href="#">
Forgot Password?
</a>
</div>
</form>
<% end %>
defmodule ElijahWeb.ModalComponent do
use Phoenix.LiveComponent
require Logger
def mount(socket) do
Logger.info("mount ElijahWeb.ModalComponent")
IO.inspect(socket)
{:ok, assign(socket, state: "CLOSED", action: nil)}
end
def render(assigns) do
IO.inspect(assigns)
if assigns.state == "OPEN" do
~L"""
<div
id="<%= @id %>"
phx-hook="initModal"
x-data="{ open: false }"
x-init="() => {
setTimeout(() => open = true, 100);
$watch('open', isOpen => $dispatch('modal-change', { open: isOpen }))
}"
x-show="open"
@close-modal="setTimeout(() => open = false, 100)"
class="z-50 fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center"
>
<%= if @action == "CLOSE" do %>
<div id="close-modal-<%= @id %>" data-modal-id="<%= @id %>" phx-hook="closeModal"></div>
<% end %>
<!-- BACKDROP -->
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-800 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full">
<div @click.away="open = false" class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button type="button" @click="open = false" class="text-gray-400 hover:text-gray-500 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- CONTENT -->
<%= @inner_content.([]) %>
</div>
</div>
</div>
"""
else
~L"""
<div id="closed-modal-<%= @id %>"></div>
"""
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment