lib/polymorphic_embed/html/form.ex

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, input_value: 2]

    @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)

        _ ->
          nil
      end
    end

    @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 = 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(
        Enum.map(forms, fn form ->
          [hidden_inputs_for(form), fun.(form)]
        end)
      )
    end

    def to_form(%{action: parent_action} = source_changeset, form, field, type, options) do
      id = to_string(form.id <> "_#{field}")
      name = to_string(form.name <> "[#{field}]")

      params = Map.get(source_changeset.params || %{}, to_string(field), %{}) |> List.wrap()
      list_data = get_data(source_changeset, field, type) |> List.wrap()

      list_data
      |> Enum.with_index()
      |> Enum.map(fn {data, i} ->
        params = Enum.at(params, i) || %{}

        changeset =
          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 == []
        }

        %Phoenix.HTML.Form{
          source: changeset,
          impl: Phoenix.HTML.FormData.Ecto.Changeset,
          id: id,
          index: if(length(list_data) > 1, do: i),
          name: name,
          errors: errors,
          data: data,
          params: params,
          hidden: [__type__: to_string(type)],
          options: options
        }
      end)
    end

    defp get_data(changeset, field, type) do
      struct = Ecto.Changeset.apply_changes(changeset)

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

        data ->
          data
      end
    end

    # If the parent changeset had no action, we need to remove the action
    # from children changeset so we ignore all errors accordingly.
    defp apply_action(changeset, nil), do: %{changeset | action: nil}
    defp apply_action(changeset, _action), do: changeset

    defp get_errors(%{action: nil}), do: []
    defp get_errors(%{action: :ignore}), do: []
    defp get_errors(%{errors: errors}), do: errors
  end
end