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()
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.
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.
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
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 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.
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.
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.