-
-
Save SteffenDE/cf7cdb91ba037b08cdc583763e4ffc69 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.8"}, | |
| {:phoenix_live_view, "~> 1.1.18"}, | |
| # {: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} | |
| alias Phoenix.LiveView.JS | |
| defp js_start_view_transition(js \\ %JS{}, opts) do | |
| {to, opts} = Keyword.pop!(opts, :to) | |
| JS.dispatch(js, "phx:start-view-transition", to: to, detail: Map.new(opts)) | |
| end | |
| def mount(_params, _session, socket) do | |
| {:ok, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1)) | |
| |> push_event("start-view-transition", %{type: "page"}, 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)) | |
| |> push_event("start-view-transition", %{type: "same-document"}, dispatch: :before)} | |
| 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)) | |
| |> push_event("start-view-transition", %{type: "same-document"}, dispatch: :before)} | |
| 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 transitionEls = []; | |
| let scheduleTransition = null; | |
| let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, { | |
| dom: { | |
| onDocumentPatch(start) { | |
| const update = () => { | |
| // reset transitionEls | |
| transitionEls.forEach((el) => el.style.viewTransitionName = ""); | |
| transitionEls = []; | |
| transitionTypes = []; | |
| scheduleTransition = null; | |
| start(); | |
| } | |
| if (transitionEls.length !== 0 || scheduleTransition) { | |
| // firefox 144 doesn't support the callbackOptions yet, so fallback to the basic version. | |
| try { | |
| document.startViewTransition({ | |
| // tsc somehow doesn't know about the `update` param??! | |
| // @ts-expect-error | |
| update, | |
| types: transitionTypes.length ? transitionTypes : ["same-document"], | |
| }); | |
| } catch (error) { | |
| document.startViewTransition(update); | |
| } | |
| } else { | |
| update(); | |
| } | |
| } | |
| } | |
| }) | |
| liveSocket.connect() | |
| window.addEventListener("phx:start-view-transition", (e) => { | |
| const opts = e.detail; | |
| if (opts.temp_name && e.target !== window) { | |
| e.target.style.viewTransitionName = opts.temp_name; | |
| transitionEls.push(e.target); | |
| } | |
| if (opts.type) { | |
| transitionTypes.push(opts.type); | |
| } | |
| scheduleTransition = true; | |
| }); | |
| </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}"} | |
| style={"view-transition-name: card-#{@entry.id}; view-transition-class: card;"} | |
| 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 phx-click={ | |
| js_start_view_transition(temp_name: "img-full", to: "#card-#{@entry.id} > div.bg-contain") | |
| |> JS.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: "page"}, 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="view-transition-name: img-full" | |
| 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; | |
| --enter-anim: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |
| 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |
| --exit-anim: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |
| 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |
| } | |
| .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 fade-in { | |
| from { opacity: 0; } | |
| } | |
| @keyframes fade-out { | |
| to { opacity: 0; } | |
| } | |
| @keyframes slide-from-right { | |
| from { transform: translateX(30px); } | |
| } | |
| @keyframes slide-to-left { | |
| to { transform: translateX(-30px); } | |
| } | |
| html:active-view-transition-type(page) { | |
| &::view-transition-group(root) { | |
| z-index: -10; | |
| } | |
| &::view-transition-old(root) { | |
| animation: var(--exit-anim); | |
| } | |
| &::view-transition-new(root) { | |
| animation: var(--enter-anim); | |
| } | |
| &::view-transition-new(img-full) { | |
| animation: none; | |
| } | |
| &::view-transition-group(.card) { | |
| z-index: -1; | |
| } | |
| &::view-transition-old(.card) { | |
| animation: var(--exit-anim); | |
| } | |
| &::view-transition-new(.card) { | |
| animation: var(--enter-anim); | |
| } | |
| } | |
| </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
@SteffenDE you missed the scheduleTransition = true; inside the window.addEventListener("phx:start-view-transition", (e) => {, then it's good.
Thanks for updating it 👍
Author
@spicychickensauce thanks for double-checking! I also fixed the view transition not triggering in the add / remove events, since I actually relied on it always triggering for that case :D
@SteffenDE perfect, thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@spicychickensauce I actually was aware that it would always trigger when I posted the example, but I was lazy and didn't care to update it. You're right that it'll throw people off, so it's update now. Thank you! :)