Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inputs_for for liveview #52

Merged
merged 13 commits into from
Jul 14, 2022
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
use Mix.Config
import Config

import_config "#{Mix.env()}.exs"
4 changes: 3 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use Mix.Config
import Config

config :logger, level: :warn

config :phoenix, :json_library, Jason

config :polymorphic_embed,
ecto_repos: [PolymorphicEmbed.Repo]

Expand Down
136 changes: 121 additions & 15 deletions lib/html/form.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,119 @@
if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) do
defmodule PolymorphicEmbed.HTML.Form do
import Phoenix.HTML, only: [html_escape: 1]
import Phoenix.HTML.Form, only: [hidden_inputs_for: 1]
import Phoenix.HTML.Form, only: [hidden_inputs_for: 1, input_value: 2]

def polymorphic_embed_inputs_for(form, field, type, fun)
@doc """
Returns the polymorphic type of the given field in the given form data.
"""
def get_polymorphic_type(%Phoenix.HTML.Form{} = form, schema, field) do
case input_value(form, field) do
%Ecto.Changeset{data: value} ->
PolymorphicEmbed.get_polymorphic_type(schema, field, value)

%_{} = value ->
PolymorphicEmbed.get_polymorphic_type(schema, field, value)

%{"__type__" => type} ->
maybe_to_existing_atom(type)

%{__type__: type} ->
maybe_to_existing_atom(type)

_ ->
nil
end
end

defp maybe_to_existing_atom(type) when is_binary(type), do: String.to_existing_atom(type)
defp maybe_to_existing_atom(type) when is_atom(type), do: type

@doc """
Generates a new form builder without an anonymous function.

Similarly to `Phoenix.HTML.Form.inputs_for/3`, this function exists for
integration with `Phoenix.LiveView`.

Unlike `polymorphic_embed_inputs_for/4`, this function does not generate
hidden inputs.

## Example

<.form
let={f}
for={@changeset}
id="reminder-form"
phx-change="validate"
phx-submit="save"
>
<%= for channel_form <- polymorphic_embed_inputs_for f, :channel do %>
<%= hidden_inputs_for(channel_form) %>

<%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
<% :sms -> %>
<%= label channel_form, :number %>
<%= text_input channel_form, :number %>

<% :email -> %>
<%= label channel_form, :email %>
<%= text_input channel_form, :email %>
<% end %>
</.form>
"""
def polymorphic_embed_inputs_for(form, field)
when is_atom(field) or is_binary(field) do
options = Keyword.take(form.options, [:multipart])
%schema{} = form.source.data
type = get_polymorphic_type(form, schema, field)
to_form(form.source, form, field, type, options)
end

@doc """
Like `polymorphic_embed_inputs_for/4`, but determines the type from the
form data.

## Example

<%= inputs_for f, :reminders, fn reminder_form -> %>
<%= polymorphic_embed_inputs_for reminder_form, :channel, fn channel_form -> %>
<%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
<% :sms -> %>
<%= label poly_form, :number %>
<%= text_input poly_form, :number %>

<% :email -> %>
<%= label poly_form, :email %>
<%= text_input poly_form, :email %>
<% end %>
<% end %>
<% end %>

While `polymorphic_embed_inputs_for/4` renders empty fields if the data is
`nil`, this function does not. Instead, you can initialize your changeset
to render an empty fieldset:

changeset = reminder_changeset(
%Reminder{},
%{"channel" => %{"__type__" => "sms"}}
)
"""
def polymorphic_embed_inputs_for(form, field, fun)
when is_atom(field) or is_binary(field) do
options =
form.options
|> Keyword.take([:multipart])
options = Keyword.take(form.options, [:multipart])
%schema{} = form.source.data
type = get_polymorphic_type(form, schema, field)
forms = to_form(form.source, form, field, type, options)

html_escape(
Enum.map(forms, fn form ->
[hidden_inputs_for(form), fun.(form)]
end)
)
end

def polymorphic_embed_inputs_for(form, field, type, fun)
when is_atom(field) or is_binary(field) do
options = Keyword.take(form.options, [:multipart])
forms = to_form(form.source, form, field, type, options)

