feature: add_embed and remove_embed helpers

This commit is contained in:
Zach Daniel 2021-03-16 21:42:40 -04:00
parent 73fd890dbe
commit 46ab44634b
2 changed files with 311 additions and 2 deletions

View file

@ -48,4 +48,301 @@ defmodule AshPhoenix do
def hiding_errors?(%Ash.Query{} = query) do def hiding_errors?(%Ash.Query{} = query) do
query.context[:private][:ash_phoenix][:hide_errors] == true query.context[:private][:ash_phoenix][:hide_errors] == true
end end
@doc """
A utility to support "add" buttons on embedded types used in forms.
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="append_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("append_thing", %{"path" => path}, socket) do
changeset = add_embed(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
You can also pass a specific value to be added, to seed the changes in a customized way.
By default, `%{}` is used.
"""
def add_embed(query, path, outer_form_name, add \\ %{})
def add_embed(%Ash.Changeset{} = changeset, path, outer_form_name, add) do
[^outer_form_name, key | path] = decode_path(path)
cond do
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
new_value = add_to_path(current_value, path, add)
new_value =
case attr.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
changeset
|> Ash.Changeset.change_attribute(attr.name, new_value)
|> mark_removed(new_value, attr.name)
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
current_value = Ash.Changeset.get_argument(changeset, arg.name)
new_value = add_to_path(current_value, path, add)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
changeset
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
true ->
changeset
end
end
def add_embed(%Ash.Query{} = query, path, outer_form_name, add) do
[^outer_form_name, key | path] = decode_path(path)
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
if arg do
current_value = Ash.Query.get_argument(query, arg.name)
new_value = add_to_path(current_value, path, add)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
query
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
else
query
end
end
def remove_embed(%Ash.Changeset{} = changeset, path, outer_form_name) do
[^outer_form_name, key | path] = decode_path(path)
cond do
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
new_value =
if path == [] do
nil
else
new_value = remove_from_path(current_value, path)
new_value =
case attr.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
end
changeset
|> Ash.Changeset.change_attribute(attr.name, new_value)
|> mark_removed(new_value, attr.name)
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
current_value = Ash.Changeset.get_argument(changeset, arg.name)
new_value =
if path == [] do
nil
else
new_value = remove_from_path(current_value, path)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
end
changeset
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
true ->
changeset
end
end
@doc """
A utility to support "remove" buttons on embedded types used in forms.
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
```elixir
# In your template, inside a form called `:change`
<button phx-click="remove_thing" phx-value-path={{form.path}}>
</button>
# In the view/component
def handle_event("remove_thing", %{"path" => path}, socket) do
changeset = remove_embed(socket.assigns.changeset, path, "change")
{:noreply, assign(socket, changeset: changeset)}
end
```
"""
def remove_embed(%Ash.Query{} = query, path, outer_form_name) do
[^outer_form_name, key | path] = decode_path(path)
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
if arg do
current_value = Ash.Query.get_argument(query, arg.name)
new_value = remove_from_path(current_value, path)
new_value =
case arg.type do
{:array, _} -> List.wrap(new_value)
_ -> new_value
end
query
|> Ash.Changeset.set_argument(arg.name, new_value)
|> mark_removed(new_value, arg.name)
else
query
end
end
defp mark_removed(%Ash.Query{} = query, value, name) do
Ash.Query.put_context(query, :private, %{removed_embeds: %{name => value in [nil, []]}})
end
defp mark_removed(%Ash.Changeset{} = changeset, value, name) do
Ash.Changeset.put_context(changeset, :private, %{
removed_embeds: %{name => value in [nil, []]}
})
end
defp add_to_path(nil, [], add) do
add
end
defp add_to_path(value, [], add) when is_list(value) do
value ++ List.wrap(add)
end
defp add_to_path(value, [key | rest], add) when is_integer(key) and is_list(value) do
List.update_at(value, key, &add_to_path(&1, rest, add))
end
defp add_to_path(empty, [key | rest], add) when is_integer(key) and empty in [nil, []] do
[add_to_path(nil, rest, add)]
end
defp add_to_path(value, [key | rest], add)
when (is_binary(key) or is_atom(key)) and is_map(value) do
cond do
Map.has_key?(value, key) ->
Map.update!(value, key, &add_to_path(&1, rest, add))
is_atom(key) && Map.has_key?(value, to_string(key)) ->
Map.update!(value, to_string(key), &add_to_path(&1, rest, add))
is_binary(key) && Enum.any?(Map.keys(value), &(to_string(&1) == key)) ->
Map.update!(value, String.to_existing_atom(key), &add_to_path(&1, rest, add))
true ->
Map.put(value, key, add_to_path(nil, rest, add))
end
end
defp add_to_path(nil, [key | rest], add) when is_binary(key) or is_atom(key) do
%{key => add_to_path(nil, rest, add)}
end
defp remove_from_path(value, [key]) when is_integer(key) and is_list(value) do
List.delete_at(value, key)
end
defp remove_from_path(value, [key]) when is_map(value) and (is_binary(key) or is_atom(key)) do
cond do
is_atom(key) ->
if is_struct(value) do
Map.put(value, key, nil)
else
Map.drop(value, [key, to_string(key)])
end
is_binary(key) && Enum.any?(Map.keys(value), &(to_string(&1) == key)) ->
if is_struct(value) do
Map.put(value, String.to_existing_atom(key), nil)
else
Map.drop(value, [key, String.to_existing_atom(key)])
end
true ->
Map.delete(value, key)
end
end
defp remove_from_path(value, [key | rest]) when is_list(value) and is_integer(key) do
List.update_at(value, key, &remove_from_path(&1, rest))
end
defp remove_from_path(value, [key | rest])
when is_map(value) and (is_binary(key) or is_atom(key)) do
cond do
Map.has_key?(value, key) ->
Map.update!(value, key, &remove_from_path(&1, rest))
is_atom(key) && Map.has_key?(value, to_string(key)) ->
Map.update!(value, to_string(key), &remove_from_path(&1, rest))
is_binary(key) && Enum.any?(Map.keys(value), &(to_string(&1) == key)) ->
Map.update!(value, String.to_existing_atom(key), &remove_from_path(&1, rest))
true ->
Map.put(value, key, remove_from_path(nil, rest))
end
end
defp remove_from_path(value, _), do: value
defp decode_path(path) do
path = Plug.Conn.Query.decode(path)
do_decode_path(path)
end
defp do_decode_path(path) when is_map(path) and path != %{} do
path_part = Enum.at(path, 0)
rest = do_decode_path(elem(path_part, 1))
path_part
|> elem(0)
|> Integer.parse()
|> case do
{integer, ""} ->
[integer | rest]
_ ->
[elem(path_part, 0) | rest]
end
end
defp do_decode_path(""), do: []
defp do_decode_path(other) do
[other]
end
end end

View file

@ -58,6 +58,14 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
[] []
end end
removed_embed_values =
changeset.context[:private][:removed_embeds]
|> Kernel.||(%{})
|> Enum.filter(&elem(&1, 1))
|> Enum.map(fn {name, _} -> {name, nil} end)
hidden = hidden ++ removed_embed_values
%Phoenix.HTML.Form{ %Phoenix.HTML.Form{
action: changeset.action && changeset.action.name, action: changeset.action && changeset.action.name,
source: Ash.Changeset.put_context(changeset, :form, %{path: []}), source: Ash.Changeset.put_context(changeset, :form, %{path: []}),
@ -141,9 +149,13 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
data = data =
if is_list(data) do if is_list(data) do
prepend ++ data ++ append Enum.reject(prepend ++ data ++ append, &(&1 == ""))
else else
data if data == "" do
nil
else
data
end
end end
data data