Skip to content

Instantly share code, notes, and snippets.

@elepedus
Created February 13, 2025 12:12
Show Gist options
  • Save elepedus/d03086bfeee896aeb7b3306474347d37 to your computer and use it in GitHub Desktop.
Save elepedus/d03086bfeee896aeb7b3306474347d37 to your computer and use it in GitHub Desktop.
How to use Liveview in Livebook without opening separate ports

Proxy Liveview Longpoll IFrame

Mix.install(
  [
    {:kino, "~> 0.14.2"},
    {:phoenix_live_view, "~> 1.0"},
    {:jason, "~> 1.4"}
  ],
  config: [
    proxy_live_view: [
      {
        ProxyEndpoint,
        [
          server: false,
          adapter: Bandit.PhoenixAdapter,
          live_view: [signing_salt: "aasdffas"],
          secret_key_base: String.duplicate("a", 64),
          pubsub_server: ProxyLiveview.PubSub,
          url: [path: "/proxy/sessions/2rp6rvkl4s7ohcbexl776pht4ordlbrt6pdhj6yecoodwrwb"] # change this to your proxy url
        ]
      }
    ]
  ]
)

Section

defmodule ProxyLiveView.Iframe do
  use Kino.JS

  def start(endpoint: endpoint) do
    Kino.start_child!({Phoenix.PubSub, [name: ProxyLiveview.PubSub]})
    Kino.start_child!({Registry, [name: nil, keys: :unique]})
    Kino.start_child!(endpoint)
    Kino.Proxy.listen(ProxyEndpoint)
    new(src: apply(endpoint, :path, ["/"]))
  end

  def new(src: src) do
    Kino.JS.new(__MODULE__, src)
  end

  asset "main.js" do
    """
    export function init(ctx, src) {
      const iframe = document.createElement('iframe')
      iframe.setAttribute("src", src)
      iframe.style.width = "100%"
      iframe.style.height = "660px"
      iframe.style.border = "none"
      ctx.root.appendChild(iframe)
    }
    """
  end
end
defmodule ProxyLiveView.Layout do
  use Phoenix.Component

  def render("root.html", assigns) do
    ~H"""
    <!DOCTYPE html>
    <html lang="en" class="h-full">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <.live_title>
          <%= assigns[:page_title] || "Phoenix Playground" %>
        </.live_title>
        <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/ajN8ioAAAAASUVORK5CYII=">
      </head>
      <body>
        <script src={ProxyEndpoint.static_path("/assets/phoenix/phoenix.js")}></script>
        <script src={ProxyEndpoint.static_path("/assets/phoenix_live_view/phoenix_live_view.js")}></script>
        <script src="https://cdn.tailwindcss.com"></script>
        <script>
          // Set global hooks and uploaders objects to be used by the LiveSocket,
          // so they can be overwritten in user provided templates.
          window.hooks = {}
          window.uploaders = {}

           let liveSocket =
            new window.LiveView.LiveSocket(
              "<%= ProxyEndpoint.static_path("/proxylive") %>",
              window.Phoenix.Socket,
              { hooks, uploaders, transport: window.Phoenix.LongPoll }
            )
          liveSocket.connect()
        </script>
        <div class="container mx-auto">
          <%= @inner_content %>
        </div>
      </body>
    </html>
    """
  end
end

defmodule ProxyEndpoint.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule ProxyLiveView.RootLive do
  use Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(value: true)}
  end

  def render(assigns) do
    ~H"""
    {@value}
    <button class="border rounded bg-teal-100 p-2" phx-click="toggle">toggle</button>
    """
  end

  def handle_event("toggle", _, socket) do
    {:noreply,
     socket
     |> assign(value: !socket.assigns.value)}
  end
end

defmodule ProxyRouter do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:put_root_layout, html: {ProxyLiveView.Layout, :root})
  end

  scope "/", ProxyLiveView do
    pipe_through(:browser)
    live("/", RootLive)
  end
end
defmodule ProxyEndpoint do
  use Phoenix.Endpoint,
    otp_app: :proxy_live_view

  plug(Plug.Logger, log: :debug)

  socket("/proxylive", Phoenix.LiveView.Socket,
    longpoll: true,
    websocket: false,
    pubsub_server: ProxyLiveView.PubSub
  )

  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(ProxyRouter)
end
ProxyLiveView.Iframe.start(endpoint: ProxyEndpoint)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment