From e93fd13a1a49215ec83d04913c28a15ac9c4b48f Mon Sep 17 00:00:00 2001 From: Dalibor Korpar Date: Fri, 14 Aug 2026 19:58:25 +0200 Subject: [PATCH] fix: export rules added after DateInput's format rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DateInput registered its format check as a closure. Nette's Helpers::exportRules() breaks out of the loop on the first rule whose canExport() is false, and Rule::canExport() accepts only strings and static callables — never closures. The format rule is added in the constructor, so it sat in front of everything else and every addRule()/addCondition()/addConditionOn() registered afterwards was silently dropped from data-nette-rules. setRequired() kept working because Rules::addRule() stores the required rule in a separate slot instead of the ordered list, so it never sat behind the closure. Turning the check into a public static method makes the rule exportable and stops it truncating the rest. Server-side validation is unchanged. Client-side the rule now appears in data-nette-rules under a name netteForms.js has no validator for; validateRule() returns null for unknown validators and validateControl() skips it, so the browser moves on to the user's conditions. Closes #61 Co-Authored-By: Claude Opus 5 --- src/Inputs/DateInput.php | 18 +++++++++++++++++- tests/Inputs/DateInputTest.php | 27 +++++++++++++++++++++++++-- tests/Inputs/DateTimeInputTest.php | 30 ++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/Inputs/DateInput.php b/src/Inputs/DateInput.php index e237308..d559bc3 100644 --- a/src/Inputs/DateInput.php +++ b/src/Inputs/DateInput.php @@ -55,11 +55,27 @@ public function __construct($label = null, ?int $maxLength = null) parent::__construct($label, null); - $this->addRule(fn (Control $input) => DateTimeFormat::validate($this->format, $input->getValue()), $this->invalidFormatMessage); + // must be a static callable, otherwise Nette::exportRules() stops at this rule + // and every rule/condition added later is missing in data-nette-rules + $this->addRule([self::class, 'validateFormat'], $this->invalidFormatMessage); $this->setFormat(static::$defaultFormat); } + /** + * Checks that the value matches the control's format. + * + * Kept as a public static method (instead of a closure) so that the rule is exportable + * and does not cut off the rules that follow it. The client side has no validator of + * this name and simply skips it. + */ + public static function validateFormat(Control $input): bool + { + assert($input instanceof self); + + return DateTimeFormat::validate($input->getFormat(), $input->getValue()); + } + /** * @inheritdoc */ diff --git a/tests/Inputs/DateInputTest.php b/tests/Inputs/DateInputTest.php index 04582dd..7123b0b 100644 --- a/tests/Inputs/DateInputTest.php +++ b/tests/Inputs/DateInputTest.php @@ -5,16 +5,20 @@ use Contributte\FormsBootstrap\BootstrapForm; use Contributte\FormsBootstrap\Enums\DateTimeFormat; use Contributte\FormsBootstrap\Inputs\DateInput; +use Nette\Forms\Form; use Tests\BaseTestCase; class DateInputTest extends BaseTestCase { + // rule that DateInput adds in its constructor + private const FORMAT_RULE = '[{"op":"Contributte\\\\FormsBootstrap\\\\Inputs\\\\DateInput::validateFormat","msg":"invalid/incorrect format"}]'; + public function testDefaultDate(): void { $form = new BootstrapForm(); $dt = $form->addBootstrapDate('date', 'Date'); - $this->assertEquals('', $dt->getControl()->render()); + $this->assertEquals('', $dt->getControl()->render()); } public function testWithCustomStaticFormat(): void @@ -22,7 +26,26 @@ public function testWithCustomStaticFormat(): void $form = new BootstrapForm(); DateInput::$defaultFormat = DateTimeFormat::D_YMD_DASHES; $dt = $form->addBootstrapDate('date', 'Date'); - $this->assertEquals('', $dt->getControl()->render()); + $this->assertEquals('', $dt->getControl()->render()); + } + + /** + * @see https://github.com/contributte/forms-bootstrap/issues/61 + */ + public function testConditionsAreExported(): void + { + $form = new BootstrapForm(); + $checkbox = $form->addCheckbox('agree', 'Agree'); + $date = $form->addBootstrapDate('date', 'Date'); + $date->addConditionOn($checkbox, Form::Equal, true) + ->setRequired('Date is required'); + $date->addCondition(Form::Filled) + ->addRule(Form::MinLength, 'Too short', 3); + + $rules = $date->getControl()->render(); + $this->assertStringContainsString(':equal', $rules); + $this->assertStringContainsString(':filled', $rules); + $this->assertStringContainsString(':minLength', $rules); } } diff --git a/tests/Inputs/DateTimeInputTest.php b/tests/Inputs/DateTimeInputTest.php index 9867dd2..0ec9b23 100644 --- a/tests/Inputs/DateTimeInputTest.php +++ b/tests/Inputs/DateTimeInputTest.php @@ -11,11 +11,14 @@ class DateTimeInputTest extends BaseTestCase { + // rule that DateInput adds in its constructor + private const FORMAT_RULE = '[{"op":"Contributte\\\\FormsBootstrap\\\\Inputs\\\\DateInput::validateFormat","msg":"invalid/incorrect format"}]'; + public function testDefaultDateTime(): void { $form = new BootstrapForm(); $dt = $form->addBootstrapDateTime('datetime', 'Date and time'); - $this->assertEquals('', $dt->getControl()->render()); + $this->assertEquals('', $dt->getControl()->render()); } public function testDefaultAdditionalClasses(): void @@ -24,7 +27,7 @@ public function testDefaultAdditionalClasses(): void DateTimeInput::$additionalHtmlClasses[] = 'datetimepicker'; DateTimeInput::$additionalHtmlClasses[] = 'cool'; $dt = $form->addBootstrapDateTime('datetime', 'Date and time'); - $this->assertEquals('', $dt->getControl()->render()); + $this->assertEquals('', $dt->getControl()->render()); } public function testNotMessingHtmlClassOfDateAndDateTime(): void @@ -62,4 +65,27 @@ public function testValidationWithCorrectFormat(): void $this->assertEquals(new DateTime('2020-05-01'), $dt->getValue()); } + public function testValidationWithIncorrectFormat(): void + { + $form = new BootstrapForm(); + $dt = $form->addBootstrapDateTime('datetime', 'Date and time'); + $dt->setValue('not a date at all'); + $submit = $form->addSubmit('send'); + $form->setSubmittedBy($submit); + $form->validate(); + $this->assertFalse($form->isValid()); + $this->assertSame([$dt->invalidFormatMessage], $dt->getErrors()); + } + + public function testEmptyOptionalValueDoesNotFailValidation(): void + { + $form = new BootstrapForm(); + $dt = $form->addBootstrapDateTime('datetime', 'Date and time'); + $submit = $form->addSubmit('send'); + $form->setSubmittedBy($submit); + $form->validate(); + $this->assertTrue($form->isValid()); + $this->assertSame([], $dt->getErrors()); + } + }