Why my nested form inputs does not have my input helper's style classes?

I have this cliente schema:

  schema "clientes" do
    field :nome, :string
    field :telefone, :string
    field :possui_empresa?, :boolean, default: false

    belongs_to :user, User
    has_one :empresa, Empresa

    timestamps()
  end

  @doc false
  def changeset(cliente, attrs) do
    cliente
    |> cast(attrs, [:possui_empresa?, :nome, :telefone])
    |> validate_required([:possui_empresa?, :nome, :telefone])
    |> parse_string(:nome)
    |> validate_phone(:telefone)
    |> cast_assoc(:user, required: true, with: &User.cliente_changeset/2)
  end
end

and this is the user schema:

  schema "users" do
  ¦ field :email, :string
  ¦ field :password, :string, virtual: true
  ¦ field :password_confirmation, :string, virtual: true
  ¦ field :hashed_password, :string
  ¦ field :role, Ecto.Enum, values: @roles, default: :visitante
  ¦ field :confirmed_at, :naive_datetime

  ¦ has_one :admin, Admin
  ¦ has_one :cliente, Cliente
  ¦ has_one :contador, Contador
  ¦ has_one :colaborador, Colaborador

  ¦ timestamps()
  end

this is my input_helpers:

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"

      true ->
        IO.inspect(form)
        "border-blue-500"
    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

and this is my current cliente form:

[...]
            <%= 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 %>
            <!-- Form Inputs End -->

            <%= submit "Cadastrar", class: "basic-form-submit", disabled: !@changeset.valid?, phx_disable_with: "Cadastrando..." %>
[...]

However, users’s inputs, aka cliente[user] does not have those style classes (border-blue-500 or border-red-500)

What I can do to these “nested” inputs act like those that are out?

screenshot:
image

Ok I don’t know if I expressed myself clearly, but, to summarize my probem, here’s a TL;DR:

1 - I have a cliente and user schemas!
2 - A user has one cliente
3 - I created a nested form to clientes's user
4- users's form odes not get my input styles :confused:

Here’s a GIF to show you all what exactly happens:
114580681-73917d80-9c55-11eb-9075-1f33cc79b951

Hmm, have you tried putting an IO.inspect or IEx.pry here?

It may be worth checking whether action here is still false-y when you’re expecting it to be truth-y and so this first condition catches. Also, consider taking another look at the note from the Phoenix.HTML docs quoted below that could be relevant.

A note on :errors

If no action has been applied to the changeset or action was set to :ignore, no errors are shown on the form object even if the changeset has a non-empty :errors value.

This is useful for things like validation hints on form fields, e.g. an empty changeset for a new form. That changeset isn’t valid, but we don’t want to show errors until an actual user action has been performed.

Ecto automatically applies the action for you when you call Repo.insert/update/delete, but if you want to show errors manually you can also set the action yourself, either directly on the Ecto.Changeset struct field or by using Ecto.Changeset.apply_action/2.

source: Phoenix.HTML.Form

Another issue that may or may not be related is that there’s no separate error_tag for your nested form for user u. It might be worth trying to do what’s done in this post on Nested Forms In Phoenix:

 <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <%= inputs_for f, :items, fn p -> %>
    <div class="form-group">
      <%= label p, :body, class: "control-label" %>
      <%= text_input p, :body, class: "form-control" %>
      <%= error_tag p, :body %>
    </div>
  <% end %>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

Good luck, hope this helped somewhat!

1 Like

That’s the result of tryint to wirte a invalid email:

#Ecto.Changeset<
  action: :insert,
  changes: %{
    user: #Ecto.Changeset<
      action: :insert,
      changes: %{email: "a", role: :cliente},
      errors: [
        password: {"can't be blank", [validation: :required]},
        password_confirmation: {"can't be blank", [validation: :required]},
        email: {"must have the @ sign and no spaces", [validation: :format]}
      ],
      data: #Conts.Accounts.User<>,
      valid?: false
    >
  },
  errors: [
    nome: {"can't be blank", [validation: :required]},
    telefone: {"can't be blank", [validation: :required]}
  ],
  data: #Conts.Users.Cliente<>,
  valid?: false
>

Both cliente and user changeset have the :action attribute set to :insert!

Also, I tried to inspect if the fields are having errors “correctly”, and yes:

nome: {"can't be blank", [validation: :required]}
telefone: {"can't be blank", [validation: :required]}
email: {"must have the @ sign and no spaces", [validation: :format]}
password: {"can't be blank", [validation: :required]}
password_confirmation: {"can't be blank", [validation: :required]}

I bet is the state_class is not set up to receive nested forms… I’ll verify that now!

We do have a separated errot_tag! Here is th relevant part on input_heleprs.ex:

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

Yeah, by bet is right! Nested form’s fields are falling down to the !forms.action… I trying to solve this!

Ok, I removed the cond and replaced with a if-else in the state_class function as:

  defp state_class(form, field) do
    if form.errors[field] do
      # we do have errors in this field
      "border-red-500"
    else
      # we don't need any additional style at all
      # only with focus:within
      ""
    end
  end

That’s solved the issue! But my doubt remains active… Why the nested form action isn’t set and how I could set it manually?

Glad you got it working!

More than a bit late back to the the party, but if you take a look at the implementation of inputs_for/3 as well as the implementation of to_form/4, it looks like the action field from the top level form f is never passed into the nested form u which would explain why you saw !form.action in the conditional returning true as form.action would be nil so !nil would be truthy.

I wouldn’t suggest doing this, but I guess you could try to manually set it.

you can also set the action yourself, either directly on the Ecto.Changeset struct field or by using Ecto.Changeset.apply_action/2 . source: Phoenix.HTML.Form docs

That said, basing it off the errors field like you did makes more sense in my book.