Repository: phoenix-playground/phoenix_playground
Branch: main
Commit: 681a0eda7fea
Files: 27
Total size: 43.0 KB
Directory structure:
gitextract__377zb3b/
├── .formatter.exs
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── examples/
│ ├── demo_controller.exs
│ ├── demo_controller_test.exs
│ ├── demo_endpoint.exs
│ ├── demo_hooks.exs
│ ├── demo_live.exs
│ ├── demo_live.livemd
│ ├── demo_live_pubsub.exs
│ ├── demo_live_test.exs
│ ├── demo_plug.exs
│ └── demo_router.exs
├── lib/
│ ├── phoenix_playground/
│ │ ├── application.ex
│ │ ├── code_reloader.ex
│ │ ├── endpoint.ex
│ │ ├── error_view.ex
│ │ ├── layout.ex
│ │ ├── router.ex
│ │ └── test.ex
│ └── phoenix_playground.ex
├── mix.exs
└── test/
├── examples_test.exs
├── phoenix_playground_test.exs
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
[
import_deps: [:phoenix],
inputs: ["{mix,.formatter}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"]
]
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-22.04
env:
MIX_ENV: test
strategy:
fail-fast: false
matrix:
include:
- pair:
elixir: "1.15"
otp: "24.3.4.10"
- pair:
elixir: "1.18"
otp: "27.3.4.2"
lint: lint
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@main
with:
otp-version: ${{ matrix.pair.otp }}
elixir-version: ${{ matrix.pair.elixir }}
version-type: strict
- uses: actions/cache@v4
with:
path: deps
key: mix-deps-${{ hashFiles('**/mix.lock') }}
- run: mix deps.get --check-locked
- run: mix format --check-formatted
if: ${{ matrix.lint }}
- run: mix deps.unlock --check-unused
if: ${{ matrix.lint }}
- run: mix deps.compile
- run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}
- run: mix test
if: ${{ ! matrix.lint }}
- run: mix test --warnings-as-errors
if: ${{ matrix.lint }}
================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
phoenix_playground-*.tar
# Temporary files, for example, from tests.
/tmp/
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## v0.1.8 (2025-07-30)
* Use Phoenix LiveView v1.1.
## v0.1.7 (2024-09-24)
* Delegate LiveView `handle_async/3`.
## v0.1.6 (2024-09-03)
* Always use PhoenixPlayground.CodeReloader
* Add blank favicon to layout
* Add `:endpoint` option
## v0.1.5 (2024-08-14)
* Fix live reloading.
* Add `:debug_errors` option.
* Document `PhoenixPlayground.Layout`.
* Add `examples/demo_router.exs`.
## v0.1.4 (2024-07-18)
* Delegate LiveView `handle_params/3`.
* Add support for LiveView hooks.
* Add support for LiveView uploaders.
* Add support for `:page_title` assign.
* Add `:endpoint_options` option
## v0.1.3 (2024-05-16)
* Use state-preserving live reload. (Requires Phoenix LiveView 1.0.0-rc.1+.)
* Display errors using `Plug.Debugger`.
## v0.1.2 (2024-04-29)
* Deprecate setting `:router` in favour of `:plug`.
## v0.1.1 (2024-04-23)
* Use local javascript assets.
* Update secret key base.
* Replace `PhoenixPlayground.start_link/1` with `start/1`.
* Fix opening up component definition/caller: set `:debug_heex_annotations` on application boot.
* Add `:child_specs` option to specify additional processes to run in the supervision tree.
* Prevent VM from halting when playground is started.
## v0.1.0 (2024-04-18)
* Initial release.
================================================
FILE: README.md
================================================
# Phoenix Playground
[](https://github.com/phoenix-playground/phoenix_playground/actions/workflows/ci.yml)
[](https://hex.pm/packages/phoenix_playground)
[](https://hexdocs.pm/phoenix_playground)
Phoenix Playground makes it easy to create single-file [Phoenix](https://www.phoenixframework.org) applications.
## Examples
[](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fphoenix-playground%2Fphoenix_playground%2Fblob%2Fmain%2Fexamples%2Fdemo_live.livemd)
Create a `demo_live.exs` file:
```elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>{@count}</span>
<button phx-click="inc">+</button>
<style type="text/css">
body { padding: 1em; }
</style>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, count: socket.assigns.count + 1)}
end
end
PhoenixPlayground.start(live: DemoLive)
```
and run it:
```
$ iex demo_live.exs
```
<img width="1195" alt="image" src="assets/demo.png">
See more examples below:
* [`examples/demo_live.exs`]
* [`examples/demo_live_test.exs`]
* [`examples/demo_controller.exs`]
* [`examples/demo_controller_test.exs`]
* [`examples/demo_plug.exs`]
* [`examples/demo_router.exs`]
* [`examples/demo_hooks.exs`]
* [`examples/demo_endpoint.exs`]
* [`examples/demo_live_pubsub.exs`]
## License
Copyright (c) 2024 Wojtek Mach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
[`examples/demo_live.exs`]: examples/demo_live.exs
[`examples/demo_live_test.exs`]: examples/demo_live_test.exs
[`examples/demo_controller.exs`]: examples/demo_controller.exs
[`examples/demo_controller_test.exs`]: examples/demo_controller_test.exs
[`examples/demo_plug.exs`]: examples/demo_plug.exs
[`examples/demo_router.exs`]: examples/demo_router.exs
[`examples/demo_hooks.exs`]: examples/demo_hooks.exs
[`examples/demo_endpoint.exs`]: examples/demo_endpoint.exs
[`examples/demo_live_pubsub.exs`]: examples/demo_live_pubsub.exs
================================================
FILE: examples/demo_controller.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoController do
use Phoenix.Controller, formats: [:html]
use Phoenix.Component
plug :put_layout, false
plug :put_view, __MODULE__
def index(conn, params) do
count =
case Integer.parse(params["count"] || "") do
{n, ""} -> n
_ -> 0
end
render(conn, :index, count: count)
end
def index(assigns) do
~H"""
<span>{@count}</span>
<button onclick={"window.location.href=\"/?count=#{@count + 1}\""}>+</button>
<style type="text/css">
body { padding: 1em; }
</style>
"""
end
end
PhoenixPlayground.start(controller: DemoController)
================================================
FILE: examples/demo_controller_test.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoController do
use Phoenix.Controller, formats: [:html]
use Phoenix.Component
plug :put_layout, false
plug :put_view, __MODULE__
def index(conn, params) do
count =
case Integer.parse(params["count"] || "") do
{n, ""} -> n
_ -> 0
end
render(conn, :index, count: count)
end
def index(assigns) do
~H"""
<span>Count: {@count}</span>
<button onclick={"window.location.href='/?count=#{@count + 1}'"}>+</button>
"""
end
end
Logger.configure(level: :warning)
ExUnit.start()
defmodule DemoControllerTest do
use ExUnit.Case
use PhoenixPlayground.Test, controller: DemoController
test "it works" do
conn = get(build_conn(), "/")
assert html_response(conn, 200) =~ "Count: 0"
conn = get(build_conn(), "/?count=1")
assert html_response(conn, 200) =~ "Count: 1"
end
end
================================================
FILE: examples/demo_endpoint.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {})
liveSocket.connect()
</script>
<span>Count: {@count}</span>
<button phx-click="inc">+</button>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
defmodule Demo.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
end
scope "/" do
pipe_through :browser
live "/", DemoLive
end
end
defmodule Demo.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix_playground
plug Plug.Logger
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"
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2
plug Demo.Router
end
PhoenixPlayground.start(endpoint: Demo.Endpoint)
================================================
FILE: examples/demo_hooks.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoHooks do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<span id="hook" phx-hook="MyHook">LiveView</span>
<script>
window.hooks.MyHook = {
mounted() {
this.el.innerHTML += " rocks";
setInterval(() => this.el.innerHTML += "!", 1000);
}
}
</script>
<style type="text/css">
body { padding: 1em; }
</style>
"""
end
end
PhoenixPlayground.start(live: DemoHooks)
================================================
FILE: examples/demo_live.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>{@count}</span>
<button phx-click="inc">+</button>
<style type="text/css">
body { padding: 1em; }
</style>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, count: socket.assigns.count + 1)}
end
end
PhoenixPlayground.start(live: DemoLive)
================================================
FILE: examples/demo_live.livemd
================================================
# Demo
```elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
```
## Section
```elixir
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>{@count}</span>
<button phx-click="inc">+</button>
<style type="text/css">
body { padding: 1em; }
span { font-family: monospace; }
</style>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, count: socket.assigns.count + 1)}
end
end
PhoenixPlayground.start(live: DemoLive)
```
================================================
FILE: examples/demo_live_pubsub.exs
================================================
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule TimelineLive do
use Phoenix.LiveView
@topic "timeline"
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(PhoenixPlayground.PubSub, @topic)
end
socket =
socket
|> assign(temporary_assigns: [form: nil])
|> stream(:posts, [])
|> assign(:form, to_form(%{"content" => ""}))
{:ok, socket}
end
def handle_event("create_post", %{"content" => content}, socket) do
post = %{
id: System.unique_integer([:positive]),
content: content,
inserted_at: DateTime.utc_now()
}
Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, @topic, {:new_post, post})
{:noreply,
socket
|> assign(:form, to_form(%{"content" => ""}))}
end
def handle_event("delete_post", %{"dom_id" => dom_id}, socket) do
Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, @topic, {:delete_post, dom_id})
{:noreply, socket}
end
def handle_info({:new_post, post}, socket) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
end
def handle_info({:delete_post, dom_id}, socket) do
{:noreply, stream_delete_by_dom_id(socket, :posts, dom_id)}
end
def render(assigns) do
~H"""
<div class="timeline">
<h1>Timeline</h1>
<.form for={@form} phx-submit="create_post" id="post-form">
<textarea name="content" placeholder="What's on your mind?" id="content" phx-hook="CmdEnterSubmit"><%= @form.params["content"] %></textarea>
<button type="submit">Post</button>
</.form>
<div class="posts" phx-update="stream" id="posts">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id} class="post">
<div class="post-content">
<p><%= post.content %></p>
<small>{DateTime.truncate(post.inserted_at, :second)}</small>
</div>
<button phx-click="delete_post" phx-value-dom_id={dom_id} class="delete-btn">Delete</button>
</div>
</div>
</div>
<script>
window.hooks.CmdEnterSubmit = {
mounted() {
this.el.addEventListener("keydown", (e) => {
if (e.metaKey && e.key === 'Enter') {
this.el.form.dispatchEvent(
new Event('submit', {bubbles: true, cancelable: true}));
}
})
}
}
</script>
<style>
* {
box-sizing: border-box;
}
.timeline {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 1em;
text-align: center;
}
.post {
border: 1px solid #e1e8ed;
border-radius: 12px;
padding: 16px;
margin: 16px 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.post:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.post-content {
flex: 1;
margin-right: 16px;
}
.post-content p {
color: #2c3e50;
font-size: 1.1em;
line-height: 1.5;
margin: 0 0 8px 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.post-content small {
color: #8795a1;
font-size: 0.9em;
display: block;
word-wrap: break-word;
overflow-wrap: break-word;
}
form {
margin: 20px 0;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
width: 100%;
}
textarea {
width: 100%;
min-height: 3ch;
margin-bottom: 16px;
padding: 12px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 1em;
resize: vertical;
transition: border-color 0.2s ease;
}
textarea:focus {
outline: none;
border-color: #4a9eff;
}
button {
padding: 10px 20px;
background: #4a9eff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.2s ease;
}
button:hover {
background: #357abd;
transform: translateY(-1px);
}
.delete-btn {
background: #ff4a4a;
margin-left: 10px;
padding: 8px 16px;
font-size: 0.9em;
opacity: 0.8;
}
.delete-btn:hover {
background: #bd3535;
opacity: 1;
}
body {
background: #f8fafc;
margin: 0;
padding: 20px;
}
</style>
"""
end
end
PhoenixPlayground.start(live: TimelineLive)
================================================
FILE: examples/demo_live_test.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>Count: {@count}</span>
<button phx-click="inc">+</button>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
Logger.configure(level: :warning)
ExUnit.start()
defmodule DemoLiveTest do
use ExUnit.Case
use PhoenixPlayground.Test, live: DemoLive
test "it works" do
{:ok, view, html} = live(build_conn(), "/")
assert html =~ "Count: 0"
assert render_click(view, :inc, %{}) =~ "Count: 1"
assert render_click(view, :inc, %{}) =~ "Count: 2"
end
end
================================================
FILE: examples/demo_plug.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
PhoenixPlayground.start(
plug: fn conn ->
Plug.Conn.send_resp(conn, 200, "Hello!")
end
)
================================================
FILE: examples/demo_router.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>{@count}</span>
<button phx-click="inc">+</button>
<style type="text/css">
body { padding: 1em; }
</style>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, count: socket.assigns.count + 1)}
end
end
defmodule DemoRouter do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", CounterLive
end
end
PhoenixPlayground.start(plug: DemoRouter)
================================================
FILE: lib/phoenix_playground/application.ex
================================================
defmodule PhoenixPlayground.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
# suppress Phoenix warning about no endpoint configuration
Application.put_env(:phoenix_playground, PhoenixPlayground.Endpoint, [])
Application.put_env(:phoenix_live_view, :debug_heex_annotations, true)
Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__)
end
end
================================================
FILE: lib/phoenix_playground/code_reloader.ex
================================================
defmodule PhoenixPlayground.CodeReloader do
@moduledoc false
def reload(_endpoint, _options \\ []) do
if path = Application.get_env(:phoenix_playground, :file) do
case File.read(path) do
{:ok, contents} ->
old = Code.get_compiler_option(:ignore_module_conflict) == true
Code.put_compiler_option(:ignore_module_conflict, true)
Code.eval_string(contents, [], file: path)
Code.put_compiler_option(:ignore_module_conflict, old)
# ignore fs errors. (Seems like saving file in vim sometimes make it temp dissapear?)
{:error, _reason} ->
:ok
end
else
# in Livebook, path is nil
:ok
end
end
end
================================================
FILE: lib/phoenix_playground/endpoint.ex
================================================
defmodule PhoenixPlayground.Endpoint do
@moduledoc false
use Phoenix.Endpoint, otp_app: :phoenix_playground
@signing_salt "ll+Leuc4"
@session_options [
store: :cookie,
key: "_phoenix_playground_key",
signing_salt: @signing_salt,
same_site: "Lax",
# 14 days
max_age: 14 * 24 * 60 * 60
]
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"
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug :run_live_reload
# TODO:
# plug Phoenix.Ecto.CheckRepoStatus, otp_app: :phoenix_playground
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.Session, @session_options
plug PhoenixPlayground.Router
defp run_live_reload(conn, _options) do
if Application.get_env(:phoenix_playground, :live_reload) do
conn
|> Phoenix.LiveReloader.call([])
|> Phoenix.CodeReloader.call(reloader: &PhoenixPlayground.CodeReloader.reload/2)
else
conn
end
end
# See https://github.com/phoenixframework/phoenix/blob/v1.7.14/lib/phoenix/endpoint.ex#L484:L490
@plug_debugger [
banner: {Phoenix.Endpoint.RenderErrors, :__debugger_banner__, []},
style: [
primary: "#EB532D",
logo:
"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
]
]
# See https://github.com/elixir-plug/plug/blob/v1.16.1/lib/plug/debugger.ex#L129:L146
def call(conn, opts) do
if Application.get_env(:phoenix_playground, :debug_errors, false) do
try do
case conn do
%Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} ->
Plug.Debugger.run_action(conn)
%Plug.Conn{} ->
super(conn, opts)
end
rescue
e in Plug.Conn.WrapperError ->
%{conn: conn, kind: kind, reason: reason, stack: stack} = e
Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger)
catch
kind, reason ->
Plug.Debugger.__catch__(conn, kind, reason, __STACKTRACE__, @plug_debugger)
end
else
super(conn, opts)
end
end
end
================================================
FILE: lib/phoenix_playground/error_view.ex
================================================
defmodule PhoenixPlayground.ErrorView do
@moduledoc false
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
================================================
FILE: lib/phoenix_playground/layout.ex
================================================
defmodule PhoenixPlayground.Layout do
@moduledoc """
Built-in layout.
This is the layout used by `PhoenixPlayground.start/1` with option `:live` or `:controller`
To customize page title, set `:page_title` assign.
You can customize LiveSocket hooks and uploaders by changing `window.hooks` and `window.uploaders`.
"""
use Phoenix.Component
@doc false
def render(template, assigns)
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="/assets/phoenix/phoenix.js"></script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js"></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(
"/live",
window.Phoenix.Socket,
{ hooks, uploaders }
)
liveSocket.connect()
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// Enable server log streaming to client. Disable with reloader.disableServerLogs()
reloader.enableServerLogs()
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//
// * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", e => keyDown = null)
window.addEventListener("click", e => {
if(keyDown === "c"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
} else if(keyDown === "d"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
}
}, true)
window.liveReloader = reloader
})
</script>
<%= @inner_content %>
</body>
</html>
"""
end
end
================================================
FILE: lib/phoenix_playground/router.ex
================================================
defmodule PhoenixPlayground.Router do
@moduledoc false
@behaviour Plug
@impl true
def init([]) do
[]
end
@impl true
def call(conn, []) do
endpoint = conn.private.phoenix_endpoint
options = endpoint.config(:phoenix_playground)
cond do
options[:live] ->
PhoenixPlayground.Router.LiveRouter.call(conn, [])
controller = options[:controller] ->
conn = put_in(conn.private[:phoenix_playground_controller], controller)
PhoenixPlayground.Router.ControllerRouter.call(conn, [])
options[:plug] ->
# always fetch plug from app env to allow code reloading anonymous functions
plug = Application.fetch_env!(:phoenix_playground, :plug)
case plug do
module when is_atom(module) ->
module.call(conn, module.init([]))
{module, options} when is_atom(module) ->
module.call(conn, module.init(options))
fun when is_function(fun, 1) ->
fun.(conn)
fun when is_function(fun, 2) ->
fun.(conn, [])
end
true ->
raise ArgumentError, "expected :live, :controller, or :plug, got: #{inspect(options)}"
end
end
defmodule ControllerRouter do
@moduledoc false
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html", "json"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
get "/", PhoenixPlayground.Router.DelegateController, :index
end
end
defmodule DelegateController do
@moduledoc false
def init(options) do
options
end
def call(conn, options) do
controller = conn.private.phoenix_playground_controller
controller.call(conn, controller.init(options))
end
end
defmodule LiveRouter do
@moduledoc false
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", PhoenixPlayground.Router.DelegateLive, :index
end
end
defmodule DelegateLive do
@moduledoc false
use Phoenix.LiveView
@impl true
def mount(params, session, socket) do
module().mount(params, session, socket)
end
@impl true
def render(assigns) do
module().render(assigns)
end
@impl true
def handle_params(unsigned_params, uri, socket) do
if function_exported?(module(), :handle_params, 3) do
module().handle_params(unsigned_params, uri, socket)
else
{:noreply, socket}
end
end
@impl true
def handle_event(event, params, socket) do
module().handle_event(event, params, socket)
end
@impl true
def handle_info(message, socket) do
module().handle_info(message, socket)
end
@impl true
def handle_async(message, result, socket) do
module().handle_async(message, result, socket)
end
def module do
Application.fetch_env!(:phoenix_playground, :live)
end
end
end
================================================
FILE: lib/phoenix_playground/test.ex
================================================
defmodule PhoenixPlayground.Test do
@moduledoc ~S'''
Conveniences for testing single-file Phoenix apps.
## Examples
Mix.install([
{:phoenix_playground, "~> 0.1.0"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<span>Count: <%= @count %></span> <button phx-click="inc">+</button>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
Logger.configure(level: :info)
ExUnit.start()
defmodule DemoLiveTest do
use ExUnit.Case
use PhoenixPlayground.Test, live: DemoLive
test "it works" do
{:ok, view, html} = live(build_conn(), "/")
assert html =~ "Count: 0"
assert render_click(view, :inc, %{}) =~ "Count: 1"
assert render_click(view, :inc, %{}) =~ "Count: 2"
end
end
'''
@secret_key_base String.duplicate("a", 32)
@signing_salt "ll+Leuc4"
defmacro __using__(options) do
options =
Keyword.validate!(options, [
:live,
:controller,
endpoint: PhoenixPlayground.Endpoint
])
imports =
if options[:live] do
quote do
import(Phoenix.LiveViewTest)
end
end
quote do
import Phoenix.ConnTest
unquote(imports)
@endpoint unquote(options[:endpoint])
setup do
options = unquote(options)
if live = options[:live] do
Application.put_env(:phoenix_playground, :live, live)
end
start_supervised!(
{@endpoint,
secret_key_base: unquote(@secret_key_base),
live_view: [signing_salt: unquote(@signing_salt)],
phoenix_playground: options}
)
:ok
end
end
end
end
================================================
FILE: lib/phoenix_playground.ex
================================================
defmodule PhoenixPlayground do
@moduledoc """
Phoenix Playground makes it easy to create single-file Phoenix applications.
"""
require Logger
@secret_key_base [
then(:inet.gethostname(), fn {:ok, host} -> host end),
System.get_env("USER", ""),
System.version(),
:erlang.system_info(:version),
:erlang.system_info(:system_architecture)
]
|> :erlang.md5()
|> Base.url_encode64(padding: false)
@signing_salt "ll+Leuc4"
@doc """
Starts Phoenix Playground.
This functions starts Phoenix with a LiveView (`:live`), a controller (`:controller`),
or a router (`:router`).
## Options
* `:live` - a LiveView module.
Phoenix Playground adds the following conveniences to the given LiveView:
* a `:page_title` assign can be used to customise `<head>` `<title>` tag.
* a `window.hooks` object can be used to register hooks. See `examples/demo_hooks.exs`.
This LiveView will automatically use `PhoenixPlayground.Layout`.
* `:controller` - a controller module.
This controller will automatically use `PhoenixPlayground.Layout`.
* `:plug` - a plug.
* `:port` - port to listen on, defaults to: `4000`.
* `:endpoint_options` - additional Phoenix endpoint options, defaults to `[]`.
* `:debug_errors` - whether to use Phoenix error debugger, defaults to `true`.
* `:open_browser` - whether to open the browser on start, defaults to `true`.
* `:child_specs` - child specs to run in Phoenix Playground supervision tree. The playground
Phoenix endpoint is automatically added and is always the last child spec. Defaults to `[]`.
"""
def start(options) do
options =
case Application.fetch_env(:phoenix_playground, :file) do
{:ok, file} ->
Keyword.put(options, :file, file)
:error ->
file = get_file()
Application.put_env(:phoenix_playground, :file, file)
Keyword.put(options, :file, file)
end
options =
if router = options[:router] do
IO.warn("setting :router is deprecated in favour of setting :plug")
options
|> Keyword.delete(:router)
|> Keyword.put(:plug, router)
else
options
end
if plug = options[:plug] do
Application.put_env(:phoenix_playground, :plug, plug)
end
case Supervisor.start_child(PhoenixPlayground.Application, {PhoenixPlayground, options}) do
{:ok, pid} ->
{:ok, pid}
{:error, {:already_started, pid}} ->
# In Livebook, path is nil. Livebook does its own code reloading, this just refreshes LV.
unless options[:file] do
Phoenix.PubSub.broadcast(
PhoenixPlayground.PubSub,
"live_view",
{:phoenix_live_reload, :live_view, nil}
)
end
{:ok, pid}
{:error, {{:EXIT, {exception, _trace}}, _child_info}} ->
raise exception
other ->
other
end
end
defp get_file do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
get_file(stacktrace)
end
defp get_file([
{PhoenixPlayground, :start, 1, _},
{_, :__FILE__, 1, meta} | _
]) do
Path.expand(Keyword.fetch!(meta, :file))
end
defp get_file([_ | rest]) do
get_file(rest)
end
defp get_file([]) do
nil
end
@doc false
def child_spec(options) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [options]},
type: :supervisor
}
end
@doc false
def start_link(options) do
options =
Keyword.validate!(options, [
:live,
:controller,
:plug,
:file,
port: 4000,
host: "localhost",
live_reload: true,
ip: {127, 0, 0, 1},
endpoint: PhoenixPlayground.Endpoint,
endpoint_options: [],
debug_errors: true,
open_browser: true,
child_specs: []
])
child_specs = Keyword.fetch!(options, :child_specs)
path = options[:file]
Application.put_env(:phoenix_playground, :file, path)
if options[:debug_errors] do
Application.put_env(:phoenix_playground, :debug_errors, true)
end
if options[:open_browser] do
Application.put_env(:phoenix, :browser_open, true)
end
if live = options[:live] do
Application.put_env(:phoenix_playground, :live, live)
end
# in Livebook, path is nil
if path do
Application.put_env(:phoenix_live_reload, :dirs, [
Path.dirname(path)
])
end
Application.put_env(:phoenix_playground, :live_reload, options[:live_reload])
if options[:live_reload] do
# PhoenixLiveReload requires Hex
{:ok, _} = Application.ensure_all_started(:hex)
{:ok, _} = Application.ensure_all_started(:phoenix_live_reload)
end
live_reload_options =
if options[:live] &&
unquote(
# TODO: remove when depending on LV 1.0.
Version.match?(
to_string(Application.spec(:phoenix_live_view, :vsn)),
">= 1.0.0-rc.1"
)
) do
[
# In Livebook, path is nil
patterns:
if path do
# TODO: this should not be needed given we set :phoenix_live_reload :dirs
[~r/^does_not_matter$/]
else
[]
end,
notify: [
live_view: [
~r/^#{path}$/
]
]
]
else
[
patterns: [
~r/^#{path}$/
]
]
end
lr_options =
if options[:live_reload] do
[
web_console_logger: true,
debounce: 100,
reloader: {PhoenixPlayground.CodeReloader, :reload, []}
] ++ live_reload_options
else
[
notify: [
live_view: []
],
reloader: {PhoenixPlayground.CodeReloader, :reload, []}
]
end
# Some compile-time options are defined at the top of lib/phoenix_playground/endpoint.ex
endpoint_options =
[
adapter: Bandit.PhoenixAdapter,
http: [ip: options[:ip], port: options[:port]],
url: [host: options[:host]],
server: !!options[:port],
live_view: [signing_salt: @signing_salt],
secret_key_base: @secret_key_base,
pubsub_server: PhoenixPlayground.PubSub,
live_reload: lr_options,
phoenix_playground: Keyword.take(options, [:live, :controller, :plug])
]
|> Keyword.merge(Keyword.get(options, :endpoint_options, []))
children =
child_specs ++
[
{Phoenix.PubSub, name: PhoenixPlayground.PubSub},
{Keyword.fetch!(options, :endpoint), endpoint_options}
]
System.no_halt(true)
case Supervisor.start_link(children, strategy: :one_for_one) do
{:ok, pid} ->
{:ok, pid}
{:error, reason} ->
Logger.error(Exception.format_exit(reason))
{:error, reason}
end
end
end
================================================
FILE: mix.exs
================================================
defmodule PhoenixPlayground.MixProject do
use Mix.Project
@source_url "https://github.com/phoenix-playground/phoenix_playground"
@version "0.1.8"
def project do
[
app: :phoenix_playground,
version: @version,
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
package: package(),
aliases: aliases(),
docs: docs(),
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {PhoenixPlayground.Application, []}
]
end
defp package do
[
description: "Phoenix Playground makes it easy to create single-file Phoenix applications.",
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/phoenix_playground/changelog.html"
}
]
end
defp aliases do
[
docs: ["docs.setup", "docs", "docs.teardown"],
"docs.setup": &docs_setup/1,
"docs.teardown": &docs_teardown/1
]
end
defp docs do
[
main: "readme",
source_url: @source_url,
source_ref: "v#{@version}",
extras: [
"README.md",
"CHANGELOG.md"
],
assets: %{"assets" => "assets"},
skip_code_autolink_to: [
"PhoenixPlayground.start_link/1"
]
]
end
defp docs_setup(_) do
tmp = "#{__DIR__}/tmp"
File.mkdir_p!(tmp)
File.cp!("#{__DIR__}/README.md", "#{tmp}/README.md")
File.write!("#{__DIR__}/README.md", [
File.read!("#{__DIR__}/README.md"),
"\n\n",
for path <- Path.wildcard("examples/*.exs") do
"[`#{path}`]: https://github.com/phoenix-playground/phoenix_playground/blob/v#{@version}/#{path}\n"
end
])
end
defp docs_teardown(_) do
tmp = "#{__DIR__}/tmp"
File.rename!("#{tmp}/README.md", "#{__DIR__}/README.md")
end
defp deps do
[
{:phoenix, "~> 1.0"},
{:phoenix_live_view, "~> 1.1"},
{:bandit, "~> 1.0"},
{:jason, "~> 1.0"},
{:lazy_html, "~> 0.1.3"},
# Don't start phoenix_live_reload in case someone just wants PhoenixPlayground.Test.
# Instead, manually start it in PhoenixPlayground.start_link/1.
{:phoenix_live_reload, "~> 1.5", runtime: false},
# dev-only
{:ex_doc, ">= 0.0.0", only: :dev}
]
end
end
================================================
FILE: test/examples_test.exs
================================================
defmodule Examples do
def run(path) do
{:ok, quoted} =
path
|> File.read!()
|> Code.string_to_quoted()
quoted
|> remove_mix_install()
|> Code.eval_quoted([], file: path)
end
defp remove_mix_install(quoted) do
Macro.prewalk(quoted, fn
{{:., _, [{:__aliases__, _, [:Mix]}, :install]}, _, _} ->
nil
ast ->
ast
end)
end
end
Examples.run("#{__DIR__}/../examples/demo_controller_test.exs")
Examples.run("#{__DIR__}/../examples/demo_live_test.exs")
================================================
FILE: test/phoenix_playground_test.exs
================================================
defmodule PhoenixPlaygroundTest do
use ExUnit.Case, async: true
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
gitextract__377zb3b/
├── .formatter.exs
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── examples/
│ ├── demo_controller.exs
│ ├── demo_controller_test.exs
│ ├── demo_endpoint.exs
│ ├── demo_hooks.exs
│ ├── demo_live.exs
│ ├── demo_live.livemd
│ ├── demo_live_pubsub.exs
│ ├── demo_live_test.exs
│ ├── demo_plug.exs
│ └── demo_router.exs
├── lib/
│ ├── phoenix_playground/
│ │ ├── application.ex
│ │ ├── code_reloader.ex
│ │ ├── endpoint.ex
│ │ ├── error_view.ex
│ │ ├── layout.ex
│ │ ├── router.ex
│ │ └── test.ex
│ └── phoenix_playground.ex
├── mix.exs
└── test/
├── examples_test.exs
├── phoenix_playground_test.exs
└── test_helper.exs
SYMBOL INDEX (87 symbols across 19 files)
FILE: examples/demo_controller.exs
class DemoController (line 6) | defmodule DemoController
method index (line 12) | def index(conn, params) do
method index (line 22) | def index(assigns) do
FILE: examples/demo_controller_test.exs
class DemoController (line 6) | defmodule DemoController
method index (line 12) | def index(conn, params) do
method index (line 22) | def index(assigns) do
class DemoControllerTest (line 33) | defmodule DemoControllerTest
FILE: examples/demo_endpoint.exs
class DemoLive (line 6) | defmodule DemoLive
method mount (line 9) | def mount(_params, _session, socket) do
method render (line 13) | def render(assigns) do
method handle_event (line 25) | def handle_event("inc", _params, socket) do
class Demo.Router (line 30) | defmodule Demo.Router
class Demo.Endpoint (line 44) | defmodule Demo.Endpoint
FILE: examples/demo_hooks.exs
class DemoHooks (line 6) | defmodule DemoHooks
method mount (line 9) | def mount(_params, _session, socket) do
method render (line 13) | def render(assigns) do
FILE: examples/demo_live.exs
class DemoLive (line 6) | defmodule DemoLive
method mount (line 9) | def mount(_params, _session, socket) do
method render (line 13) | def render(assigns) do
method handle_event (line 24) | def handle_event("inc", _params, socket) do
FILE: examples/demo_live_pubsub.exs
class TimelineLive (line 5) | defmodule TimelineLive
method mount (line 10) | def mount(_params, _session, socket) do
method handle_event (line 24) | def handle_event("create_post", %{"content" => content}, socket) do
method handle_event (line 38) | def handle_event("delete_post", %{"dom_id" => dom_id}, socket) do
method handle_info (line 44) | def handle_info({:new_post, post}, socket) do
method handle_info (line 48) | def handle_info({:delete_post, dom_id}, socket) do
method render (line 52) | def render(assigns) do
FILE: examples/demo_live_test.exs
class DemoLive (line 6) | defmodule DemoLive
method mount (line 9) | def mount(_params, _session, socket) do
method render (line 13) | def render(assigns) do
method handle_event (line 20) | def handle_event("inc", _params, socket) do
class DemoLiveTest (line 28) | defmodule DemoLiveTest
FILE: examples/demo_router.exs
class CounterLive (line 6) | defmodule CounterLive
method mount (line 9) | def mount(_params, _session, socket) do
method render (line 13) | def render(assigns) do
method handle_event (line 24) | def handle_event("inc", _params, socket) do
class DemoRouter (line 29) | defmodule DemoRouter
FILE: lib/phoenix_playground.ex
class PhoenixPlayground (line 1) | defmodule PhoenixPlayground
method start (line 55) | def start(options) do
method get_file (line 106) | defp get_file do
method get_file (line 111) | defp get_file([
method get_file (line 118) | defp get_file([_ | rest]) do
method get_file (line 122) | defp get_file([]) do
method child_spec (line 127) | def child_spec(options) do
method start_link (line 136) | def start_link(options) do
FILE: lib/phoenix_playground/application.ex
class PhoenixPlayground.Application (line 1) | defmodule PhoenixPlayground.Application
method start (line 7) | def start(_type, _args) do
FILE: lib/phoenix_playground/code_reloader.ex
class PhoenixPlayground.CodeReloader (line 1) | defmodule PhoenixPlayground.CodeReloader
method reload (line 4) | def reload(_endpoint, _options \\ []) do
FILE: lib/phoenix_playground/endpoint.ex
class PhoenixPlayground.Endpoint (line 1) | defmodule PhoenixPlayground.Endpoint
method run_live_reload (line 38) | defp run_live_reload(conn, _options) do
method call (line 59) | def call(conn, opts) do
FILE: lib/phoenix_playground/error_view.ex
class PhoenixPlayground.ErrorView (line 1) | defmodule PhoenixPlayground.ErrorView
method render (line 4) | def render(template, _), do: Phoenix.Controller.status_message_from_te...
FILE: lib/phoenix_playground/layout.ex
class PhoenixPlayground.Layout (line 1) | defmodule PhoenixPlayground.Layout
method render (line 15) | def render(template, assigns)
method render (line 17) | def render("root.html", assigns) do
FILE: lib/phoenix_playground/router.ex
class PhoenixPlayground.Router (line 1) | defmodule PhoenixPlayground.Router
method init (line 7) | def init([]) do
method call (line 12) | def call(conn, []) do
class ControllerRouter (line 48) | defmodule ControllerRouter
class DelegateController (line 69) | defmodule DelegateController
method init (line 72) | def init(options) do
method call (line 76) | def call(conn, options) do
class LiveRouter (line 82) | defmodule LiveRouter
class DelegateLive (line 102) | defmodule DelegateLive
method mount (line 107) | def mount(params, session, socket) do
method render (line 112) | def render(assigns) do
method handle_params (line 117) | def handle_params(unsigned_params, uri, socket) do
method handle_event (line 126) | def handle_event(event, params, socket) do
method handle_info (line 131) | def handle_info(message, socket) do
method handle_async (line 136) | def handle_async(message, result, socket) do
method module (line 140) | def module do
FILE: lib/phoenix_playground/test.ex
class PhoenixPlayground.Test (line 1) | defmodule PhoenixPlayground.Test
FILE: mix.exs
class PhoenixPlayground.MixProject (line 1) | defmodule PhoenixPlayground.MixProject
method project (line 7) | def project do
method application (line 20) | def application do
method package (line 27) | defp package do
method aliases (line 38) | defp aliases do
method docs (line 46) | defp docs do
method docs_setup (line 62) | defp docs_setup(_) do
method docs_teardown (line 76) | defp docs_teardown(_) do
method deps (line 81) | defp deps do
FILE: test/examples_test.exs
class Examples (line 1) | defmodule Examples
method run (line 2) | def run(path) do
method remove_mix_install (line 13) | defp remove_mix_install(quoted) do
FILE: test/phoenix_playground_test.exs
class PhoenixPlaygroundTest (line 1) | defmodule PhoenixPlaygroundTest
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".formatter.exs",
"chars": 133,
"preview": "# Used by \"mix format\"\n[\n import_deps: [:phoenix],\n inputs: [\"{mix,.formatter}.exs\", \"{config,examples,lib,test}/**/*."
},
{
"path": ".github/workflows/ci.yml",
"chars": 1130,
"preview": "name: CI\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-22.04\n env:\n MIX_ENV: test\n strategy:\n "
},
{
"path": ".gitignore",
"chars": 640,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": "CHANGELOG.md",
"chars": 1322,
"preview": "# Changelog\n\n## v0.1.8 (2025-07-30)\n\n * Use Phoenix LiveView v1.1.\n\n## v0.1.7 (2024-09-24)\n\n * Delegate LiveView `hand"
},
{
"path": "README.md",
"chars": 2914,
"preview": "# Phoenix Playground\n\n[\n\ndefmodule DemoController do\n use Phoenix.Co"
},
{
"path": "examples/demo_controller_test.exs",
"chars": 944,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoController do\n use Phoenix.Co"
},
{
"path": "examples/demo_endpoint.exs",
"chars": 1393,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n use Phoenix.LiveView"
},
{
"path": "examples/demo_hooks.exs",
"chars": 596,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoHooks do\n use Phoenix.LiveVie"
},
{
"path": "examples/demo_live.exs",
"chars": 554,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n use Phoenix.LiveView"
},
{
"path": "examples/demo_live.livemd",
"chars": 619,
"preview": "# Demo\n\n```elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n```\n\n## Section\n\n```elixir\ndefmodule DemoLive do\n"
},
{
"path": "examples/demo_live_pubsub.exs",
"chars": 5034,
"preview": "Mix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule TimelineLive do\n use Phoenix.LiveView\n\n @topic \"timeli"
},
{
"path": "examples/demo_live_test.exs",
"chars": 796,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n use Phoenix.LiveView"
},
{
"path": "examples/demo_plug.exs",
"chars": 173,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\nPhoenixPlayground.start(\n plug: fn conn ->\n"
},
{
"path": "examples/demo_router.exs",
"chars": 903,
"preview": "#!/usr/bin/env elixir\nMix.install([\n {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule CounterLive do\n use Phoenix.LiveV"
},
{
"path": "lib/phoenix_playground/application.ex",
"chars": 423,
"preview": "defmodule PhoenixPlayground.Application do\n @moduledoc false\n\n use Application\n\n @impl true\n def start(_type, _args)"
},
{
"path": "lib/phoenix_playground/code_reloader.ex",
"chars": 705,
"preview": "defmodule PhoenixPlayground.CodeReloader do\n @moduledoc false\n\n def reload(_endpoint, _options \\\\ []) do\n if path ="
},
{
"path": "lib/phoenix_playground/endpoint.ex",
"chars": 6691,
"preview": "defmodule PhoenixPlayground.Endpoint do\n @moduledoc false\n\n use Phoenix.Endpoint, otp_app: :phoenix_playground\n\n @sig"
},
{
"path": "lib/phoenix_playground/error_view.ex",
"chars": 154,
"preview": "defmodule PhoenixPlayground.ErrorView do\n @moduledoc false\n\n def render(template, _), do: Phoenix.Controller.status_me"
},
{
"path": "lib/phoenix_playground/layout.ex",
"chars": 2738,
"preview": "defmodule PhoenixPlayground.Layout do\n @moduledoc \"\"\"\n Built-in layout.\n\n This is the layout used by `PhoenixPlaygrou"
},
{
"path": "lib/phoenix_playground/router.ex",
"chars": 3345,
"preview": "defmodule PhoenixPlayground.Router do\n @moduledoc false\n\n @behaviour Plug\n\n @impl true\n def init([]) do\n []\n end"
},
{
"path": "lib/phoenix_playground/test.ex",
"chars": 1976,
"preview": "defmodule PhoenixPlayground.Test do\n @moduledoc ~S'''\n Conveniences for testing single-file Phoenix apps.\n\n ## Exampl"
},
{
"path": "lib/phoenix_playground.ex",
"chars": 7194,
"preview": "defmodule PhoenixPlayground do\n @moduledoc \"\"\"\n Phoenix Playground makes it easy to create single-file Phoenix applica"
},
{
"path": "mix.exs",
"chars": 2308,
"preview": "defmodule PhoenixPlayground.MixProject do\n use Mix.Project\n\n @source_url \"https://github.com/phoenix-playground/phoeni"
},
{
"path": "test/examples_test.exs",
"chars": 524,
"preview": "defmodule Examples do\n def run(path) do\n {:ok, quoted} =\n path\n |> File.read!()\n |> Code.string_to_qu"
},
{
"path": "test/phoenix_playground_test.exs",
"chars": 70,
"preview": "defmodule PhoenixPlaygroundTest do\n use ExUnit.Case, async: true\nend\n"
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the phoenix-playground/phoenix_playground GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (43.0 KB), approximately 14.2k tokens, and a symbol index with 87 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.