From bb39ba3f03cd4bd835e5da784cb7cbd7f134766f Mon Sep 17 00:00:00 2001 From: Amos King Date: Mon, 20 Feb 2023 17:32:04 -0600 Subject: [PATCH] Fixes default values on dead forms (#74) --- lib/ash_phoenix/form/form.ex | 49 ++++++++++++++++++++- lib/ash_phoenix/live_view.ex | 1 + test/form_test.exs | 34 ++++++++++++++ test/support/resources/post_with_default.ex | 1 + 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/lib/ash_phoenix/form/form.ex b/lib/ash_phoenix/form/form.ex index 45bd6bf..bc4b9b5 100644 --- a/lib/ash_phoenix/form/form.ex +++ b/lib/ash_phoenix/form/form.ex @@ -283,6 +283,16 @@ defmodule AshPhoenix.Form do type: :string, doc: "The http method to associate with the form. Defaults to `post` for creates, and `put` for everything else." + ], + exclude_fields_if_empty: [ + type: {:list, {:or, [:atom, :string, {:tuple, [:any, :any]}]}}, + doc: """ + These fields will be ignored if they are empty strings. + + This list of fields supports dead view forms. When a form is submitted from dead view + empty fields are submitted as empty strings. This is problematic for fields that allow_nil + or those that have default values. + """ ] ] @@ -2199,6 +2209,7 @@ defmodule AshPhoenix.Form do form = to_form!(form) # These options aren't documented because they are still experimental hidden? = Keyword.get(opts, :hidden?, true) + excluded_empty_fields = Keyword.get(opts, :exclude_fields_if_empty, Keyword.get(form.opts, :exclude_fields_if_empty, [])) indexer = opts[:indexer] indexed_lists? = opts[:indexed_lists?] || not is_nil(indexer) || false transform = opts[:transform] @@ -2214,7 +2225,10 @@ defmodule AshPhoenix.Form do |> Keyword.keys() |> Enum.flat_map(&[&1, to_string(&1)]) - params = Map.drop(form.params, form_keys) + params = + form.params + |> Map.drop(form_keys) + |> exclude_empty_fields(excluded_empty_fields) params = if only_touched? do @@ -2245,6 +2259,7 @@ defmodule AshPhoenix.Form do nested_form = form.forms[key] if nested_form && filter.(nested_form) do + opts = Keyword.put(opts, :exclude_fields_if_empty, Keyword.get(excluded_empty_fields, key, [])) nested_params = params(nested_form, opts) if nested_params["_ignore"] == "true" do @@ -4158,6 +4173,38 @@ defmodule AshPhoenix.Form do defp form_for_method(:create), do: "post" defp form_for_method(_), do: "put" + defp exclude_empty_fields(params, []) do + params + end + + defp exclude_empty_fields(params, _) when params == %{} do + params + end + + defp exclude_empty_fields(params, [unset_key | rest]) when is_atom(unset_key) do + {_, new} = + Map.get_and_update(params, to_string(unset_key), fn + "" -> :pop + nil -> :pop + value -> {value, value} + end) + + exclude_empty_fields(new, rest) + end + + defp exclude_empty_fields(params, [{nested_type, nested_keys} | rest]) do + {_, new} = + Map.get_and_update(params, to_string(nested_type), fn + nil -> + :pop + + map -> + {map, exclude_empty_fields(map, nested_keys)} + end) + + exclude_empty_fields(new, rest) + end + defimpl Phoenix.HTML.FormData do import AshPhoenix.FormData.Helpers diff --git a/lib/ash_phoenix/live_view.ex b/lib/ash_phoenix/live_view.ex index b174386..0baa9b0 100644 --- a/lib/ash_phoenix/live_view.ex +++ b/lib/ash_phoenix/live_view.ex @@ -600,6 +600,7 @@ defmodule AshPhoenix.LiveView do Phoenix.LiveView.assign(socket, one, two) end end + {:error, :nofile} -> defp assign(%Phoenix.LiveView.Socket{} = socket, one, two) do Phoenix.LiveView.assign(socket, one, two) diff --git a/test/form_test.exs b/test/form_test.exs index 917287c..1b9c232 100644 --- a/test/form_test.exs +++ b/test/form_test.exs @@ -191,6 +191,40 @@ defmodule AshPhoenix.FormTest do assert form.valid? == false end + test "blank form values unset - helps support dead view forms" do + form = + Form.for_create(PostWithDefault, :create, api: Api, exclude_fields_if_empty: [:text, :title]) + + {:ok, post} = Form.submit(form, params: %{"title" => "", "text" => "bar"}) + assert post.text == "bar" + assert post.title == nil + end + + test "blank nested form values unset - helps support dead view forms" do + form = + Comment + |> Form.for_create(:create, + api: Api, + forms: [ + post: [ + resource: PostWithDefault, + create_action: :create + ] + ], + exclude_fields_if_empty: [post: [:title, :description]] + ) + |> Form.add_form(:post) + + {:ok, comment} = + Form.submit(form, + params: %{"text" => "comment", "post" => %{"title" => "", "text" => "bar"}} + ) + + post = comment.post + assert post.text == "bar" + assert post.title == nil + end + test "phoenix forms are accepted as input in some cases" do form = Form.for_create(PostWithDefault, :create, api: Api) form = AshPhoenix.Form.validate(form, %{"text" => ""}, errors: form.submitted_once?) diff --git a/test/support/resources/post_with_default.ex b/test/support/resources/post_with_default.ex index a6d55f5..db1e24d 100644 --- a/test/support/resources/post_with_default.ex +++ b/test/support/resources/post_with_default.ex @@ -13,5 +13,6 @@ defmodule AshPhoenix.Test.PostWithDefault do attributes do uuid_primary_key(:id) attribute(:text, :string, allow_nil?: false, default: "foo") + attribute(:title, :string) end end