Search code examples
elixirphoenix-frameworkphoenix-live-view

How I could handle nested form action in Phoenix Live View?


So, I followwed this tutorial for creating dynamic forms: http://blog.plataformatec.com.br/2016/09/dynamic-forms-with-phoenix/

Okay, this is my actual input_helpers.ex:

defmodule Conts.InputHelpers do
  @moduledoc """
  Define <%= input f, :pass, type: :password_input %> syntax
  to create dynamic form inputs
  """

  use Phoenix.HTML

  alias Phoenix.HTML.Form, as: PhxForm
  import ContsWeb.ErrorHelpers, only: [error_tag: 2]

  @label_opts "basic-form-label"
  @input_opts "basic-form-input"

  def input(form, field, opts \\ []) do
    label_text = opts[:label] || humanize(field)
    type = opts[:type] || PhxForm.input_type(form, field)
    additional_classes = opts[:class] || ""

    label_opts = [class: @label_opts]

    input_opts = [
      class: "#{@input_opts} #{state_class(form, field)} #{additional_classes}",
      id: opts[:id]
    ]

    label_opts = if opts[:id], do: [for: opts[:for]] ++ label_opts, else: label_opts

    content_tag :fieldset do
      label = label(form, field, label_text, label_opts)
      input = input(type, form, field, input_opts)
      error = error_tag(form, field)
      error = if Enum.empty?(error), do: "", else: error

      [label, input, error]
    end
  end

  defp state_class(form, field) do
    cond do
      # The form was not yet submitted
      !form.action -> ""
      form.errors[field] -> "border-red-500"
      # we don't need any additional style at all
      # only with focus:within
      true -> ""
    end
  end

  # Implement clauses below for custom inputs.
  # defp input(:datepicker, form, field, input_opts) do
  #   raise "not yet implemented"
  # end
  defp input(:password_confirmation, form, field, input_opts) do
    apply(PhxForm, :password, [form, field, input_opts])
  end

  defp input(type, form, field, input_opts) do
    apply(PhxForm, type, [form, field, input_opts])
  end
end

I this project we have a cliente and a user schema, with a 1-1 realtionship!

In Live View template, I did as:


<%= input f, :nome, label: "Nome completo", phx_debounce: "blur" %>

<%= input f, :telefone, label: "Número de telefone", phx_debounce: "blur", class: "mobile-masked" %>

<%= inputs_for f, :user, fn u -> %>
  <%= input u, :email, phx_debounce: "blur" %>

  <%= input u, :password, label: "Senha", phx_debounce: "blur" %>

  <%= input u, :password_confirmation, label: "Confirme a Senha", phx_debounce: "blur" %>
<% end %>

The first two fields are from cliente and other from user changeset. When the validation starts, aka users start to type input, cliente's fields correctly applies the border-red-500 class, however user's fields do not!

I debug it and found that when it tries to validate any field of the user changeset, in the state_class function of input_helpers.ex, it falls to the !form.acton clause.

My question is: how can I handle this nested form action? Why it falls into the aforementioned cond clause?

Here's a GIF to demonstrate the problem: enter image description here


Solution

  • The nested fields are missing the :action field. On the parent form, it is usually :validate.

    There is cheeky "hacky" way of achieving what you want. Just change your HTML from this:

    <%= inputs_for f, :user, fn u -> %>
    

    to this:

    <%= for u <- inputs_for(f, :user) |> Enum.map(&Map.put(&1, :action, :validate)) do %>
    

    :P