Skip to content

Commit c13f124

Browse files
committed
Add PolymorphicEmbed.HTML.Component + restructure
1 parent 9bd8345 commit c13f124

4 files changed

Lines changed: 369 additions & 119 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &&
2+
Code.ensure_loaded?(Phoenix.Component) do
3+
defmodule PolymorphicEmbed.HTML.Component do
4+
use Phoenix.Component
5+
6+
import PolymorphicEmbed.HTML.Helpers
7+
8+
@doc """
9+
Renders nested form inputs for polymorphic embeds.
10+
11+
See `Phoenix.Component.inputs_for/1`.
12+
"""
13+
@doc type: :component
14+
attr(:field, Phoenix.HTML.FormField,
15+
required: true,
16+
doc: "A %Phoenix.HTML.Form{}/field name tuple, for example: {@form[:email]}."
17+
)
18+
19+
attr(:id, :string,
20+
doc: """
21+
The id to be used in the form, defaults to the concatenation of the given
22+
field to the parent form id.
23+
"""
24+
)
25+
26+
attr(:as, :atom,
27+
doc: """
28+
The name to be used in the form, defaults to the concatenation of the given
29+
field to the parent form name.
30+
"""
31+
)
32+
33+
attr(:default, :any, doc: "The value to use if none is available.")
34+
35+
attr(:prepend, :list,
36+
doc: """
37+
The values to prepend when rendering. This only applies if the field value
38+
is a list and no parameters were sent through the form.
39+
"""
40+
)
41+
42+
attr(:append, :list,
43+
doc: """
44+
The values to append when rendering. This only applies if the field value
45+
is a list and no parameters were sent through the form.
46+
"""
47+
)
48+
49+
attr(:skip_hidden, :boolean,
50+
default: false,
51+
doc: """
52+
Skip the automatic rendering of hidden fields to allow for more tight control
53+
over the generated markup.
54+
"""
55+
)
56+
57+
slot(:inner_block, required: true, doc: "The content rendered for each nested form.")
58+
59+
@persistent_id "_persistent_id"
60+
def polymorphic_embed_inputs_for(assigns) do
61+
%Phoenix.HTML.FormField{field: field_name, form: parent_form} = assigns.field
62+
options = assigns |> Map.take([:id, :as, :default, :append, :prepend]) |> Keyword.new()
63+
64+
options =
65+
parent_form.options
66+
|> Keyword.take([:multipart])
67+
|> Keyword.merge(options)
68+
69+
type = get_polymorphic_type(parent_form, field_name)
70+
71+
forms =
72+
to_form(
73+
parent_form.source,
74+
parent_form,
75+
field_name,
76+
[{:polymorphic_type, type} | options]
77+
)
78+
79+
seen_ids = for f <- forms, vid = f.params[@persistent_id], into: %{}, do: {vid, true}
80+
81+
{forms, _} =
82+
Enum.map_reduce(forms, seen_ids, fn %Phoenix.HTML.Form{params: params} = form, seen_ids ->
83+
id =
84+
case params do
85+
%{@persistent_id => id} -> id
86+
%{} -> next_id(map_size(seen_ids), seen_ids)
87+
end
88+
89+
form_id = "#{parent_form.id}_#{field_name}_#{id}"
90+
new_params = Map.put(params, @persistent_id, id)
91+
new_hidden = [{@persistent_id, id} | form.hidden]
92+
93+
new_form = %Phoenix.HTML.Form{
94+
form
95+
| id: form_id,
96+
params: new_params,
97+
hidden: new_hidden
98+
}
99+
100+
{new_form, Map.put(seen_ids, id, true)}
101+
end)
102+
103+
assigns = assign(assigns, :forms, forms)
104+
105+
~H"""
106+
<%= for finner <- @forms do %>
107+
<%= unless @skip_hidden do %>
108+
<%= for {name, value_or_values} <- finner.hidden,
109+
id = Phoenix.HTML.Form.input_id(finner, name),
110+
name = name_for_value_or_values(finner, name, value_or_values),
111+
value <- List.wrap(value_or_values) do %>
112+
<input type="hidden" id={id} name={name} value={value} />
113+
<% end %>
114+
<% end %>
115+
<%= render_slot(@inner_block, finner) %>
116+
<% end %>
117+
"""
118+
end
119+
120+
defp next_id(idx, %{} = seen_ids) do
121+
id_str = to_string(idx)
122+
123+
if Map.has_key?(seen_ids, id_str) do
124+
next_id(idx + 1, seen_ids)
125+
else
126+
id_str
127+
end
128+
end
129+
130+
defp name_for_value_or_values(form, field, values) when is_list(values) do
131+
Phoenix.HTML.Form.input_name(form, field) <> "[]"
132+
end
133+
134+
defp name_for_value_or_values(form, field, _value) do
135+
Phoenix.HTML.Form.input_name(form, field)
136+
end
137+
end
138+
end

lib/polymorphic_embed/html/form.ex

Lines changed: 8 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,13 @@
11
if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &&
22
Code.ensure_loaded?(PhoenixHTMLHelpers.Form) do
33
defmodule PolymorphicEmbed.HTML.Form do
4-
@moduledoc """
5-
Defines functions for using PolymorphicEmbed with `Phoenix.HTML.Form`.
6-
"""
74
import Phoenix.HTML, only: [html_escape: 1]
8-
import Phoenix.HTML.Form, only: [input_value: 2]
95
import PhoenixHTMLHelpers.Form, only: [hidden_inputs_for: 1]
106

