Skip to content

Instantly share code, notes, and snippets.

@Wigny
Last active June 6, 2025 05:14
Show Gist options
  • Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
Application.put_env(:example, Example.Endpoint,
server: true,
http: [ip: {127, 0, 0, 1}, port: 4001],
adapter: Bandit.PhoenixAdapter,
render_errors: [formats: [html: Example.ErrorHTML], layout: false],
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Application.put_env(:ex_money, :default_cldr_backend, Example.Cldr)
Mix.install([
{:bandit, "~> 1.6"},
{:phoenix, "~> 1.8.0-rc.2", override: true},
{:phoenix_live_view, "~> 1.0.10"},
{:phoenix_html, "~> 4.2"},
{:phoenix_ecto, "~> 4.6"},
{:ecto, "~> 3.12"},
{:ex_cldr, "~> 2.42"},
{:ex_cldr_numbers, "~> 2.35"},
{:ex_cldr_currencies, "~> 2.16"},
{:ex_money, "~> 5.21", runtime: false},
{:ex_money_sql, "~> 1.11"}
])
defmodule Example.ErrorHTML do
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
defmodule Example.Layouts do
use Phoenix.Component
def render("root.html", assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Example</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script
defer
type="text/javascript"
src={"https://cdn.jsdelivr.net/npm/phoenix@#{Application.spec(:phoenix, :vsn)}/priv/static/phoenix.min.js"}
>
</script>
<script
defer
type="text/javascript"
src={"https://cdn.jsdelivr.net/npm/phoenix_live_view@#{Application.spec(:phoenix_live_view, :vsn)}/priv/static/phoenix_live_view.min.js"}
>
</script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4">
</script>
<script type="module">
const liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket);
liveSocket.connect();
</script>
</head>
<body>
{@inner_content}
</body>
</html>
"""
end
slot :inner_block, required: true
def app(assigns) do
~H"""
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
"""
end
end
defmodule Example.Cldr do
use Cldr,
locales: ["en", "pt"],
default_locale: "pt",
providers: [Cldr.Number, Money]
end
defmodule Example.CoreComponents do
use Phoenix.Component
def input(%{field: %Phoenix.HTML.FormField{} = field, type: "text"} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns =
assigns
|> assign(field: nil)
|> assign_new(:id, fn -> field.id end)
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|> assign_new(:name, fn -> field.name end)
|> assign_new(:value, fn -> field.value end)
~H"""
<fieldset class="fieldset mb-2">
<label>
<span :if={@label} class="fieldset-label mb-1">{@label}</span>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"w-full input",
@errors != [] && "input-error"
]}
/>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</fieldset>
"""
end
def input(%{field: %Phoenix.HTML.FormField{} = field, type: "money"} = assigns) do
errors = if nested_used_input?(field, :amount), do: field.errors, else: []
assigns =
assigns
|> assign(:field, nil)
|> assign(:errors, Enum.map(errors, &translate_error/1))
|> assign_new(:id, fn -> field.id end)
|> assign_new(:name, fn -> field.name end)
|> assign_new(:value, fn -> field.value end)
|> update(:value, &money_value/1)
|> assign_new(:default_currency, fn ->
backend = Money.default_backend()
Cldr.Currency.current_currency_from_locale(backend.get_locale())
end)
~H"""
<div class="fieldset mb-2">
<div>
<label :if={@label} class="fieldset-label mb-1">{@label}</label>
<div class="join w-full">
<div class="w-full">
<input
type="text"
name={"#{@name}[amount]"}
id={"#{@id}_amount"}
value={@value[:amount]}
inputmode="numeric"
placeholder="0.00"
class={["input join-item w-full", @errors != [] && "input-error"]}
/>
</div>
<select id={"#{@id}_currency"} name={"#{@name}[currency]"} class={["select join-item w-fit", @errors != [] && "select-error"]}>
{Phoenix.HTML.Form.options_for_select(
Money.known_currencies(),
@value[:currency] || @default_currency
)}
</select>
</div>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</div>
"""
end
defp money_value(nil) do
nil
end
defp money_value(%Money{} = money) do
%{amount: money.amount, currency: money.currency}
end
defp money_value(%{"amount" => amount, "currency" => currency}) do
%{amount: amount, currency: currency}
end
defp error(assigns) do
~H"""
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
{render_slot(@inner_block)}
</p>
"""
end
def translate_error({msg, opts}) do
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
def nested_used_input?(%Phoenix.HTML.FormField{field: field, form: form}, subkey) do
nested_params = Map.get(form.params, to_string(field), %{})
not Map.has_key?(nested_params, "_unused_#{subkey}")
end
end
defmodule Example.HomeLive do
use Phoenix.LiveView
import Example.CoreComponents
alias Example.Layouts
defmodule Data do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :name, :string
field :price, Money.Ecto.Map.Type
end
def change(data, attrs) do
data
|> cast(attrs, [:price, :name])
|> validate_required([:price, :name])
end
end
def render(assigns) do
~H"""
<Layouts.app>
<.form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label="Name" placeholder="aa"/>
<.input type="money" field={@form[:price]} label="Price" />
<footer>
<button type="submit" phx-disable-with="Saving..." class="btn btn-primary">
Save
</button>
</footer>
</.form>
</Layouts.app>
"""
end
def mount(_params, _session, socket) do
data = %Data{}
changeset = Data.change(data, %{})
{:ok,
socket
|> assign(:data, data)
|> assign(:form, to_form(changeset))}
end
def handle_event("validate", %{"data" => data_params}, socket) do
changeset = Data.change(socket.assigns.data, data_params)
{:noreply, assign(socket, :form, to_form(changeset, action: :validate))}
end
def handle_event("save", %{"data" => data_params}, socket) do
changeset = Data.change(socket.assigns.data, data_params)
case Ecto.Changeset.apply_action(changeset, :insert) do
{:ok, data} -> {:noreply, assign(socket, :data, data)}
{:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset))}
end
end
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :put_root_layout, html: {Example.Layouts, :root}
end
scope "/", Example do
pipe_through :browser
live "/", HomeLive, :index
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :example
socket "/live", Phoenix.LiveView.Socket
plug Example.Router
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment