Unexpected AshPhoenix.Form.validate/3 behaviour when typing into text field

So I have a form which validates on change.

  def handle_event("validate", %{"form" => params}, socket) do
    form = AshPhoenix.Form.validate(socket.assigns.form, params)
    {:noreply, assign(socket, form: form)}
  end

While trying to read the value of the following field I noticed it did not change until it was valid. The field is on a nested form. I have not shown the root form.

%Phoenix.HTML.FormField{
  id: "reactant_formulas_0_formula_0_reactant_0_identity",
  name: "reactant[formulas][0][formula][0][reactant][identity]",
  errors: [{"is required", []}],
  field: :identity,
  form: %Phoenix.HTML.Form{
    source: #AshPhoenix.Form<
      resource: Flame.App.Reactant,
      action: :create,
      type: :create,
      params: %{"_form_type" => "create"},
      source: #Ash.Changeset<
        action_type: :create,
        action: :create,
        attributes: %{},
        relationships: %{},
        errors: [
          %Ash.Error.Changes.Required{
            field: :identity,
            type: :attribute,
            resource: Flame.App.Reactant,
            changeset: nil,
            query: nil,
            error_context: [],
            vars: [],
            path: [],
            stacktrace: #Stacktrace<>,
            class: :invalid
          }
        ],
        data: #Flame.App.Reactant<
          sources: #Ash.NotLoaded<:relationship>,
          formulas: #Ash.NotLoaded<:relationship>,
          sources_join_assoc: #Ash.NotLoaded<:relationship>,
          __meta__: #Ecto.Schema.Metadata<:built, "reactants">,
          id: nil,
          identity: nil,
          spec: nil,
          created_at: nil,
          updated_at: nil,
          aggregates: %{},
          calculations: %{},
          ...
        >,
        valid?: false
      >,
      name: "reactant[formulas][0][formula][0][reactant]",
      data: nil,
      form_keys: [],
      forms: %{},
      api: nil,
      method: "post",
      submit_errors: nil,
      id: "reactant_formulas_0_formula_0_reactant",
      transform_errors: nil,
      original_data: nil,
      transform_params: nil,
      prepare_params: nil,
      prepare_source: nil,
      warn_on_unhandled_errors?: true,
      any_removed?: false,
      added?: true,
      changed?: false,
      touched_forms: MapSet.new(["_form_type"]),
      valid?: false,
      errors: true,
      submitted_once?: false,
      just_submitted?: false,
      ...
    >,
    impl: Phoenix.HTML.FormData.AshPhoenix.Form,
    id: "reactant_formulas_0_formula_0_reactant_0",
    name: "reactant[formulas][0][formula][0][reactant]",
    data: nil,
    hidden: [
      {"_persistent_id", "0"},
      {:_touched, "_form_type"},
      {:_form_type, "create"}
    ],
    params: %{"_form_type" => "create", "_persistent_id" => "0"},
    errors: [identity: {"is required", []}],
    options: [method: "post"],
    index: nil,
    action: nil
  },
  value: nil
}

The field is for the following attribute.

    attribute :identity, :string do
      allow_nil? false
      constraints trim?: false, max_length: 20, min_length: 4, allow_empty?: false
    end

Upon inputting t into the text input for identity the field becomes the following after validation.

%Phoenix.HTML.FormField{
  id: "reactant_formulas_0_formula_0_reactant_0_identity",
  name: "reactant[formulas][0][formula][0][reactant][identity]",
  errors: [
    {"length must be greater than or equal to %{min}",
     [
       field: "identity",
       message: "length must be greater than or equal to %{min}",
       min: 4
     ]}
  ],
  field: :identity,
  form: %Phoenix.HTML.Form{
    source: #AshPhoenix.Form<
      resource: Flame.App.Reactant,
      action: :create,
      type: :create,
      params: %{
        "_form_type" => "create",
        "_persistent_id" => "0",
        "_touched" => "_form_type",
        "identity" => "t"
      },
      source: #Ash.Changeset<
        action_type: :create,
        action: :create,
        attributes: %{},
        relationships: %{},
        errors: [
          %Ash.Error.Changes.InvalidAttribute{
            field: :identity,
            message: "length must be greater than or equal to %{min}",
            private_vars: nil,
            value: "t",
            changeset: nil,
            query: nil,
            error_context: [],
            vars: [
              field: :identity,
              message: "length must be greater than or equal to %{min}",
              min: 4
            ],
            path: [],
            stacktrace: #Stacktrace<>,
            class: :invalid
          }
        ],
        data: #Flame.App.Reactant<
          sources: #Ash.NotLoaded<:relationship>,
          formulas: #Ash.NotLoaded<:relationship>,
          sources_join_assoc: #Ash.NotLoaded<:relationship>,
          __meta__: #Ecto.Schema.Metadata<:built, "reactants">,
          id: nil,
          identity: nil,
          spec: nil,
          created_at: nil,
          updated_at: nil,
          aggregates: %{},
          calculations: %{},
          ...
        >,
        valid?: false
      >,
      name: "reactant[formulas][0][formula][0][reactant]",
      data: nil,
      form_keys: [],
      forms: %{},
      api: nil,
      method: "post",
      submit_errors: nil,
      id: "reactant_formulas_0_formula_0_reactant",
      transform_errors: nil,
      original_data: nil,
      transform_params: nil,
      prepare_params: nil,
      prepare_source: nil,
      warn_on_unhandled_errors?: true,
      any_removed?: false,
      added?: true,
      changed?: false,
      touched_forms: MapSet.new(["_form_type", "_persistent_id", "_touched",
       "identity"]),
      valid?: false,
      errors: true,
      submitted_once?: false,
      just_submitted?: false,
      ...
    >,
    impl: Phoenix.HTML.FormData.AshPhoenix.Form,
    id: "reactant_formulas_0_formula_0_reactant_0",
    name: "reactant[formulas][0][formula][0][reactant]",
    data: nil,
    hidden: [
      {"_persistent_id", "0"},
      {:_touched, "_form_type,_persistent_id,_touched,identity"},
      {:_form_type, "create"}
    ],
    params: %{
      "_form_type" => "create",
      "_persistent_id" => "0",
      "_touched" => "_form_type",
      "identity" => "t"
    },
    errors: [
      identity: {"length must be greater than or equal to %{min}",
       [
         field: "identity",
         message: "length must be greater than or equal to %{min}",
         min: 4
       ]}
    ],
    options: [method: "post"],
    index: nil,
    action: nil
  },
  value: nil
}

The value: remains nil I would have expected it to become "t". "t" can be seen in the params: of the form:. The error is because the value is of insufficient length. If I continue to type e and s the value: continues to be nil with params: updating. If I then type t now we get the following field.

%Phoenix.HTML.FormField{
  id: "reactant_formulas_0_formula_0_reactant_0_identity",
  name: "reactant[formulas][0][formula][0][reactant][identity]",
  errors: [],
  field: :identity,
  form: %Phoenix.HTML.Form{
    source: #AshPhoenix.Form<
      resource: Flame.App.Reactant,
      action: :create,
      type: :create,
      params: %{
        "_form_type" => "create",
        "_persistent_id" => "0",
        "_touched" => "_form_type,_persistent_id,_touched,identity",
        "identity" => "test"
      },
      source: #Ash.Changeset<
        action_type: :create,
        action: :create,
        attributes: %{identity: "test"},
        relationships: %{},
        errors: [],
        data: #Flame.App.Reactant<
          sources: #Ash.NotLoaded<:relationship>,
          formulas: #Ash.NotLoaded<:relationship>,
          sources_join_assoc: #Ash.NotLoaded<:relationship>,
          __meta__: #Ecto.Schema.Metadata<:built, "reactants">,
          id: nil,
          identity: nil,
          spec: nil,
          created_at: nil,
          updated_at: nil,
          aggregates: %{},
          calculations: %{},
          ...
        >,
        valid?: true
      >,
      name: "reactant[formulas][0][formula][0][reactant]",
      data: nil,
      form_keys: [],
      forms: %{},
      api: nil,
      method: "post",
      submit_errors: nil,
      id: "reactant_formulas_0_formula_0_reactant",
      transform_errors: nil,
      original_data: nil,
      transform_params: nil,
      prepare_params: nil,
      prepare_source: nil,
      warn_on_unhandled_errors?: true,
      any_removed?: false,
      added?: true,
      changed?: true,
      touched_forms: MapSet.new(["_form_type", "_persistent_id", "_touched",
       "identity"]),
      valid?: true,
      errors: true,
      submitted_once?: false,
      just_submitted?: false,
      ...
    >,
    impl: Phoenix.HTML.FormData.AshPhoenix.Form,
    id: "reactant_formulas_0_formula_0_reactant_0",
    name: "reactant[formulas][0][formula][0][reactant]",
    data: nil,
    hidden: [
      {"_persistent_id", "0"},
      {:_touched, "_form_type,_persistent_id,_touched,identity"},
      {:_form_type, "create"}
    ],
    params: %{
      "_form_type" => "create",
      "_persistent_id" => "0",
      "_touched" => "_form_type,_persistent_id,_touched,identity",
      "identity" => "test"
    },
    errors: [],
    options: [method: "post"],
    index: nil,
    action: nil
  },
  value: "test"
}

The error is gone and the value: is finally updated to "test". I am trying to read the value from the field as it changes. I am passing the field to a LiveComponent and trying to access and alter the value: but it only changes when the error is gone. Should I instead be getting the value from the params:? I think it’s a little confusing that the value: only updates if there are no errors.

I just upgraded my dependencies.

ash 2.14.17 => 2.15.8
ash_authentication 3.11.8 => 3.11.15
ash_authentication_phoenix 1.8.0 => 1.8.4
ash_phoenix 1.2.17 => 1.2.19
ash_postgres 1.3.50 => 1.3.52

And now when I type t on the field in a fresh form I get the following.

%Phoenix.HTML.FormField{
  id: "reactant_formulas_0_formula_0_reactant_0_identity",
  name: "reactant[formulas][0][formula][0][reactant][identity]",
  errors: [
    {"length must be greater than or equal to %{min}",
     [
       field: "identity",
       message: "length must be greater than or equal to %{min}",
       min: 4
     ]}
  ],
  field: :identity,
  form: %Phoenix.HTML.Form{
    source: #AshPhoenix.Form<
      resource: Flame.App.Reactant,
      action: :create,
      type: :create,
      params: %{
        "_form_type" => "create",
        "_persistent_id" => "0",
        "_touched" => "_form_type",
        "identity" => "t"
      },
      source: #Ash.Changeset<
        action_type: :create,
        action: :create,
        attributes: %{},
        relationships: %{},
        errors: [
          %Ash.Error.Changes.InvalidAttribute{
            field: :identity,
            message: "length must be greater than or equal to %{min}",
            private_vars: nil,
            value: "t",
            changeset: nil,
            query: nil,
            error_context: [],
            vars: [
              field: :identity,
              message: "length must be greater than or equal to %{min}",
              min: 4
            ],
            path: [],
            stacktrace: #Stacktrace<>,
            class: :invalid
          }
        ],
        data: #Flame.App.Reactant<
          sources: #Ash.NotLoaded<:relationship>,
          formulas: #Ash.NotLoaded<:relationship>,
          sources_join_assoc: #Ash.NotLoaded<:relationship>,
          __meta__: #Ecto.Schema.Metadata<:built, "reactants">,
          id: nil,
          identity: nil,
          spec: nil,
          created_at: nil,
          updated_at: nil,
          aggregates: %{},
          calculations: %{},
          ...
        >,
        valid?: false
      >,
      name: "reactant[formulas][0][formula][0][reactant]",
      data: nil,
      form_keys: [],
      forms: %{},
      api: nil,
      method: "post",
      submit_errors: nil,
      id: "reactant_formulas_0_formula_0_reactant",
      transform_errors: nil,
      original_data: nil,
      transform_params: nil,
      prepare_params: nil,
      prepare_source: nil,
      warn_on_unhandled_errors?: true,
      any_removed?: false,
      added?: true,
      changed?: false,
      touched_forms: MapSet.new(["_form_type", "_persistent_id", "_touched",
       "identity"]),
      valid?: false,
      errors: true,
      submitted_once?: false,
      just_submitted?: false,
      ...
    >,
    impl: Phoenix.HTML.FormData.AshPhoenix.Form,
    id: "reactant_formulas_0_formula_0_reactant_0",
    name: "reactant[formulas][0][formula][0][reactant]",
    data: nil,
    hidden: [
      {"_persistent_id", "0"},
      {:_touched, "_form_type,_persistent_id,_touched,identity"},
      {:_form_type, "create"}
    ],
    params: %{
      "_form_type" => "create",
      "_persistent_id" => "0",
      "_touched" => "_form_type",
      "identity" => "t"
    },
    errors: [
      identity: {"length must be greater than or equal to %{min}",
       [
         field: "identity",
         message: "length must be greater than or equal to %{min}",
         min: 4
       ]}
    ],
    options: [method: "post"],
    index: nil,
    action: nil
  },
  value: "t"
}

value: is set to "t" as I originally expected. I guess this was a bug in an older version :person_shrugging:.

1 Like

Yes, we fixed this bug in a recent release :slight_smile:

1 Like