diff --git a/src/FormModel.php b/src/FormModel.php index ab7cf8fef..ee0bf063b 100644 --- a/src/FormModel.php +++ b/src/FormModel.php @@ -7,6 +7,7 @@ use Closure; use InvalidArgumentException; use ReflectionClass; +use Yiisoft\Form\HtmlOptions\HtmlOptionsProvider; use Yiisoft\Strings\Inflector; use Yiisoft\Strings\StringHelper; use Yiisoft\Validator\Rule\Required; @@ -45,6 +46,9 @@ public function isAttributeRequired(string $attribute): bool if ($validator instanceof Required) { return true; } + if ($validator instanceof HtmlOptionsProvider && (bool)($validator->getHtmlOptions()['required'] ?? false)) { + return true; + } } return false; @@ -235,39 +239,7 @@ public function addError(string $attribute, string $error): void $this->attributesErrors[$attribute][] = $error; } - /** - * Returns the validation rules for attributes. - * - * Validation rules are used by {@see \Yiisoft\Validator\Validator} to check if attribute values are valid. - * Child classes may override this method to declare different validation rules. - * - * Each rule is an array with the following structure: - * - * ```php - * public function rules(): array - * { - * return [ - * 'login' => $this->loginRules() - * ]; - * } - * - * private function loginRules(): array - * { - * return [ - * new \Yiisoft\Validator\Rule\Required(), - * (new \Yiisoft\Validator\Rule\HasLength()) - * ->min(4) - * ->max(40) - * ->tooShortMessage('Is too short.') - * ->tooLongMessage('Is too long.'), - * new \Yiisoft\Validator\Rule\Email() - * ]; - * } - * ``` - * - * @return array validation rules - */ - protected function rules(): array + public function rules(): array { return []; } diff --git a/src/FormModelInterface.php b/src/FormModelInterface.php index 378cec419..b6568e69e 100644 --- a/src/FormModelInterface.php +++ b/src/FormModelInterface.php @@ -209,6 +209,40 @@ public function formName(): string; */ public function load(array $data, ?string $formName = null): bool; + /** + * Returns the validation rules for attributes. + * + * Validation rules are used by {@see \Yiisoft\Validator\Validator} to check if attribute values are valid. + * Child classes may override this method to declare different validation rules. + * + * Each rule is an array with the following structure: + * + * ```php + * public function rules(): array + * { + * return [ + * 'login' => $this->loginRules() + * ]; + * } + * + * private function loginRules(): array + * { + * return [ + * new \Yiisoft\Validator\Rule\Required(), + * (new \Yiisoft\Validator\Rule\HasLength()) + * ->min(4) + * ->max(40) + * ->tooShortMessage('Is too short.') + * ->tooLongMessage('Is too long.'), + * new \Yiisoft\Validator\Rule\Email() + * ]; + * } + * ``` + * + * @return array Validation rules. + */ + public function rules(): array; + /** * Performs the data validation. * diff --git a/src/HtmlOptions/EmailHtmlOptions.php b/src/HtmlOptions/EmailHtmlOptions.php new file mode 100644 index 000000000..7f27d1c15 --- /dev/null +++ b/src/HtmlOptions/EmailHtmlOptions.php @@ -0,0 +1,25 @@ +rule = $rule; + } + + public function getHtmlOptions(): array + { + return [ + 'type' => 'email', + ]; + } +} diff --git a/src/HtmlOptions/HasLengthHtmlOptions.php b/src/HtmlOptions/HasLengthHtmlOptions.php new file mode 100644 index 000000000..6de06f5d0 --- /dev/null +++ b/src/HtmlOptions/HasLengthHtmlOptions.php @@ -0,0 +1,27 @@ +rule = $rule; + } + + public function getHtmlOptions(): array + { + $options = $this->rule->getOptions(); + return [ + 'minlength' => $options['min'], + 'maxlength' => $options['max'], + ]; + } +} diff --git a/src/HtmlOptions/HtmlOptionsProvider.php b/src/HtmlOptions/HtmlOptionsProvider.php new file mode 100644 index 000000000..944d2ace3 --- /dev/null +++ b/src/HtmlOptions/HtmlOptionsProvider.php @@ -0,0 +1,16 @@ + 'optionValue']` format. + */ + public function getHtmlOptions(): array; +} diff --git a/src/HtmlOptions/MatchRegularExpressionHtmlOptions.php b/src/HtmlOptions/MatchRegularExpressionHtmlOptions.php new file mode 100644 index 000000000..4868dd0d4 --- /dev/null +++ b/src/HtmlOptions/MatchRegularExpressionHtmlOptions.php @@ -0,0 +1,27 @@ +rule = $rule; + } + + public function getHtmlOptions(): array + { + $options = $this->rule->getOptions(); + return [ + 'pattern' => Html::normalizeRegexpPattern($options['pattern']), + ]; + } +} diff --git a/src/HtmlOptions/NumberHtmlOptions.php b/src/HtmlOptions/NumberHtmlOptions.php new file mode 100644 index 000000000..fe03a9d35 --- /dev/null +++ b/src/HtmlOptions/NumberHtmlOptions.php @@ -0,0 +1,28 @@ +rule = $rule; + } + + public function getHtmlOptions(): array + { + $options = $this->rule->getOptions(); + return [ + 'type' => 'number', + 'min' => $options['min'], + 'max' => $options['max'], + ]; + } +} diff --git a/src/HtmlOptions/RequiredHtmlOptions.php b/src/HtmlOptions/RequiredHtmlOptions.php new file mode 100644 index 000000000..ec2a4aa3f --- /dev/null +++ b/src/HtmlOptions/RequiredHtmlOptions.php @@ -0,0 +1,35 @@ +rule = $rule; + } + + public function withAriaAttribute(bool $value): self + { + $new = clone $this; + $new->ariaAttribute = $value; + return $new; + } + + public function getHtmlOptions(): array + { + return [ + 'required' => true, + 'aria-required' => $this->ariaAttribute ? 'true' : false, + ]; + } +} diff --git a/src/HtmlOptions/RuleAwareTrait.php b/src/HtmlOptions/RuleAwareTrait.php new file mode 100644 index 000000000..775c86fa5 --- /dev/null +++ b/src/HtmlOptions/RuleAwareTrait.php @@ -0,0 +1,19 @@ +rule->validate($value, $dataSet, $previousRulesErrored); + } +} diff --git a/src/Widget/Field.php b/src/Widget/Field.php index 8daafa954..ca0bdf080 100644 --- a/src/Widget/Field.php +++ b/src/Widget/Field.php @@ -11,7 +11,6 @@ use Yiisoft\Form\Helper\HtmlForm; use Yiisoft\Html\Html; use Yiisoft\Widget\Widget; - use function array_merge; use function strtr; @@ -284,7 +283,7 @@ public function input(string $type, array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = Input::widget() ->type($type) @@ -323,7 +322,7 @@ public function textInput(array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = TextInput::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -359,7 +358,7 @@ public function hiddenInput(array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{label}'] = ''; $this->parts['{hint}'] = ''; @@ -398,7 +397,7 @@ public function passwordInput(array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = PasswordInput::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -436,7 +435,7 @@ public function fileInput(array $options = [], bool $withoutHiddenInput = false) unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = FileInput::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -472,7 +471,7 @@ public function textArea(array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = TextArea::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -622,7 +621,7 @@ public function dropDownList(array $items, array $options = []): self unset($options['class']); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = DropDownList::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -663,7 +662,7 @@ public function listBox(array $items, array $options = []): self $new->setForInLabel($options); $new->setAriaAttributes($options); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $this->parts['{input}'] = ListBox::widget() ->config($new->data, $new->attribute, $new->inputOptions) @@ -697,7 +696,7 @@ public function checkboxList(array $items, array $options = []): self $new->setForInLabel($options); $new->setAriaAttributes($options); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $new->skipForInLabel = true; $this->parts['{input}'] = CheckBoxList::widget() @@ -735,7 +734,7 @@ public function radioList(array $items, array $options = []): self $new->addErrorCssClassToInput(); $new->addSuccessCssClassToInput(); $new->setInputRole($options); - $new->inputOptions = array_merge($options, $new->inputOptions); + $new->inputOptions = array_merge($new->inputOptions, $options); $new->skipForInLabel = true; $this->parts['{input}'] = RadioList::widget() @@ -971,10 +970,6 @@ private function setForInLabel(array $options = []): void private function setAriaAttributes(array $options = []): void { if ($this->ariaAttribute) { - if (!isset($options['aria-required']) && $this->data->isAttributeRequired($this->attribute)) { - $this->inputOptions['aria-required'] = 'true'; - } - if (!isset($options['aria-invalid']) && $this->data->hasErrors($this->attribute)) { $this->inputOptions['aria-invalid'] = 'true'; } diff --git a/src/Widget/Input.php b/src/Widget/Input.php index 5746024ba..ad26c45f8 100644 --- a/src/Widget/Input.php +++ b/src/Widget/Input.php @@ -7,9 +7,9 @@ use Yiisoft\Arrays\ArrayHelper; use Yiisoft\Form\FormModelInterface; use Yiisoft\Form\Helper\HtmlForm; +use Yiisoft\Form\HtmlOptions\HtmlOptionsProvider; use Yiisoft\Html\Html; use Yiisoft\Widget\Widget; - use function in_array; final class Input extends Widget @@ -57,7 +57,13 @@ public function config(FormModelInterface $data, string $attribute, array $optio $new = clone $this; $new->data = $data; $new->attribute = $attribute; - $new->options = $options; + $rules = $data->rules()[$attribute] ?? []; + foreach ($rules as $rule) { + if ($rule instanceof HtmlOptionsProvider) { + $new->options = array_merge($new->options, $rule->getHtmlOptions()); + } + } + $new->options = array_merge($new->options, $options); return $new; } diff --git a/tests/FormModelTest.php b/tests/FormModelTest.php index 8e5b9ab5a..a910cfa0b 100644 --- a/tests/FormModelTest.php +++ b/tests/FormModelTest.php @@ -348,7 +348,7 @@ public function attributeHints(): array ]; } - protected function rules(): array + public function rules(): array { return [ 'id' => new Required(), diff --git a/tests/HtmlOptions/FieldInputTest.php b/tests/HtmlOptions/FieldInputTest.php new file mode 100644 index 000000000..e49c2d444 --- /dev/null +++ b/tests/HtmlOptions/FieldInputTest.php @@ -0,0 +1,103 @@ +config($data, $propertyName) + ->run(); + $this->assertEqualsWithoutLE($expectedHtml, $actualHtml); + } + + public function htmlOptionsDataProvider(): array + { + return [ + 'number' => [ + 'number', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + 'hasLength' => [ + 'hasLength', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + 'required' => [ + 'required', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + 'pattern' => [ + 'pattern', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + 'email' => [ + 'email', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + 'combined' => [ + 'combined', + <<<'HTML' +
+ + + +
+
+ HTML, + ], + ]; + } +} diff --git a/tests/Stub/HtmlOptionsForm.php b/tests/Stub/HtmlOptionsForm.php new file mode 100644 index 000000000..9331d8670 --- /dev/null +++ b/tests/Stub/HtmlOptionsForm.php @@ -0,0 +1,93 @@ + [ + $this->getNumberHtmlOptions(), + ], + 'hasLength' => [ + $this->getHasLengthHtmlOptions(), + ], + 'required' => [ + $this->getRequiredHtmlOptions(), + ], + 'pattern' => [ + $this->getMatchRegularExpressionHtmlOptions(), + ], + 'email' => [ + $this->getEmailHtmlOptions(), + ], + 'combined' => [ + $this->getNumberHtmlOptions(), + $this->getHasLengthHtmlOptions(), + $this->getRequiredHtmlOptions(), + $this->getMatchRegularExpressionHtmlOptions(), + ], + ]; + } + + private function getMatchRegularExpressionHtmlOptions(): MatchRegularExpressionHtmlOptions + { + return new MatchRegularExpressionHtmlOptions( + new MatchRegularExpression('/\w+/') + ); + } + + private function getEmailHtmlOptions(): EmailHtmlOptions + { + return new EmailHtmlOptions( + new Email() + ); + } + + private function getRequiredHtmlOptions(): RequiredHtmlOptions + { + return new RequiredHtmlOptions( + new Required() + ); + } + + private function getHasLengthHtmlOptions(): HasLengthHtmlOptions + { + return new HasLengthHtmlOptions( + (new HasLength()) + ->min(4)->tooShortMessage('Is too short.') + ->max(5)->tooLongMessage('Is too long.') + ); + } + + private function getNumberHtmlOptions(): NumberHtmlOptions + { + return new NumberHtmlOptions( + (new Number()) + ->min(4)->tooSmallMessage('Is too small.') + ->max(5)->tooBigMessage('Is too big.') + ); + } +} diff --git a/tests/Stub/LoginForm.php b/tests/Stub/LoginForm.php index d8c709a1f..baff63192 100644 --- a/tests/Stub/LoginForm.php +++ b/tests/Stub/LoginForm.php @@ -63,7 +63,7 @@ public function attributeLabels(): array ]; } - protected function rules(): array + public function rules(): array { return [ 'login' => $this->loginRules(), diff --git a/tests/Stub/PersonalForm.php b/tests/Stub/PersonalForm.php index 8bb4716e9..2584cbd03 100644 --- a/tests/Stub/PersonalForm.php +++ b/tests/Stub/PersonalForm.php @@ -5,6 +5,7 @@ namespace Yiisoft\Form\Tests\Stub; use Yiisoft\Form\FormModel; +use Yiisoft\Form\HtmlOptions\RequiredHtmlOptions; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\HasLength; use Yiisoft\Validator\Rule\MatchRegularExpression; @@ -48,13 +49,13 @@ public function customErrorWithIcon(): string return '(✖) This is custom error message.'; } - protected function rules(): array + public function rules(): array { return [ 'name' => [new Required(), (new HasLength())->min(4)->tooShortMessage('Is too short.')], 'email' => [new Email()], 'password' => [ - new Required(), + (new RequiredHtmlOptions(new Required()))->withAriaAttribute(true), (new MatchRegularExpression("/(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/")) ->message( 'Must contain at least one number and one uppercase and lowercase letter, and at least 8 or ' . diff --git a/tests/Widget/FieldErrorTest.php b/tests/Widget/FieldErrorTest.php index dde9d8814..82380a691 100644 --- a/tests/Widget/FieldErrorTest.php +++ b/tests/Widget/FieldErrorTest.php @@ -21,7 +21,7 @@ public function testFieldError(): void $expected = <<<'HTML'
- +
Write your first name.
Is too short.
@@ -42,7 +42,7 @@ public function testFieldErrorOptions(): void $expected = <<<'HTML'
- +
Write your first name.
Is too short.
diff --git a/tests/Widget/FieldHintTest.php b/tests/Widget/FieldHintTest.php index 8cd62b4a4..e1f7453d8 100644 --- a/tests/Widget/FieldHintTest.php +++ b/tests/Widget/FieldHintTest.php @@ -17,7 +17,7 @@ public function testFieldsHint(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -35,7 +35,7 @@ public function testFieldHintCustom(): void $expected = <<<'HTML'
- +
Custom hint.
@@ -54,7 +54,7 @@ public function testFieldAnyHint(): void $expected = <<<'HTML'
- +
diff --git a/tests/Widget/FieldPasswordInputTest.php b/tests/Widget/FieldPasswordInputTest.php index adf1f7c20..629ec23eb 100644 --- a/tests/Widget/FieldPasswordInputTest.php +++ b/tests/Widget/FieldPasswordInputTest.php @@ -22,7 +22,7 @@ public function testFieldsPasswordInput(): void $expected = <<<'HTML'
- +
Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters.
@@ -41,7 +41,7 @@ public function testFieldsPasswordInputWithLabelCustom(): void $expected = <<<'HTML'
- +
@@ -61,7 +61,7 @@ public function testFieldsPasswordInputAnyLabel(): void $expected = <<<'HTML'
- +
diff --git a/tests/Widget/FieldSuccessTest.php b/tests/Widget/FieldSuccessTest.php index 802126101..742ddb878 100644 --- a/tests/Widget/FieldSuccessTest.php +++ b/tests/Widget/FieldSuccessTest.php @@ -21,7 +21,7 @@ public function testFieldSuccess(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -42,7 +42,7 @@ public function testFieldErrorOptions(): void $expected = <<<'HTML'
- +
Write your first name.
diff --git a/tests/Widget/FieldTest.php b/tests/Widget/FieldTest.php index 32f5b30f7..a9a62be8d 100644 --- a/tests/Widget/FieldTest.php +++ b/tests/Widget/FieldTest.php @@ -34,7 +34,7 @@ public function testFieldAriaAttributes(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -50,7 +50,7 @@ public function testFieldAriaAttributes(): void $expected = <<<'HTML'
- +
Write your first name.
Is too short.
@@ -68,7 +68,7 @@ public function testEnclosedByContainer(): void $expected = <<<'HTML' - +
Write your first name.
HTML; diff --git a/tests/Widget/FieldTextInputTest.php b/tests/Widget/FieldTextInputTest.php index 0f4e6c073..7c8362d58 100644 --- a/tests/Widget/FieldTextInputTest.php +++ b/tests/Widget/FieldTextInputTest.php @@ -17,7 +17,7 @@ public function testFieldTextInput(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -35,7 +35,7 @@ public function testFieldTextInputWithLabelCustom(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -54,7 +54,7 @@ public function testFieldTextInputAnyLabel(): void $expected = <<<'HTML'
- +
Write your first name.
diff --git a/tests/Widget/FormTest.php b/tests/Widget/FormTest.php index b9a2a744f..37fdf67c4 100644 --- a/tests/Widget/FormTest.php +++ b/tests/Widget/FormTest.php @@ -139,7 +139,7 @@ public function testFormsFieldsOptions(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -155,7 +155,7 @@ public function testFormsFieldsOptions(): void $expected = <<<'HTML'
- +
Write your first name.
@@ -239,7 +239,7 @@ public function testFormsFieldsValidationOnContainer(): void $expected = <<<'HTML'
- +
Write your first name.
Is too short.
@@ -269,7 +269,7 @@ public function testFormsFieldsValidationOnInput(): void $expected = <<<'HTML'
- +
Write your first name.
Is too short.
- +
Write your first name.
diff --git a/tests/Widget/PasswordInputTest.php b/tests/Widget/PasswordInputTest.php index 5652dc817..721c9d130 100644 --- a/tests/Widget/PasswordInputTest.php +++ b/tests/Widget/PasswordInputTest.php @@ -15,7 +15,7 @@ public function testPasswordInput(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -28,7 +28,7 @@ public function testPasswordInputOptions(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password', ['class' => 'customClass']) @@ -41,7 +41,7 @@ public function testPasswordInputAutofocus(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -55,7 +55,7 @@ public function testPasswordInputDisabled(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -69,7 +69,7 @@ public function testPasswordInputForm(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -83,7 +83,7 @@ public function testPasswordInputMinLength(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -97,7 +97,7 @@ public function testPasswordInputMaxLength(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -111,7 +111,7 @@ public function testPasswordInputNoPlaceholder(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -125,7 +125,7 @@ public function testPasswordInputPattern(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -141,7 +141,7 @@ public function testPasswordInputPlaceholder(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -155,7 +155,7 @@ public function testPasswordInputReadOnly(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -169,7 +169,7 @@ public function testPasswordInputRequired(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') @@ -183,7 +183,7 @@ public function testPasswordInputTabIndex(): void $data = new PersonalForm(); $expected = <<<'HTML' - + HTML; $html = PasswordInput::widget() ->config($data, 'password') diff --git a/tests/Widget/TextInputTest.php b/tests/Widget/TextInputTest.php index a3c40af11..6d11a3c21 100644 --- a/tests/Widget/TextInputTest.php +++ b/tests/Widget/TextInputTest.php @@ -195,6 +195,19 @@ public function testTextInputRequired(): void $this->assertEquals($expected, $html); } + public function testAriaRequiredProvidedByRule(): void + { + $data = new PersonalForm(); + + $expected = <<<'HTML' + +HTML; + $html = TextInput::widget() + ->config($data, 'password') + ->run(); + $this->assertEquals($expected, $html); + } + public function testTextInputTabIndex(): void { $data = new PersonalForm();