Skip to content

Instantly share code, notes, and snippets.

@sukidhar
Created April 26, 2026 09:31
Show Gist options
  • Select an option

  • Save sukidhar/63bb5a47e8fe34af2f4c81e95b5413df to your computer and use it in GitHub Desktop.

Select an option

Save sukidhar/63bb5a47e8fe34af2f4c81e95b5413df to your computer and use it in GitHub Desktop.
Single file Phoenix app
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode16()
secret_base = :crypto.strong_rand_bytes(32) |> Base.encode16()
host = "localhost"
Application.put_env(:phoenix, :json_library, Jason)
Application.put_env(:sample, Sample.Endpoint,
adapter: Bandit.PhoenixAdapter,
url: [host: host],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
],
server: true,
live_view: [signing_salt: signing_salt],
secret_key_base: secret_base,
pubsub_server: Sample.PubSub
)
Mix.install([
{:bandit, "~> 1.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.8"},
{:phoenix_live_view, path: "../phoenix_live_view"}
])
defmodule Sample.ErrorView do
def render(_, _), do: "error"
end
defmodule Sample.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
@upload_dir Path.expand("uploads", __DIR__)
def mount(_params, _session, socket) do
File.mkdir_p!(@upload_dir)
socket =
socket
|> assign(:uploaded_files, [])
|> allow_upload(:files, accept: :any, max_entries: 5, max_file_size: 10_000_000)
{:ok, socket}
end
def live(assigns) do
~H"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
<style>
* { font-size: 1.1em; }
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; }
button { margin: 0.25rem; padding: 0.5rem 1rem; cursor: pointer; }
.drop-zone { border: 2px dashed #aaa; border-radius: 8px; padding: 2rem; text-align: center; margin: 1rem 0; transition: border-color 0.2s; }
.drop-zone.active { border-color: #29d; background: #f0f8ff; }
.entry { display: flex; align-items: center; gap: 1rem; padding: 0.5rem 0; }
.progress { width: 100px; height: 8px; background: #eee; border-radius: 4px; overflow: hidden; }
.progress-bar { height: 100%; background: #29d; transition: width 0.2s; }
.error { color: #c33; font-size: 0.9em; }
.file-list { margin-top: 1rem; }
.file-list li { padding: 0.25rem 0; }
</style>
</head>
<body>
{@inner_content}
<script src="/assets/phoenix.js"></script>
<script src="/assets/phoenix_live_view.js"></script>
<script>
let Hooks = {}
Hooks.FileEntry = {
mounted() {
const ref = this.el.dataset.ref
const inputId = this.el.dataset.inputId
const input = document.getElementById(inputId)
console.log("FileEntry mounted, ref:", ref, "inputId:", inputId, "input:", input)
this.el.querySelector("[data-role='ref-display']").textContent = LiveView.getFileURLForUpload(input, ref)
},
updated() {
const ref = this.el.dataset.ref
const inputId = this.el.dataset.inputId
const input = document.getElementById(inputId)
console.log("FileEntry updated, ref:", ref, "inputId:", inputId, "input:", input)
this.el.querySelector("[data-role='ref-display']").textContent = LiveView.getFileURLForUpload(input, ref)
}
}
let liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, {hooks: Hooks})
liveSocket.connect()
</script>
</body>
</html>
"""
end
def render(assigns) do
~H"""
<h1>File Uploader</h1>
<form id="upload-form" phx-submit="save" phx-change="validate">
<div id="drop-zone" class="drop-zone" phx-drop-target={@uploads.files.ref}>
<p>Drag & drop files here or</p>
<.live_file_input upload={@uploads.files} />
</div>
<div :for={entry <- @uploads.files.entries} id={"file-#{entry.ref}"} phx-hook="FileEntry" data-ref={entry.ref} data-input-id={@uploads.files.ref} class="entry">
<span>{entry.client_name}</span>
<span>{entry.progress}%</span>
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref}>&times;</button>
<p :for={err <- upload_errors(@uploads.files, entry)} class="error">{inspect(err)}</p>
<p data-role="ref-display"></p>
</div>
<button :if={@uploads.files.entries != []} type="submit">Upload</button>
</form>
<div :if={@uploaded_files != []} class="file-list">
<h2>Uploaded</h2>
<ul>
<li :for={file <- @uploaded_files}>{file}</li>
</ul>
</div>
"""
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :files, fn %{path: path}, entry ->
dest = Path.join(@upload_dir, entry.client_name)
File.cp!(path, dest)
{:ok, entry.client_name}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :files, ref)}
end
end
defmodule Sample.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, false
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", Sample.HomeLive
end
end
defmodule Sample.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket "/live", Phoenix.LiveView.Socket
plug Plug.Static,
at: "/assets",
from: {:phoenix, "priv/static"},
only: ~w(phoenix.js phoenix.min.js)
plug Plug.Static,
at: "/assets",
from: {:phoenix_live_view, "priv/static"},
only: ~w(phoenix_live_view.js phoenix_live_view.min.js)
plug Plug.Session,
store: :cookie,
key: "_sample_key",
signing_salt: "sample"
plug Sample.Router
end
if System.get_env("EXS_DRY_RUN") == "true" do
System.halt(0)
else
{:ok, _} = Supervisor.start_link([
{Phoenix.PubSub, name: Sample.PubSub},
Sample.Endpoint
], strategy: :one_for_one)
Process.sleep(:infinity)
end
@sukidhar

Copy link
Copy Markdown
Author

I have used the sample from here and customized it to work with local phoenix liveview clone which happens to be on sibling folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment