Ecto changeset - validate_length not executing if changeset is repu

Hi,

I know i am making some mistake but not sure where. I am creating a changeset in update callback of Live component as below:

  def update(%{experience: experience, current_step: current_step} = assigns, socket) do
    changeset = get_changeset(experience, current_step)
    {:ok, socket |> assign(assigns) |> assign_form(changeset)}
  end

I am creating changeset based on some criteria:

defp get_changeset(experience, current_step) do
    sub_step = find_sub_step(current_step)
    
    # It created changeset based on the required fields of the sub step
    changeset = Experiences.change_experience(experience, sub_step[:required])
    
    # if sub step also has a validate length requirement then conditionally build it further
    if sub_step[:length] do
      Experiences.validate_length_experience(
        changeset,
        sub_step[:length],
        sub_step[:min],
        sub_step[:max]
      )
    else
      changeset
    end
  end

Experiences.validate_length_experience is defined as such

def validate_length_experience(changeset, field, min_length, max_length) do
    validate_length(changeset, field, min: min_length, max: max_length)
end

It is not validating the length and that’s the problem. If i include validate_length just after the validate_required in context or in schema file, it works as it should.

It’s hard to understand what is happening without more context but in your case I would first check if sub_step[:length] is returning an atom.

To understand better the problem it would help if you could share the output of IO.inspect(sub_step) and Experiences.change_experience(experience, sub_step[:required]) |> IO.inspect()

IO.inspect(sub_step)

%{
  max: 500,
  min: 50,
  length: :description,
  title: "What we'll do",
  step: 1,
  required: [:description]
}

Experiences.change_experience(experience, sub_step[:required]) |> IO.inspect()

#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [description: {"can't be blank", [validation: :required]}],
  data: #Culture.Experiences.Experience<>,
  valid?: false
>

I am building a multi-step form and at each step i am detemining what kind of validations i need to do. required is a common validation among all steps but length is not. If a step/sub step has got a length field only then conditionally build changeset accordingly.

In your example the changeset that you pass to validate_length already contains an error for the description field so Ecto does not validate this field any further. Also validate_length only validates fields that are not nil. If a field is not present or is nil then Ecto won’t validate it, so if you want to make sure that the field is both present and has a certain length you need to combine validate_required with validate_length.

Only changes to your data are validated. The existing data is expected to be valid as given. Your changeset doesn’t show any changes, so nothing to validate.

That’s not how things work. All validations even for the same field will run if there are actually changes for that field.

1 Like

But how validate_required works then as it is working fine with the below line:
changeset = Experiences.change_experience(experience, sub_step[:required])
i am creating the above changeset as below and i am not passing any changes while creating this changeset:

  def change_experience(%__MODULE__{} = experience, required_fields \\ [], attrs \\ %{}) do
    experience
    |> cast(attrs, required_fields)
    |> validate_required(required_fields)
  end

Ah, missed to mention that. validate_required is the only exception, because otherwise it would be quite useless tbh. It does validate that the field is filled based on both the initial data and the changes.

1 Like

makes sense :slight_smile: Thanks a lot for the clarification. I have been stuck since yesterday on the same; I am very new to Live View and learning by doing.

does it make sense to just create a changeset (with required fields only in mount or update) and run All the validations on phx-change or any other similar event handling to validate it.

Is the form savable at the various steps, or are the steps just virtual fields to make a wizard-like multi-page form? If it’s the latter, it might be easiest to break the steps up into multiple functions:

# experiences/experience.ex
def validate_step_1(changeset, attrs) do
  changeset
  |> cast([:length, :max, :min])
  |> validate...
...
end

def validate_step_2(changeset, attrs) do
  changeset
  |> validate_step_1(attrs)
  |> cast([:other, :field])
  |> validate...
...
end

yeah; i thought about it initially but did not want to have 15 different validation changesets as there are more than 15 small steps in the form.

May be it is a better alternative. Giving my initial idea a try and if not then these separate changesets will work for sure.

I see nothing wrong with having 15 or even more validation changesets if they all lend themselves to a particular current state of your form.

hmm i think you misunderstood what i was saying. I was looking for an alternative and wanted to give it a try. By doing that i got to learn more about validations than i would have missed probably.
My fallback plan is to use separate validation changesets.

I think I got you but I’m not sure you’d need alternatives. What’s wrong with changesets? Why do you need alternatives?

I didn’t say that something is wrong with changesets. I am using changesets. Rather than using static changesets i am building changesets dynamically. It seems you are not convinced; so can you please explain why one is better than the other?

defp get_changeset(experience, current_step) do
    sub_step = find_sub_step(current_step)
    
    # It created changeset based on the required fields of the sub step
    changeset = Experiences.change_experience(experience, sub_step[:required])
    
    # if sub step also has a validate length requirement then conditionally build it further
    if sub_step[:length] do
      Experiences.validate_length_experience(
        changeset,
        sub_step[:length],
        sub_step[:min],
        sub_step[:max]
      )
    else
      changeset
    end
  end

One important thing to understand with changeset is that they’re generally not meant to “be build over time”. You don’t start with a (blank) changeset, then add a change later and get it validated.

Instead changeset are always build from the to-be-changed data and all the to-be-applied changes and it’ll validate if things are fine with the given changes. When the to-be-applied changes “change” for some reason or another before the previous have been applied you’d build a completely fresh changeset again.

can it be done this way (steps always executed sequentially in the same order). For mount/update:

  1. build a changeset with No changes
  2. add some validations
  3. add some more validations

and during validation (like phx-change or any other such events):

  1. build a changeset with Changes
  2. add some validations
  3. add some more validations
  4. validate all the changes (either by saving in db or manually making the changeset validate the changes)

Dynamically building changesets loses you explicitness. Now somebody (yourself included) has to read the code to understand why a dynamically created changeset is invalid.

I get how we the programmers sometimes feel we have to automate stuff because it seems repetitive but it pays off to stop and think how would you work with your own solution down the line. Personally I’d prefer to have a stack trace pointing at a “static” (as in, hard-coded) changeset like it’s usually done.

TL;DR I’d optimize for quick work with my code in the future. What you seem to want to do is being clever for no good reason except maybe save some coding lines which is not an universally good thing to optimize for.

1 Like

Accepted. I was also worrying about what if i need to add another kind of validation (which is not covered in my dynamic solution yet).

I learnt a lot during this whole process. Step 3 is not working for me; i will come back to it in future why it is not working. Likely it is a mistake on my end. Thanks a lot guys @dimitarvp @LostKobrakai @dfalling

Edit: Going with the simple solution of static changesets for every step.

1 Like

Make a new topic, isolate stuff, write some tests, we can help. :slight_smile: