Last active
May 7, 2025 02:53
-
-
Save Wigny/53c433a89456f3bfbe3df14b4b2c07be 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
Mix.install([ | |
{:bandit, "~> 1.6"}, | |
{:phoenix, "~> 1.7.0"}, | |
{:phoenix_live_view, "~> 1.0.10"}, | |
{:phoenix_ecto, "~> 4.6"}, | |
{:ecto_sql, "~> 3.12"}, | |
{:postgrex, ">= 0.0.0"}, | |
{:testcontainers, "~> 1.12"} | |
]) | |
defmodule Example.Migration0 do | |
use Ecto.Migration | |
def change do | |
create table(:email_notifications) do | |
add(:name, :string) | |
add(:email, :string) | |
end | |
create(unique_index(:email_notifications, [:email])) | |
end | |
end | |
defmodule Example.Repo do | |
use Ecto.Repo, | |
otp_app: :example, | |
adapter: Ecto.Adapters.Postgres | |
end | |
defmodule Example.EmailNotification do | |
use Ecto.Schema | |
import Ecto.Changeset | |
schema "email_notifications" do | |
field :email, :string | |
field :name, :string | |
field :delete, :boolean, default: false, virtual: true | |
end | |
def changeset(email_notification, attrs) do | |
changeset = | |
email_notification | |
|> cast(attrs, [:email, :name, :delete]) | |
|> validate_required([:email, :name]) | |
|> unique_constraint(:email) | |
if get_change(changeset, :delete) do | |
%{changeset | action: :delete} | |
else | |
changeset | |
end | |
end | |
end | |
defmodule Example.ErrorHTML do | |
def render(template, _assigns), do: Phoenix.Controller.status_message_from_template(template) | |
end | |
defmodule Example.Layouts do | |
use Phoenix.Component | |
def render("root.html", assigns) do | |
~H""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<script | |
src={"https://unpkg.com/phoenix@#{Application.spec(:phoenix, :vsn)}/priv/static/phoenix.min.js"} | |
> | |
</script> | |
<script | |
src={"https://unpkg.com/phoenix_live_view@#{Application.spec(:phoenix_live_view, :vsn)}/priv/static/phoenix_live_view.min.js"} | |
> | |
</script> | |
<script type="module"> | |
let liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket) | |
liveSocket.connect() | |
</script> | |
</head> | |
<body>{@inner_content}</body> | |
</html> | |
""" | |
end | |
def render("app.html", assigns) do | |
~H""" | |
<main>{@inner_content}</main> | |
""" | |
end | |
end | |
defmodule Example.HomeLive do | |
use Phoenix.LiveView, layout: {Example.Layouts, :app} | |
alias Phoenix.LiveView.JS | |
defmodule MailingList do | |
use Ecto.Schema | |
import Ecto.Changeset | |
alias Example.EmailNotification | |
@primary_key false | |
embedded_schema do | |
embeds_many :emails, EmailNotification | |
end | |
def get do | |
emails = Example.Repo.all(EmailNotification) | |
new(emails) | |
end | |
def new(emails) do | |
%__MODULE__{emails: emails} | |
end | |
def changeset(mailing_list, attrs \\ %{}) do | |
mailing_list | |
|> cast(attrs, []) | |
|> cast_embed(:emails) | |
end | |
def delete_email_at(changeset, index) do | |
existing = get_embed(changeset, :emails) | |
{email, emails} = List.pop_at(existing, index) | |
emails = | |
if email.data.id do | |
delete = get_field(email, :delete) | |
change = EmailNotification.changeset(email, %{delete: not delete}) | |
List.replace_at(existing, index, change) | |
else | |
emails | |
end | |
put_embed(changeset, :emails, emails) | |
end | |
def append_email(changeset, email) do | |
existing = get_embed(changeset, :emails) | |
put_embed(changeset, :emails, existing ++ [email]) | |
end | |
def save(changeset) do | |
changes = get_embed(changeset, :emails) | |
case apply_changesets(changes) do | |
{:ok, emails} -> {:ok, new(Enum.reject(emails, &is_nil/1))} | |
{:error, changes} -> {:error, put_embed(changeset, :emails, changes)} | |
end | |
end | |
defp apply_changesets(changesets) do | |
multi = | |
changesets | |
|> Enum.with_index() | |
|> Enum.reduce(Ecto.Multi.new(), &add_changeset_operation/2) | |
case Example.Repo.transaction(multi) do | |
{:ok, changes} -> | |
{:ok, Map.values(changes)} | |
{:error, {:email, index}, changeset, _changes} -> | |
{:error, List.replace_at(changesets, index, changeset)} | |
end | |
end | |
defp add_changeset_operation({%{action: :delete} = changeset, index}, multi) do | |
Ecto.Multi.run(multi, {:email, index}, fn repo, _changes -> | |
with {:ok, _entry} <- repo.delete(changeset), do: {:ok, nil} | |
end) | |
end | |
defp add_changeset_operation({changeset, index}, multi) do | |
Ecto.Multi.insert_or_update(multi, {:email, index}, changeset) | |
end | |
end | |
def render(assigns) do | |
~H""" | |
<style> | |
.wrapper[data-delete="true"] { | |
border: 1px solid red; | |
} | |
</style> | |
<.form :let={f} for={@form} phx-change="validate" phx-submit="save" class="w-2/3"> | |
<.inputs_for :let={ef} field={f[:emails]}> | |
<div class="wrapper" data-delete={to_string(ef[:delete].value)}> | |
<input type="hidden" name={ef[:delete].name} value={to_string(ef[:delete].value)} /> | |
<div> | |
<label>Email</label> | |
<input | |
type="email" | |
id={ef[:email].id} | |
name={ef[:email].name} | |
value={ef[:email].value} | |
placeholder="email" | |
/> | |
<span :for={{msg, _opts} <- ef[:email].errors} :if={used_input?(ef[:email])}> | |
{msg} | |
</span> | |
</div> | |
<div> | |
<label>Name</label> | |
<input | |
type="text" | |
id={ef[:name].id} | |
name={ef[:name].name} | |
value={ef[:name].value} | |
placeholder="name" | |
/> | |
<span :for={{msg, _opts} <- ef[:name].errors} :if={used_input?(ef[:name])}> | |
{msg} | |
</span> | |
</div> | |
<button type="button" phx-click={JS.push("delete-email", value: %{index: ef.index})}> | |
delete | |
</button> | |
</div> | |
</.inputs_for> | |
<button type="button" phx-click="add-email"> | |
add more | |
</button> | |
<button type="submit"> | |
save | |
</button> | |
</.form> | |
""" | |
end | |
def mount(_params, _session, socket) do | |
mailing_list = MailingList.get() | |
changeset = MailingList.changeset(mailing_list) | |
{:ok, | |
socket | |
|> assign(:mailing_list, mailing_list) | |
|> assign(:form, to_form(changeset))} | |
end | |
def handle_event("validate", %{"mailing_list" => params}, socket) do | |
changeset = MailingList.changeset(socket.assigns.mailing_list, params) | |
{:noreply, assign(socket, :form, to_form(changeset, action: :validate))} | |
end | |
def handle_event("add-email", _params, socket) do | |
add_email = fn form -> | |
changeset = MailingList.append_email(form.source, %{}) | |
to_form(changeset, action: :add_email) | |
end | |
{:noreply, update(socket, :form, add_email)} | |
end | |
def handle_event("delete-email", %{"index" => index}, socket) do | |
delete_email = fn form -> | |
changeset = MailingList.delete_email_at(form.source, index) | |
to_form(changeset, action: :delete_email) | |
end | |
{:noreply, update(socket, :form, delete_email)} | |
end | |
def handle_event("save", %{"mailing_list" => params}, socket) do | |
changeset = MailingList.changeset(socket.assigns.mailing_list, params) | |
case MailingList.save(changeset) do | |
{:ok, mailing_list} -> | |
{:noreply, | |
socket | |
|> assign(:mailing_list, mailing_list) | |
|> assign(:form, to_form(MailingList.changeset(mailing_list)))} | |
{:error, changeset} -> | |
{:noreply, assign(socket, :form, to_form(changeset, action: :save))} | |
end | |
end | |
end | |
defmodule Example.Router do | |
use Phoenix.Router | |
import Phoenix.LiveView.Router | |
pipeline :browser do | |
plug :accepts, ["html"] | |
plug :put_root_layout, {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, _testcontainer} = Testcontainers.start_link() | |
{:ok, container} = Testcontainers.start_container(Testcontainers.PostgresContainer.new()) | |
Application.put_env( | |
:example, | |
Example.Repo, | |
Testcontainers.PostgresContainer.connection_parameters(container) | |
) | |
Application.put_env(:example, Example.Endpoint, | |
http: [ip: {127, 0, 0, 1}, port: 5001], | |
server: true, | |
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(:phoenix, :json_library, JSON) | |
{:ok, _} = | |
Supervisor.start_link( | |
[ | |
{Ecto.Migrator, | |
migrator: &Ecto.Migrator.run(&1, [{0, Example.Migration0}], &2, &3), | |
repos: [Example.Repo], | |
log_migrations_sql: :debug}, | |
Example.Repo, | |
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