html_escape(
Expand All @@ -31,19 +136,19 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
params = Enum.at(params, i) || %{}

changeset =
Ecto.Changeset.change(data)
data
|> Ecto.Changeset.change()
|> apply_action(parent_action)

errors = get_errors(changeset)

changeset =
%Ecto.Changeset{
changeset
| action: parent_action,
params: params,
errors: errors,
valid?: errors == []
}
changeset = %Ecto.Changeset{
changeset
| action: parent_action,
params: params,
errors: errors,
valid?: errors == []
}

%Phoenix.HTML.Form{
source: changeset,
Expand All @@ -65,7 +170,8 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d

case Map.get(struct, field) do
nil ->
struct(PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type))
module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
if module, do: struct(module), else: []

data ->
data
Expand Down
24 changes: 13 additions & 11 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule PolymorphicEmbed do
end

types_metadata =
Keyword.fetch!(opts, :types)
opts
|> Keyword.fetch!(:types)
|> Enum.map(fn
{type_name, type_opts} when is_list(type_opts) ->
{type_name, type_opts}
Expand All @@ -24,10 +25,10 @@ defmodule PolymorphicEmbed do
|> Enum.map(fn
{type_name, type_opts} ->
%{
type: type_name |> to_string(),
type: to_string(type_name),
module: Keyword.fetch!(type_opts, :module),
identify_by_fields:
Keyword.get(type_opts, :identify_by_fields, []) |> Enum.map(&to_string/1)
type_opts |> Keyword.get(:identify_by_fields, []) |> Enum.map(&to_string/1)
}
end)

Expand Down Expand Up @@ -254,7 +255,8 @@ defmodule PolymorphicEmbed do

def load(data, loader, params) when is_map(data), do: do_load(data, loader, params)

def load(data, loader, params) when is_binary(data), do: do_load(Jason.decode!(data), loader, params)
def load(data, loader, params) when is_binary(data),
do: do_load(Jason.decode!(data), loader, params)

def do_load(data, _loader, %{types_metadata: types_metadata, type_field: type_field}) do
case do_get_polymorphic_module_from_map(data, type_field, types_metadata) do
Expand Down Expand Up @@ -375,8 +377,8 @@ defmodule PolymorphicEmbed do

def traverse_errors(%Ecto.Changeset{changes: changes, types: types} = changeset, msg_func)
when is_function(msg_func, 1) or is_function(msg_func, 3) do

Ecto.Changeset.traverse_errors(changeset, msg_func)
changeset
|> Ecto.Changeset.traverse_errors(msg_func)
|> merge_polymorphic_keys(changes, types, msg_func)
end

Expand All @@ -389,7 +391,7 @@ defmodule PolymorphicEmbed do
end

defp merge_polymorphic_keys(map, changes, types, msg_func) do
Enum.reduce types, map, fn
Enum.reduce(types, map, fn
{field, {:parameterized, PolymorphicEmbed, _opts}}, acc ->
if changeset = Map.get(changes, field) do
case traverse_errors(changeset, msg_func) do
Expand All @@ -409,15 +411,15 @@ defmodule PolymorphicEmbed do
end)

case all_empty? do
true -> acc
true -> acc
false -> Map.put(acc, field, errors)
end
else
acc
end

{_, _}, acc ->
acc
end
{_, _}, acc ->
acc
end)
end
end
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ defmodule PolymorphicEmbed.MixProject do
{:ex_doc, "~> 0.23", only: :dev},
{:ecto_sql, "~> 3.6", only: :test},
{:postgrex, "~> 0.15", only: :test},
{:query_builder, "~> 0.19.2", only: :test},
{:phoenix_ecto, "~> 4.2", only: :test}
{:query_builder, "~> 1.0.0", only: :test},
{:phoenix_ecto, "~> 4.2", only: :test},
{:phoenix_live_view, "~> 0.17.7", only: :test},
{:floki, "~> 0.33.0", only: :test}
]
end

Expand Down
Loading