Loading...
Loading...
Phoenix Framework with LiveView on the BEAM
npx skill4agent add bobmatnyc/claude-mpm-skills phoenix-liveview# Erlang + Elixir via asdf (recommended)
asdf install erlang 27.0
asdf install elixir 1.17.3
asdf global erlang 27.0 elixir 1.17.3
# Install Phoenix generator
mix archive.install hex phx_new
# Create project with LiveView + Ecto + esbuild
mix phx.new my_app --live
cd my_app
mix deps.get
mix ecto.create
mix phx.serverlib/my_app/application.exlib/my_app_web/endpoint.exlib/my_app_web/router.exlib/my_app/test/support/{conn_case,data_case}.exdef start(_type, _args) do
children = [
MyApp.Repo,
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint,
{Oban, Application.fetch_env!(:my_app, Oban)},
MyApp.Metrics
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
enddefmodule MyApp.Counter do
use GenServer
def start_link(initial \\ 0), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)
def increment(), do: GenServer.call(__MODULE__, :inc)
@impl true
def handle_call(:inc, _from, state) do
new_state = state + 1
{:reply, new_state, new_state}
end
endGenServer.castsenddefmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive
resources "/users", UserController
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", Api.UserController, except: [:new, :edit]
end
enddefmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :hashed_password, :string
field :confirmed_at, :naive_datetime
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 12)
|> unique_constraint(:email)
|> put_password_hash()
end
defp put_password_hash(%{valid?: true} = changeset),
do: put_change(changeset, :hashed_password, Argon2.hash_pwd_salt(get_change(changeset, :password)))
defp put_password_hash(changeset), do: changeset
enddefmodule MyApp.Accounts do
import Ecto.Query, warn: false
alias MyApp.{Repo, Accounts.User}
def list_users, do: Repo.all(User)
def get_user!(id), do: Repo.get!(User, id)
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
endalias Ecto.Multi
def register_and_welcome(attrs) do
Multi.new()
|> Multi.insert(:user, User.registration_changeset(%User{}, attrs))
|> Multi.run(:welcome_email, fn _repo, %{user: user} ->
MyApp.Mailer.deliver_welcome(user)
{:ok, :sent}
end)
|> Repo.transaction()
enddefmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<div class="space-y-4">
<p class="text-lg">Count: <%= @count %></p>
<button phx-click="inc" class="btn">Increment</button>
</div>
"""
end
endassign_new/3stream/3handle_params/3defmodule MyAppWeb.NavComponent do
use MyAppWeb, :live_component
def render(assigns) do
~H"""
<nav>
<%= for item <- @items do %>
<.link navigate={item.href}><%= item.label %></.link>
<% end %>
</nav>
"""
end
end@impl true
def mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "orders")
{:ok, assign(socket, orders: [])}
end
@impl true
def handle_info({:order_created, order}, socket) do
{:noreply, update(socket, :orders, fn orders -> [order | orders] end)}
enddef create_order(attrs) do
with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_created, order})
{:ok, order}
end
enddefmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
alias Phoenix.Presence
def join("room:" <> room_id, _payload, socket) do
send(self(), :after_join)
{:ok, assign(socket, :room_id, room_id)}
end
def handle_info(:after_join, socket) do
Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
endjoin/3mix test# test/support/conn_case.ex
use MyAppWeb.ConnCase, async: true
test "renders home", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome"
end# LiveView test
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
test "counter increments", %{conn: conn} do
{:ok, view, _html} = live(conn, "/counter")
view |> element("button", "Increment") |> render_click()
assert render(view) =~ "Count: 1"
endExMachinatest/support/fixtures[:phoenix, :endpoint, ...]:telemetry_pollerOpentelemetryPhoenixOpentelemetryEctomix assets.deployMIX_ENV=prod mix releaseconfig/runtime.exsPHX_SERVER=true _build/prod/rel/my_app/bin/my_app startlibcluster:telemetryconnected?/1Ecto.Multimix phx.routesmix phx.gen.live Accounts User users email:string confirmed_at:naive_datetimemix format && mix credo --strictmix test --seed 0 --max-failures 1mix test.watch