Created
December 12, 2025 08:42
-
-
Save mxgrn/c860982bedb02c2c6f5b9518cc057378 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Application.put_env(:sample, Example.Endpoint, | |
| http: [ip: {127, 0, 0, 1}, port: 5001], | |
| server: true, | |
| live_view: [signing_salt: "aaaaaaaa"], | |
| secret_key_base: String.duplicate("a", 64) | |
| ) | |
| Mix.install([ | |
| {:plug_cowboy, "~> 2.5"}, | |
| {:jason, "~> 1.0"}, | |
| {:phoenix, "~> 1.7"}, | |
| # please test your issue using the latest version of LV from GitHub! | |
| {:phoenix_live_view, | |
| github: "phoenixframework/phoenix_live_view", branch: "main", override: true} | |
| # {:phoenix_live_view, path: "~/oss/phoenix_live_view", override: true}, | |
| ]) | |
| # if you're trying to test a specific LV commit, it may be necessary to manually build | |
| # the JS assets. To do this, uncomment the following lines: | |
| # this needs mix and npm available in your path! | |
| # | |
| # path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../") | |
| # System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream()) | |
| # System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream()) | |
| # System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream()) | |
| defmodule Example.ErrorView do | |
| def render(template, _), do: Phoenix.Controller.status_message_from_template(template) | |
| end | |
| defmodule Example.DemoLive do | |
| use Phoenix.LiveView, layout: {__MODULE__, :live} | |
| def mount(_params, _session, socket) do | |
| {:ok, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1)) | |
| |> push_event("start-view-transition", %{type: "back"}, dispatch: :before)} | |
| end | |
| def handle_event("add", _, socket) do | |
| Agent.update(DB, &(&1 ++ [%{id: "#{:rand.uniform(1_000_000)}"}])) | |
| {:noreply, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1))} | |
| end | |
| def handle_event("remove", %{"id" => id}, socket) do | |
| Agent.update(DB, fn entries -> Enum.reject(entries, &(&1.id == id)) end) | |
| {:noreply, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1))} | |
| end | |
| def render("live.html", assigns) do | |
| ~H""" | |
| <script src="/assets/phoenix/phoenix.js"> | |
| </script> | |
| <script src="/assets/phoenix_live_view/phoenix_live_view.js"> | |
| </script> | |
| <%!-- uncomment to use enable tailwind --%> | |
| <%!-- <script src="https://cdn.tailwindcss.com"></script> --%> | |
| <script> | |
| let transitionTypes = []; | |
| let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, { | |
| dom: { | |
| onDocumentPatch(start) { | |
| const types = transitionTypes.length ? transitionTypes : ["same-document"]; | |
| const update = () => { | |
| transitionTypes = []; | |
| start(); | |
| } | |
| // firefox 144 doesn't support the callbackOptions yet, so fallback to the basic version. | |
| try { | |
| document.startViewTransition({ update, types }); | |
| } catch (error) { | |
| document.startViewTransition(update); | |
| } | |
| } | |
| } | |
| }) | |
| liveSocket.connect() | |
| window.addEventListener("phx:start-view-transition", (e) => { | |
| const opts = e.detail; | |
| if (opts.type) { | |
| transitionTypes.push(opts.type); | |
| } | |
| }); | |
| </script> | |
| <style> | |
| * { font-size: 1.1em; } | |
| </style> | |
| {@inner_content} | |
| """ | |
| end | |
| def render(assigns) do | |
| ~H""" | |
| <Assets.styles /> | |
| <button class="btn" phx-click="add">Add</button> | |
| <.card :for={entry <- @entries} entry={entry} /> | |
| """ | |
| end | |
| defp card(assigns) do | |
| ~H""" | |
| <div | |
| id={"card-#{@entry.id}"} | |
| class="flex gap-2 bg-gray-800 p-2 m-4 rounded" | |
| > | |
| <.card_img id={@entry.id} class="h-24" style="" /> | |
| <div class="flex gap-2"> | |
| <.link navigate={"/card/#{@entry.id}"}>Entry {@entry.id}</.link> | |
| <div><button class="btn" phx-click="remove" phx-value-id={@entry.id}>Remove</button></div> | |
| </div> | |
| </div> | |
| """ | |
| end | |
| def card_img(assigns) do | |
| assigns = | |
| assign(assigns, color: "hsl(#{String.to_integer(assigns.id) / 1_000_000}turn 40% 25%)") | |
| ~H""" | |
| <div | |
| class={[ | |
| "bg-contain aspect-square bg-[url(https://www.phoenixframework.org/images/icon.svg)]", | |
| @class | |
| ]} | |
| style={"background-color: #{@color};" <> @style} | |
| /> | |
| """ | |
| end | |
| end | |
| defmodule Example.CardLive do | |
| use Phoenix.LiveView, layout: {Example.DemoLive, :live} | |
| def mount(%{"id" => id}, _session, socket) do | |
| {:ok, | |
| assign(socket, id: id) | |
| |> push_event("start-view-transition", %{type: "forward"}, dispatch: :before)} | |
| end | |
| def render(assigns) do | |
| ~H""" | |
| <Assets.styles /> | |
| <div class="contents"> | |
| <.link navigate="/">← back</.link> | |
| <h2 class="m-4 font-bold text-xl">Entry {@id}</h2> | |
| <div class="bg-gray-800"> | |
| <Example.DemoLive.card_img | |
| id={@id} | |
| style="" | |
| class="mx-auto h-128" | |
| /> | |
| </div> | |
| </div> | |
| """ | |
| end | |
| end | |
| defmodule Assets do | |
| use Phoenix.Component | |
| def styles(assigns) do | |
| ~H""" | |
| <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"> | |
| </script> | |
| <style type="text/tailwindcss"> | |
| html { | |
| background-color: var(--color-gray-950); | |
| } | |
| :root { | |
| color-scheme: dark; | |
| } | |
| .btn { | |
| @apply bg-slate-600 p-1 rounded | |
| } | |
| @media (prefers-reduced-motion) { | |
| ::view-transition-group(*), | |
| ::view-transition-old(*), | |
| ::view-transition-new(*) { | |
| animation: none !important; | |
| } | |
| } | |
| @view-transition { | |
| navigation: auto; | |
| types: page | |
| } | |
| @keyframes slide-from-right { | |
| from { transform: translateX(100%); } | |
| } | |
| @keyframes slide-to-left { | |
| to { transform: translateX(-100%); } | |
| } | |
| @keyframes slide-from-left { | |
| from { transform: translateX(-100%); } | |
| } | |
| @keyframes slide-to-right { | |
| to { transform: translateX(100%); } | |
| } | |
| html:active-view-transition-type(forward) { | |
| &::view-transition-old(root) { | |
| animation: none; | |
| } | |
| &::view-transition-new(root) { | |
| z-index: 1; | |
| animation: 300ms ease-out both slide-from-right; | |
| } | |
| } | |
| html:active-view-transition-type(back) { | |
| &::view-transition-old(root) { | |
| z-index: 1; | |
| animation: 300ms ease-out both slide-to-right; | |
| } | |
| &::view-transition-new(root) { | |
| animation: none; | |
| } | |
| } | |
| </style> | |
| """ | |
| end | |
| end | |
| defmodule Example.Router do | |
| use Phoenix.Router | |
| import Phoenix.LiveView.Router | |
| pipeline :browser do | |
| plug(:accepts, ["html"]) | |
| end | |
| scope "/", Example do | |
| pipe_through(:browser) | |
| live("/", DemoLive) | |
| live("/card/:id", CardLive) | |
| end | |
| end | |
| defmodule Example.Endpoint do | |
| use Phoenix.Endpoint, otp_app: :sample | |
| socket("/live", Phoenix.LiveView.Socket) | |
| plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix") | |
| plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view") | |
| plug(Example.Router) | |
| end | |
| {:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one) | |
| Agent.start_link(fn -> [] end, name: DB) | |
| Process.sleep(:infinity) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This one is based on https://gist.github.com/SteffenDE/cf7cdb91ba037b08cdc583763e4ffc69