11-
@doc """
12-
Returns the polymorphic type of the given field in the given form data.
13-
"""
14-
def get_polymorphic_type(%Phoenix.HTML.Form{} = form, field) do
15-
%schema{} = form.source.data
16-
get_polymorphic_type(form, schema, field)
17-
end
18-
19-
def get_polymorphic_type(%Phoenix.HTML.Form{} = form, schema, field) do
20-
case input_value(form, field) do
21-
%Ecto.Changeset{data: value} ->
22-
PolymorphicEmbed.get_polymorphic_type(schema, field, value)
23-
24-
%_{} = value ->
25-
PolymorphicEmbed.get_polymorphic_type(schema, field, value)
26-
27-
%{} = map ->
28-
case PolymorphicEmbed.get_polymorphic_module(schema, field, map) do
29-
nil ->
30-
nil
31-
32-
module ->
33-
PolymorphicEmbed.get_polymorphic_type(schema, field, module)
34-
end
7+
defdelegate get_polymorphic_type(form, field), to: PolymorphicEmbed.HTML.Helpers
358

36-
list when is_list(list) ->
37-
raise "Cannot infer the polymorphic type as the list of embeds may contain multiple types"
38-
39-
nil ->
40-
nil
41-
end
42-
end
9+
defdelegate to_form(source_changeset, form, field, options),
10+
to: PolymorphicEmbed.HTML.Helpers
4311

4412
@doc """
4513
Generates a new form builder without an anonymous function.
@@ -62,15 +30,14 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &
6230
<%= for channel_form <- polymorphic_embed_inputs_for f, :channel do %>
6331
<%= hidden_inputs_for(channel_form) %>
6432
65-
<%= case get_polymorphic_type(f, Reminder, :channel) do %>
33+
<%= case get_polymorphic_type(reminder_form, Reminder, :channel) do %>
6634
<% :sms -> %>
6735
<%= label channel_form, :number %>
6836
<%= text_input channel_form, :number %>
6937
7038
<% :email -> %>
71-
<%= label channel_form, :email_address %>
72-
<%= text_input channel_form, :address %>
73-
<% end %>
39+
<%= label channel_form, :email %>
40+
<%= text_input channel_form, :email %>
7441
<% end %>
7542
</.form>
7643
"""
@@ -95,8 +62,8 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &
9562
<%= text_input poly_form, :number %>
9663
9764
<% :email -> %>
98-
<%= label poly_form, :email_address %>
99-
<%= text_input poly_form, :address %>
65+
<%= label poly_form, :email %>
66+
<%= text_input poly_form, :email %>
10067
<% end %>
10168
<% end %>
10269
<% end %>
@@ -133,79 +100,5 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &
133100
end)
134101
)
135102
end
136-
137-
def to_form(%{action: parent_action} = source_changeset, form, field, options) do
138-
id = to_string(form.id <> "_#{field}")
139-
name = to_string(form.name <> "[#{field}]")
140-
141-
params = Map.get(source_changeset.params || %{}, to_string(field), %{}) |> List.wrap()
142-
143-
struct = Ecto.Changeset.apply_changes(source_changeset)
144-
145-
list_data =
146-
case Map.get(struct, field) do
147-
nil ->
148-
type = Keyword.get(options, :polymorphic_type, get_polymorphic_type(form, field))
149-
module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
150-
if module, do: [struct(module)], else: []
151-
152-
data ->
153-
List.wrap(data)
154-
end
155-
156-
list_data
157-
|> Enum.with_index()
158-
|> Enum.map(fn {data, i} ->
159-
params = Enum.at(params, i) || %{}
160-
161-
changeset =
162-
data
163-
|> Ecto.Changeset.change()
164-
|> apply_action(parent_action)
165-
166-
errors = get_errors(changeset)
167-
168-
changeset = %Ecto.Changeset{
169-
changeset
170-
| action: parent_action,
171-
params: params,
172-
errors: errors,
173-
valid?: errors == []
174-
}
175-
176-
%schema{} = source_changeset.data
177-
178-
field_opts = PolymorphicEmbed.get_field_opts(schema, field)
179-
type_field_atom = Map.get(field_opts, :type_field_atom, :__type__)
180-
# correctly set id and name for embeds_many inputs
181-
array? = Map.get(field_opts, :array?, false)
182-
183-
index_string = Integer.to_string(i)
184-
185-
type = PolymorphicEmbed.get_polymorphic_type(schema, field, changeset.data)
186-
187-
%Phoenix.HTML.Form{
188-
source: changeset,
189-
impl: Phoenix.HTML.FormData.Ecto.Changeset,
190-
id: if(array?, do: id <> "_" <> index_string, else: id),
191-
name: if(array?, do: name <> "[" <> index_string <> "]", else: name),
192-
index: if(array?, do: i),
193-
errors: errors,
194-
data: data,
195-
params: params,
196-
hidden: [{type_field_atom, to_string(type)}],
197-
options: options
198-
}
199-
end)
200-
end
201-
202-
# If the parent changeset had no action, we need to remove the action
203-
# from children changeset so we ignore all errors accordingly.
204-
defp apply_action(changeset, nil), do: %{changeset | action: nil}
205-
defp apply_action(changeset, _action), do: changeset
206-
207-
defp get_errors(%{action: nil}), do: []
208-
defp get_errors(%{action: :ignore}), do: []
209-
defp get_errors(%{errors: errors}), do: errors
210103
end
211104
end

0 commit comments

Comments
 (0)