Problem/Motivation
Validation errors from an untriggered dependent should be removed so that fields excluded by a dependency do not prevent the form from saving.
However, ConditionalFieldsFormHelper::dependentValidate() returns as soon as it finds an error:
if (!empty($field_errors)) { $form_state_addition['errors'] = $field_errors; return; }
The dependent is normally registered in conditional_fields_untriggered_dependents immediately after this block. Because of the early return, a dependent that has an error is never registered.
formValidate() therefore has no record of the dependent and cannot remove its validation errors. The fields that most need this handling are precisely the ones excluded from it.
This is the behaviour reported in #3344587: Support for field hidden by condition losing its required status. It reproduces on a normal entity form and is not specific to Inline Entity Form.
Steps to reproduce
- On Article, add a plain text field
field_trigger. - Make Body required.
- Make Body depend on
field_trigger: state Unchanged, condition Value, valuetrigger. - Add an Article. Give it a title, enter something other than
triggerinfield_trigger, leave Body empty, and save.
Actual: Drupal reports Body field is required. and the node is not saved.
Expected: The dependency is untriggered, so the Body validation error is removed and the node saves.
Proposed resolution
Register errored untriggered dependents in conditional_fields_untriggered_dependents so that formValidate() can remove their errors.
Remaining tasks
- Determine whether to register
$form_state_additionbefore retaining the earlyreturn, or remove the early return entirely. - Add functional coverage for an untriggered dependent with a Drupal validation error.
ConditionalFieldNumberZeroRequiredTest does not cover this path. It tests the conditional required state, where the validation error is never created in the first place.
Issue fork conditional_fields-3619935
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
joelpittetAlso likely related #3344587: Support for field hidden by condition losing its required status
Comment #3
orkutmuratyilmazConfirming this from a different angle, with an inline entity form dependent, and adding one result that may matter for how the fix is shaped.
Environment:
Setup
A content type
recordwith:field_party_type: taxonomy term reference,options_selectwidget, requiredfield_individual: entity reference to content typeindividual, widgetinline_entity_form_simple, not requiredfield_organization: entity reference to content typeorganization, widgetinline_entity_form_simple, not requiredindividualhas two required fields,organizationhas three. Exactly one of the two references is meant to be filled in per record.Conditional Fields rules on the
recordform display:field_individualvisible whenfield_party_typeequals the term "Individual"field_organizationvisible whenfield_party_typeequals the term "Organization"What we measured
Logging around the block this issue points at, on a real form POST (session from a one time login link,
form_build_idandform_tokentaken from the rendered markup) where "Organization" is selected and only that subform is filled in:field_organizationevaluates as triggered and returns early, as expected.field_individualevaluates as untriggered, and at that moment$form_statealready carries the errors of its embedded subform:$field_errors, the early return fires, and the dependent is never added toconditional_fields_untriggered_dependents.formValidate()then returns at its first guard, nothing is removed, and the form cannot be submitted in either direction.So in this case the errors already exist when
dependentValidate()runs, which is exactly what trips the early return. The subform is validated inside the widget's own element validation, so its violations are raised earlier than the entity level violations of an ordinary field.Applying the proposed resolution
We applied the resolution described in the issue summary locally (keep the collected errors in the form state addition and drop the
return):recordthat references a real organization also referenced a newly created, emptyindividualnode with no title and no field values. The mirror case created an emptyorganizationnode.That second part is inline_entity_form's own behaviour, tracked in #3099844 (still "needs work"): the widget builds and saves a child entity for an untouched, empty, optional IEF field. We are not suggesting this issue should solve that.
It is worth flagging though: with an IEF dependent, removing the errors alone turns a blocked form into silently created empty entities. Anyone testing a patch here against an IEF setup should check what ends up in the database, not only whether the form saves. A test covering an untriggered dependent whose values are discarded, not just whose errors are removed, would catch that.
I'll try to test a patch or MR on the setup above and report both the form result and what gets written to the database.
Comment #4
joelpittetRe #3 Can you post the code in an MR please from your work? It’s easier to see how the prose fits with what you tested against (instead of me reading between the lines…)
It’s hard to tell if that should be its own issue or be truly part of this one.
I don’t mind the AI output but could you trim the verbosity a bit please 🙏
Comment #6
orkutmuratyilmaz@joelpittet, sorry for the crowded paragraphs, I'm trying to trim them now:)
I just created an MR for the issue.
Comment #7
joelpittetThank you for the cleanup.
I’m leery of this change so might spend more time pondering 🤔 it.
I feel like I ran into this, it was annoying BUT if it’s untriggered shouldn’t it have a valid default?
There is a longstanding issue where Drupal has relied on form validation (rant coming) but as we move to jsonapi or even programmatic saves the validation is more important on the entity save for data integrity.
With that in mind, this bug sounds like a feature in disguise 🥸 as it prevents saves where the data is invalid…
A workaround I’ve used is setting a sane and valid default value. I know that doesn’t work in all cases.
I’m typing from my phone so not looking at the solution. But data integrity is really important with this type of fix.
So first look at the problem different ways and maybe even different results (valid but meaningful default) not saying this shouldn’t be fixed like this, just the issue worries me for the side effects it poses.
Comment #8
orkutmuratyilmazRe #7: you are right, this is a fair worry. My own measurement shows part of it. If we only remove the errors, Drupal saves an entity that its own field definition calls invalid. The required field stays empty. With an inline entity form we also get an empty child node. Drupal does not validate again on save, so nothing catches this later.
Still, three things make me doubt that it is a feature:
dependentValidate()promises the removal itself. Its docblock says the errors of an untriggered dependent "must be removed, together with its submitted values", so that the field looks like it is not in the form at all.formValidate()reads the form errors again for every registered dependent. An error raised afterdependentValidate(), for example an entity constraint, is already removed. Only an error raised before it stays. That is the required error core sets on a child element. The early return keeps the dependent out of the register, so nothing removes that error. The data is invalid in both cases, but one is blocked and the other is saved. It only depends on the moment the error appears.If data integrity is the line, I am happy to narrow the MR. We can remove the errors only when the result stays valid:
A required dependent with no reset would still block the form, so your protection stays. And for a field that should be required only while it is visible, the conditional "required" state is still the right tool.
Which direction do you prefer? I will reshape the MR and its test.
Also, the pipeline is green again, the
cspellfailure was a word in a comment I had written.