diff --git a/src/Form.php b/src/Form.php index c0ae6654..8a643126 100644 --- a/src/Form.php +++ b/src/Form.php @@ -2,11 +2,11 @@ namespace ipl\Html; +use Generator; use ipl\Html\Contract\DefaultFormElementDecoration; use ipl\Html\Contract\FormDecoration; use ipl\Html\Contract\FormElement; use ipl\Html\Contract\FormSubmitElement; -use ipl\Html\Contract\MutableHtml; use ipl\Html\FormDecoration\DecoratorChain; use ipl\Html\FormDecoration\FormDecorationResult; use ipl\Html\FormElement\FormElements; @@ -354,9 +354,7 @@ public function validate() */ public function validatePartial() { - $this->ensureAssembled(); - - foreach ($this->getElements() as $element) { + foreach ($this->yieldElementsRecursive($this) as $element) { if ($element->hasValue()) { $element->validate(); } @@ -442,4 +440,28 @@ protected function beforeRender(): void $this->applyDecoration(); } + + /** + * Yield all sub form elements of $from by traversing the form elements tree recursively + * + * Recurses into nested {@see FormElements} instances instead of yielding them directly. + * + * @param Contract\FormElements $from Entry point for traversing + * + * @return Generator + */ + protected function yieldElementsRecursive(Contract\FormElements $from): Generator + { + if ($from instanceof HtmlDocument) { + $from->ensureAssembled(); + } + + foreach ($from->getElements() as $element) { + if ($element instanceof Contract\FormElements) { + yield from $this->yieldElementsRecursive($element); + } else { + yield $element; + } + } + } } diff --git a/tests/FormTest.php b/tests/FormTest.php index 117d9b6c..a047d737 100644 --- a/tests/FormTest.php +++ b/tests/FormTest.php @@ -4,6 +4,7 @@ use ipl\Html\Form; use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\FormElement\FieldsetElement; use ipl\Html\Test\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -63,6 +64,46 @@ public function testEscapeReservedChars(): void $this->assertSame(Form::escapeReservedChars('foo-bar123'), 'foo-bar123'); } + public function testValidatePartialOnlyValidatesFieldsetChildrenWithAValue(): void + { + $fieldset = (new FieldsetElement('set')) + ->addElement('text', 'filled', ['required' => true]) + ->addElement('text', 'empty', ['required' => true]); + $fieldset->getElement('filled')->setValue('value'); + + $form = (new Form()) + ->addElement($fieldset) + ->validatePartial(); + + $this->assertTrue( + $form->getElement('set')->getElement('filled')->isValid(), + 'Fieldset child with a value is not validated during partial validation' + ); + $this->assertEmpty( + $form->getElement('set')->getElement('empty')->getMessages(), + 'Empty fieldset child produces a required error during partial validation' + ); + } + + public function testValidatePartialRecursesIntoNestedFieldsets(): void + { + $innerFieldset = (new FieldsetElement('inner')) + ->addElement('text', 'deep', ['required' => true]); + $innerFieldset->getElement('deep')->setValue('value'); + + $outerFieldset = (new FieldsetElement('outer')) + ->addElement($innerFieldset); + + $form = (new Form()) + ->addElement($outerFieldset) + ->validatePartial(); + + $this->assertTrue( + $form->getElement('outer')->getElement('inner')->getElement('deep')->isValid(), + 'Nested fieldset child with a value is not validated during partial validation' + ); + } + public function testFormElementsWithReservedCharsInName(): void { $form = new Form();