Skip to content

Add Form::yieldElementsRecursive() to fix partial validation for FormElements#191

Merged
lippserd merged 6 commits intomainfrom
fix/partially-validate-fieldset-elements
Mar 26, 2026
Merged

Add Form::yieldElementsRecursive() to fix partial validation for FormElements#191
lippserd merged 6 commits intomainfrom
fix/partially-validate-fieldset-elements

Conversation

@jrauh01
Copy link
Copy Markdown
Contributor

@jrauh01 jrauh01 commented Mar 19, 2026

When a form is submitted via autosubmit, only partial validation runs. For elements containing other form elements (FormElements implementation), a single child having a value was enough to trigger full validation of the entire element, causing "field is required errors" to appear for fields that had just been revealed and were still empty.

Because initiating validation is the concern of Form, a yieldElementsRecursive() method is added there. It recursively yields all sub form elements and recurses into nested FormElements containers. This enables the form to check each individual element and validate only if it has a value.

fixes #190

@jrauh01 jrauh01 self-assigned this Mar 19, 2026
@cla-bot cla-bot Bot added the cla/signed label Mar 19, 2026
@nilmerg
Copy link
Copy Markdown
Member

nilmerg commented Mar 19, 2026

There were recent attempts to properly reflect functionality like this in interfaces. Now ipl\Html\Contract\FormElements exists due to this. Please consider extending this instead, given it's fairly new and we're about to introduce other equally fundamental changes.

@jrauh01
Copy link
Copy Markdown
Contributor Author

jrauh01 commented Mar 19, 2026

I'm not quite sure about the move to the trait as well, but as both 'validatePartial()` implementations were the same it seemed reasonable to me.

@nilmerg
Copy link
Copy Markdown
Member

nilmerg commented Mar 20, 2026

You've got a point there. My first thought yesterday was because \ipl\Html\Contract\FormElements describes something that well, has multiple form elements, why shouldn't it require to validate them. But without the possibility to validate them as a whole, this doesn't make much sense. But that cannot be required by this interface. \ipl\Html\Form also has multiple elements, but requires isValid(), so there is already a clear distinction between validation and element possession. \ipl\Html\Form::validate() isn't even itself part of an interface, it's an implementation detail. \ipl\Html\Form::validatePartial() has been one as well until now.

  • \ipl\Html\Contract\Form describes a form and how to interact with it
  • \ipl\Html\Contract\FormElements describes something that has multiple form elements and how to interact with them

validatePartial() is somewhat related to both, isn't it?

\ipl\Html\FormElement\FieldsetElement implements \ipl\Html\Contract\FormElements but should never need to implement \ipl\Html\Contract\Form. Though, the reason for partial validation is part of \ipl\Html\Contract\Form:

  • \ipl\Html\Contract\Form::hasBeenSent()
  • \ipl\Html\Contract\Form::hasBeenSubmitted()

Only in case a form is sent but not submitted, partial validation kicks in. Now delegated by the form to the fieldset.

So let's take another look at this. To partially validate a fieldset, which is a \ipl\Html\Contract\FormElement but also has \ipl\Html\Contract\FormElements, why shouldn't it be able to access the elements and validate them?

We could easily support this using a recursive \ipl\Html\Contract\FormElements::yieldElements() which is recursive because if it detects an element of its own, it yields from $element->yieldElements(). Though, I'm not quite sure either if requiring this in the interface is the right approach.

Let me know what you think :)

@jrauh01
Copy link
Copy Markdown
Contributor Author

jrauh01 commented Mar 20, 2026

I tried implementing the approach and if I understand it right, personally I like it a lot. Separating the concerns of validating and yielding through the form elements tree seems reasonable to me. What exactly are your concerns of adding such a yield function to the interface?

@jrauh01 jrauh01 force-pushed the fix/partially-validate-fieldset-elements branch from e960f05 to 4e52f84 Compare March 20, 2026 11:23
@nilmerg
Copy link
Copy Markdown
Member

nilmerg commented Mar 20, 2026

For one, extending the interface. But also that yieldElements() adds nothing, except the recursion part, that isn't there already in the form of getElements().

I already mentioned that validatePartial() is an implementation detail of \ipl\Html\Form to me. So we can also make the recursion part an implementation detail.

What about \ipl\Html\Form::(protected)yieldElements(FormElements $from): Generator what allows validatePartial() to access all nested elements?

@jrauh01
Copy link
Copy Markdown
Contributor Author

jrauh01 commented Mar 20, 2026

Sounds even better and would avoid interface changes completely.

@jrauh01 jrauh01 force-pushed the fix/partially-validate-fieldset-elements branch 2 times, most recently from b2a6bee to dbeb0cb Compare March 20, 2026 14:02
Comment thread src/Form.php Outdated
* @return $this
*/
public function validatePartial()
public function validatePartial(): static
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure actually that this belongs here. It's either already done by a dedicated PR or not possible due to high risk for a bc.

Copy link
Copy Markdown
Member

@nilmerg nilmerg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise it looks fine I think.

@nilmerg nilmerg added this to the v0.10.0 milestone Mar 20, 2026
@jrauh01 jrauh01 force-pushed the fix/partially-validate-fieldset-elements branch from dbeb0cb to a858f34 Compare March 23, 2026 05:42
@jrauh01
Copy link
Copy Markdown
Contributor Author

jrauh01 commented Mar 23, 2026

Should I squash the commits before this gets merged?

@jrauh01 jrauh01 changed the title Add FieldsetElement::validatePartial() to fix required errors Add Form::yieldElements() to fix partial validation for FormElements Mar 23, 2026
@lippserd lippserd removed this from the v0.10.0 milestone Mar 24, 2026
jrauh01 added 4 commits March 26, 2026 08:26
When a form is submitted via autosubmit, only partial validation runs. For
fieldset elements, a single child having a value was enough to trigger full
validation of the entire fieldset, causing required errors to appear for fields
that had just been revealed and were still empty.

Add a `validatePartial()` method to `FieldsetElement` that checks each child
element individually for whether it has a value before validating it, and
recurses into nested fieldsets. `Form::validatePartial()` is updated to detect
fieldset elements and call `validatePartial()` on them.
When a form is submitted via autosubmit, it is only partially validated. For sub
elements implementing the `FormElements` interface it was enough for one child
to have a value to trigger full validation of the entire container, causing
"field is required" errors on fields that had just appeared and were still
empty.

`validatePartial()` is declared to the interface to future implementors are
required to provide it as well, and added to the trait to satisfy the
declaration from the interface with a default implementation.
Instead of requiring `validatePartial()` in the `Contract\FormElements`
interface (and thus implementing it in the `FormElements` trait), introduce a
recursive `yieldElements()` generator that yields all sub form elements,
delegating into nested `FormElements` containers.

Compared to the previous approach, `validatePartial()` no longer needs to be
part of `Contract\FormElements`. The earlier implementation checked
`instanceof FormElements` to recurse manually. `yieldElements()` makes that
recursion reusable and separates traversal from validation logic.
Partial validation is driven solely by `Form`, via `hasBeenSent()` and
`hasBeenSubmitted()`. Exposing `yieldElements()` on the `FormElements` interface
would force every implementor to provide a method that currently only makes
sense in a form's validation context.

Moving it to `Form` as a protected helper method keeps traversal an
implementation detail of the form. Unlike the trait based approach,
`FormElements` implementors are no longer required to satisfy a contract that
has no relevance to them.
@jrauh01 jrauh01 force-pushed the fix/partially-validate-fieldset-elements branch from a858f34 to 45ebe6d Compare March 26, 2026 07:26
jrauh01 added 2 commits March 26, 2026 09:28
Describe the recursive behavior of the method explicit in its name.
Tests cover two cases:
- A fieldset with mixed children: only the child that has a value should be
  validated, while the empty required sibling should produce no error.
- A doubly nested fieldset: `validatePartial()` should recurse all the way down
  and validate the nested element.
@jrauh01 jrauh01 requested a review from lippserd March 26, 2026 09:18
@lippserd lippserd added this to the v0.10.1 milestone Mar 26, 2026
@lippserd lippserd merged commit 62de9c9 into main Mar 26, 2026
13 checks passed
@lippserd lippserd deleted the fix/partially-validate-fieldset-elements branch March 26, 2026 11:33
@lippserd lippserd changed the title Add Form::yieldElements() to fix partial validation for FormElements Add Form::yieldElementsRecursive() to fix partial validation for FormElements Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Required fields of fieldsets are invalid on autosubmit

3 participants