mirror of
https://github.com/ash-project/ash_phoenix.git
synced 2024-09-20 07:12:49 +12:00
feature: add_embed
and remove_embed
helpers
This commit is contained in:
parent
73fd890dbe
commit
46ab44634b
2 changed files with 311 additions and 2 deletions
|
@ -48,4 +48,301 @@ defmodule AshPhoenix do
|
|||
def hiding_errors?(%Ash.Query{} = query) do
|
||||
query.context[:private][:ash_phoenix][:hide_errors] == true
|
||||
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
|
||||
|
|
|
@ -58,6 +58,14 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
|||
[]
|
||||
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{
|
||||
action: changeset.action && changeset.action.name,
|
||||
source: Ash.Changeset.put_context(changeset, :form, %{path: []}),
|
||||
|
@ -141,9 +149,13 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
|||
|
||||
data =
|
||||
if is_list(data) do
|
||||
prepend ++ data ++ append
|
||||
Enum.reject(prepend ++ data ++ append, &(&1 == ""))
|
||||
else
|
||||
data
|
||||
if data == "" do
|
||||
nil
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
data
|
||||
|
|
Loading…
Reference in a new issue