Easy Phoenix Playground Restarts in LiveBook
The Problem
Phoenix Playground is a great tool for quickly demonstrating and testing small bits of Phoenix Code. LiveBook is a great tool for sharing and easily running Elixir code from the browser. But when you run a playground in LiveBook there’s no easy way to stop it and start another one with different code.
Our goal in this post is to come up with a simple solution that will let us easily run multiple Phoenix examples in a row in a single LiveBook.
The Solution
PlaygroundManager, a super simple module with a helper function to stop the current playground if it’s running, and a helper function that will automatically stop the current playground and start a new one with the given options.
defmodule PlaygroundManager do
@moduledoc """
A wrapper around PhoenixPlayground that makes it more LiveBook friendly.
"""
def start(opts) do
stop()
PhoenixPlayground.start(opts)
end
def stop do
case Supervisor.which_children(PhoenixPlayground.Application) do
[{PhoenixPlayground, pid, _, _} | _] when is_pid(pid) ->
Supervisor.terminate_child(PhoenixPlayground.Application, PhoenixPlayground)
Supervisor.delete_child(PhoenixPlayground.Application, PhoenixPlayground)
:ok
_ -> {:error, :not_running}
end
end
end
Now let’s try it out with some LiveViews.
defmodule DemoLive do
use Phoenix.LiveView
def render(assigns) do
~H"""
<div>Welcome to DemoLive!!!</div>
"""
end
def mount(_params, _session, socket) do
{:ok, socket}
end
end
PlaygroundManager.start(live: DemoLive)
Executing that block of code should start the playground and automatically open a new tab showing the DemoLiveView.
Now let’s define another simple counter LiveView and try launching that with PlaygroundManager.
defmodule CounterLive do
use Phoenix.LiveView
def render(assigns) do
~H"""
<div>
<button phx-click="update-count" phx-value-count={@count - 1}>-</button>
<div>The current count is: <%= @count %></div>
<button phx-click="update-count" phx-value-count={@count + 1}>+</button>
</div>
"""
end
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("update-count", %{"count" => count}, socket) do
{:noreply, assign(socket, count: String.to_integer(count))}
end
end
PlaygroundManager.start(live: CounterLive)
Running that block should launch a new tab showing the counter LiveView.
Putting them together
Both of our LiveView modules are still defined, which means we can easily use them again in a new example. For example, let’s define a simple Router that will let us visit both.
defmodule DemoRouter do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
end
scope "/" do
pipe_through :browser
live "/counter", CounterLive
live "/", DemoLive
end
end
PlaygroundManager.start(plug: DemoRouter)
Now you can visit the counter LiveView at /counter
and the DemoLiveView at /
.
Conclusion
This super simple module makes it much easier to demonstrate multiple Phoenix concepts in a single LiveBook.
You can use this immediately by simply including the module in the setup of your LiveBook, you could even forgo the module and just define anonymous functions but I think the module is probably nicer. Hopefully in the future some of this functionality will be included in Phoenix Playground itself.
Suggestions
- Set a
port
variable early in the Livebook to avoid conflicts with other Phoenix apps that the reader of your LiveBook might have running. - Set an
open_browser?
variable early in the Livebook to let readers opt out of launching a new tab for each example.