Skip to content

Instantly share code, notes, and snippets.

@joenoon
Last active January 18, 2023 00:18
Show Gist options
  • Save joenoon/f92e60731720c95bd1c152b93a9d039f to your computer and use it in GitHub Desktop.
Save joenoon/f92e60731720c95bd1c152b93a9d039f to your computer and use it in GitHub Desktop.
Phoenix LiveView main/nested setup
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
</head>
<body>
<div id="phx_root">
<%= render @view_module, @view_template, assigns %>
</div>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
defmodule MyAppWeb.AppState do
require Logger
use GenServer
defmodule State do
defstruct [:root_pid, :session, :current_user]
end
def start_link(socket) do
name = via_tuple(socket)
state = %State{root_pid: root_pid(socket)}
GenServer.start_link(__MODULE__, state, name: name)
end
@impl true
def init(%State{} = state) do
Process.flag(:trap_exit, true)
{:ok, state}
end
defp via_tuple(socket),
do: {:via, Registry, {MyAppWeb.AppStateRegistry, root_pid(socket)}}
@impl true
def terminate(reason, _state) do
Logger.debug("AppState terminated: #{inspect(reason)}")
:ok
end
defp root_pid(socket) do
socket.root_pid || self()
end
def session(socket) do
GenServer.call(via_tuple(socket), :session)
end
def set_session(socket, session) do
GenServer.call(via_tuple(socket), {:set_session, session})
end
def current_user(socket) do
GenServer.call(via_tuple(socket), :current_user)
end
# @todo change to send_log_out, return socket
def log_out(socket) do
send(root_pid(socket), :log_out)
end
def log_in(socket, user_id) when user_id != nil do
send(root_pid(socket), {:log_in, user_id: user_id})
end
@impl true
def handle_call(:session, _from, state) do
{:reply, state.session, state}
end
@impl true
def handle_call({:set_session, session}, _from, state) do
{:reply, :ok, %{state | session: session}}
end
@impl true
def handle_call(:current_user, _from, state) do
state =
state
|> put_new_lazy(:current_user, fn ->
case state.session do
%{user_id: user_id} when user_id != nil ->
Accounts.get_user_by_id(user_id)
_ ->
nil
end
end)
{:reply, state.current_user, state}
end
defp put_new_lazy(%State{} = state, key, fun) do
case Map.get(state, key) do
nil -> Map.put(state, key, fun.())
_ -> state
end
end
end
# add to supervisor children:
{Registry, keys: :unique, name: MyAppWeb.AppStateRegistry}
<%= live_render(@socket, MyAppWeb.NavbarLive, id: "nav_#{@socket.view}") %>
<%= render @view_module, @view_template, assigns %>
<%= render MyAppWeb.LayoutView, "footer.html", assigns %>
defmodule MyAppWeb.HomePageLive do
use MyAppWeb, :live_view
use MyAppWeb, :main_live_view
def mount(session, socket) do
mount_as_main(session, socket, __MODULE__)
end
def render(assigns) do
Phoenix.View.render(MyAppWeb.PageView, "index.html", assigns)
end
end
defmodule MyAppWeb.LiveHelpers do
require Logger
import Phoenix.LiveView, only: [redirect: 2, assign: 3]
alias MyAppWeb.AppState
def put_body_layout(socket) do
socket
|> assign(:layout, {MyAppWeb.LayoutView, "body.html"})
end
def log_mount(socket, module) do
Logger.debug(
"MOUNT: #{inspect(module)} connected?=#{inspect(Phoenix.LiveView.connected?(socket))} root_pid=#{
inspect(socket.root_pid)
} self=#{inspect(self())}"
)
socket
end
def mount_as_main(session, socket, module) do
AppState.start_link(socket)
AppState.set_session(socket, session)
current_user = AppState.current_user(socket)
socket =
socket
|> assign(:session, session)
|> assign(:current_user, current_user)
|> assign(:current_user_id, if(current_user, do: current_user.id, else: nil))
|> put_body_layout()
|> log_mount(module)
{:ok, socket}
end
def redirect_if_no_user(%{assigns: %{current_user_id: current_user_id}} = socket)
when current_user_id != nil do
socket
end
def redirect_if_no_user(socket) do
socket
|> redirect(to: "/")
end
def handle_params_reply(socket) do
case socket do
%{redirected: nil} -> {:noreply, socket}
_ -> {:stop, socket}
end
end
end
defmodule MyAppWeb.LiveView.Session do
@moduledoc """
Joe Noon on 9/3/2019
LiveView does not have a direct way to change the session, but does provide a way to set flash.
The application can use `put_flash(socket, :session, %{...})`, and this plug will merge that
map when present into the persisted session.
"""
@behaviour Plug
def init(opts), do: opts
def call(conn, _) do
conn
|> handle_session()
|> handle_reset_session()
end
def handle_session(
%{
private: %{
plug_session: %{"phoenix_flash" => %{"session" => %{} = session}} = plug_session
}
} = conn
) do
conn
|> Plug.Conn.put_private(:plug_session, Map.merge(plug_session, session))
end
def handle_session(conn), do: conn
def handle_reset_session(
%{
private: %{
plug_session: %{"phoenix_flash" => %{"reset_session" => %{}} = phoenix_flash}
}
} = conn
) do
conn
|> Plug.Conn.put_private(:plug_session, %{"phoenix_flash" => phoenix_flash})
end
def handle_reset_session(conn), do: conn
end
defmodule MyAppWeb do
#... controller, view, router use helpers etc.
# add:
def live_view do
quote do
use Phoenix.LiveView
import MyAppWeb.LiveHelpers
alias MyAppWeb.Router.Helpers, as: Routes
alias MyAppWeb.AppState
end
end
def main_live_view do
quote do
alias MyAppWeb.Router.Helpers, as: Routes
def handle_info(:log_out, socket) do
socket =
socket
|> put_flash(:info, "You are now logged out.")
|> put_flash(:reset_session, %{})
|> redirect(to: "/")
{:noreply, socket}
end
def handle_info({:log_in, user_id: user_id}, socket) do
socket =
socket
|> put_flash(:info, "You are now logged in.")
|> put_flash(:session, %{user_id: user_id})
|> redirect(to: "/")
{:noreply, socket}
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment