Packages

A real-browser external-app verifier for patch-safe Phoenix LiveView interactions, with secondary unstyled reference components.

Current section

Files

Jump to
live_interaction_contracts priv positioning-spike app.exs
Raw

priv/positioning-spike/app.exs

#!/usr/bin/env elixir
# CSS Anchor Positioning spike fixture.
# Question under test: can a native popover be positioned relative to its trigger with
# ZERO JavaScript (anchor-name / position-anchor / position-area / position-try-fallbacks),
# across engines, and does the anchored geometry survive LiveView patches and follow the
# anchor when a patch moves it?
# All positioning CSS is server-rendered static attributes (server-owned constants), so
# reconciliation must keep it — same Delegation argument as the popover attribute itself.
# Run: elixir app.exs (http://127.0.0.1:4133)
Mix.install([
{:phoenix, "~> 1.8"},
{:phoenix_live_view, "~> 1.2"},
{:bandit, "~> 1.7"},
{:jason, "~> 1.4"}
])
Application.put_env(:phoenix, :json_library, Jason)
Application.put_env(:ps, PSWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4133],
server: true,
adapter: Bandit.PhoenixAdapter,
live_view: [signing_salt: "positioning-spike-salt"],
secret_key_base: String.duplicate("positioning-spike-secret!", 4),
pubsub_server: PS.PubSub,
check_origin: false,
debug_errors: true
)
defmodule PSWeb.Layout do
use Phoenix.Component
@phoenix_js File.read!(Application.app_dir(:phoenix, "priv/static/phoenix.min.js"))
@lv_js File.read!(Application.app_dir(:phoenix_live_view, "priv/static/phoenix_live_view.min.js"))
def render("root.html", assigns) do
assigns = assign(assigns, phoenix_js: @phoenix_js, lv_js: @lv_js)
~H"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<title>Positioning spike</title>
<script><%= Phoenix.HTML.raw(@phoenix_js) %></script>
<script><%= Phoenix.HTML.raw(@lv_js) %></script>
<script>
window.addEventListener("DOMContentLoaded", () => {
const csrf = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const ls = new LiveView.LiveSocket("/live", Phoenix.Socket, { params: {_csrf_token: csrf} });
ls.connect();
window.liveSocket = ls;
});
</script>
<style>
body { margin: 0; }
/* Anchor declarations live in a server-rendered static class, not inline style,
so morphdom treats them exactly like any other server-owned constant attr. */
.spike-anchor { anchor-name: --spike; }
.spike-pop {
position: fixed;
/* neutralize the UA popover centering (inset: 0 + margin: auto) */
inset: auto;
margin: 0;
width: 200px;
height: 100px;
box-sizing: border-box;
border: 1px solid black;
position-anchor: --spike;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block;
}
</style>
</head>
<body><%= @inner_content %></body>
</html>
"""
end
end
defmodule PSWeb.MainLive do
use Phoenix.LiveView
def mount(_p, _s, socket),
do: {:ok, assign(socket, content: 0, attr: 0, sib: 0, spacer: 40)}
def render(assigns) do
~H"""
<main>
<div id="spacer" style={"height: #{@spacer}px"}></div>
<button id="anchor-btn" class="spike-anchor" popovertarget="pop" style="display:block; width:120px; height:32px;">
anchor
</button>
<div id="pop" popover="manual" class="spike-pop" data-rev={@attr}>
<p id="pop-body">item {@content}</p>
</div>
<div id="sib">sib {@sib}</div>
<div style="height: 2000px"></div>
<button id="btn-content" phx-click="content">patch content</button>
<button id="btn-attr" phx-click="attr">patch attr</button>
<button id="btn-sib" phx-click="sib">patch sibling</button>
<button id="btn-move" phx-click="move">move anchor (+150)</button>
<button id="btn-bottom" phx-click="bottom">push anchor to viewport bottom</button>
<button id="btn-reset" phx-click="reset">reset spacer</button>
</main>
"""
end
def handle_event("content", _, s), do: {:noreply, assign(s, content: s.assigns.content + 1)}
def handle_event("attr", _, s), do: {:noreply, assign(s, attr: s.assigns.attr + 1)}
def handle_event("sib", _, s), do: {:noreply, assign(s, sib: s.assigns.sib + 1)}
def handle_event("move", _, s), do: {:noreply, assign(s, spacer: s.assigns.spacer + 150)}
# 720 viewport - 32 button - 50 margin => button bottom ~ 688; 100px popover cannot fit below
def handle_event("bottom", _, s), do: {:noreply, assign(s, spacer: 638)}
def handle_event("reset", _, s), do: {:noreply, assign(s, spacer: 40)}
end
defmodule PSWeb.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :protect_from_forgery
plug :put_root_layout, html: {PSWeb.Layout, :root}
end
scope "/" do
pipe_through :browser
live "/", PSWeb.MainLive
end
end
defmodule PSWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :ps
@session_options [store: :cookie, key: "_ps_key", signing_salt: "positioningspk", same_site: "Lax"]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
plug Plug.Session, @session_options
plug PSWeb.Router
end
{:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: PS.PubSub}, PSWeb.Endpoint], strategy: :one_for_one)
IO.puts("VERSIONS phoenix=#{Application.spec(:phoenix, :vsn)} live_view=#{Application.spec(:phoenix_live_view, :vsn)} elixir=#{System.version()} otp=#{System.otp_release()}")
IO.puts("READY http://127.0.0.1:4133")
Process.sleep(:infinity)