Last active
June 6, 2025 05:14
-
-
Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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