From 91c6cf7321607cad5535767d61e245e3f1829ca6 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sun, 10 Feb 2019 17:53:02 +0100 Subject: [PATCH] added architecture components --- .gitattributes | 6 + .gitignore | 2 + .phpstan.neon | 8 + .travis.yml | 34 ++ composer.json | 43 ++ license.md | 75 +++ readme.md | 35 ++ src/Fragments/Traits/BaseControlTrait.php | 584 ++++++++++++++++++++++ src/Fragments/Traits/TextBaseTrait.php | 149 ++++++ src/Fragments/Traits/TextInputTrait.php | 112 +++++ src/Fragments/UIComponent/BaseControl.php | 20 + src/Fragments/UIControl/BaseControl.php | 20 + src/Fragments/UIControl/TextBase.php | 18 + src/Fragments/UIControl/TextInput.php | 18 + tests/.coveralls.yml | 4 + tests/bootstrap.php | 6 + tests/php.ini | 2 + tests/php.unix-sample.ini | 3 + tests/php.win-sample.ini | 2 + tests/run.sh | 3 + 20 files changed, 1144 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .phpstan.neon create mode 100644 .travis.yml create mode 100644 composer.json create mode 100644 license.md create mode 100644 readme.md create mode 100644 src/Fragments/Traits/BaseControlTrait.php create mode 100644 src/Fragments/Traits/TextBaseTrait.php create mode 100644 src/Fragments/Traits/TextInputTrait.php create mode 100644 src/Fragments/UIComponent/BaseControl.php create mode 100644 src/Fragments/UIControl/BaseControl.php create mode 100644 src/Fragments/UIControl/TextBase.php create mode 100644 src/Fragments/UIControl/TextInput.php create mode 100644 tests/.coveralls.yml create mode 100644 tests/bootstrap.php create mode 100644 tests/php.ini create mode 100644 tests/php.unix-sample.ini create mode 100644 tests/php.win-sample.ini create mode 100755 tests/run.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..35b4052 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore +tests/ export-ignore +*.sh eol=lf +composer.json eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4a392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/.phpstan.neon b/.phpstan.neon new file mode 100644 index 0000000..2e972a4 --- /dev/null +++ b/.phpstan.neon @@ -0,0 +1,8 @@ +includes: + - vendor/phpstan/phpstan-nette/extension.neon + - vendor/phpstan/phpstan-nette/rules.neon + +parameters: + ignoreErrors: + excludes_analyse: + - %currentWorkingDirectory%/src/Fragments/Traits/* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..31195a6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + +matrix: + fast_finish: true + +cache: + directories: + - $HOME/.composer/cache + +before_script: + - composer install --no-interaction --prefer-source + + - cp ./tests/php.unix-sample.ini ./tests/php.ini + + - if [ "$TRAVIS_PHP_VERSION" == "7.3" ]; then cat ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini >> ./tests/php.ini; fi + - if [ "$TRAVIS_PHP_VERSION" == "7.3" ]; then NTESTER_FLAGS="--coverage ./coverage.xml --coverage-src ./src"; else TESTER_FLAGS=""; fi + +script: + - ./tests/run.sh -s $NTESTER_FLAGS ./tests/cases + - if [ "$TRAVIS_PHP_VERSION" == "7.3" ]; then vendor/bin/phpstan analyze src -l 7 -c .phpstan.neon; fi + +after_script: + - if [ "$TRAVIS_PHP_VERSION" == "7.3" ]; then composer require php-coveralls/php-coveralls; fi + - if [ "$TRAVIS_PHP_VERSION" == "7.3" ]; then php vendor/bin/php-coveralls -c tests/.coveralls.yml -v; fi + +after_failure: + # Print *.actual content & log content + - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + - for i in $(find tests -name \*.log); do echo "--- $i"; cat $i; echo; echo; done diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0c1d78d --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "nextras/form-components", + "type": "library", + "description": "Form components for Nette Framework.", + "keywords": ["nette", "form", "components"], + "license": ["MIT", "BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "authors": [ + { + "name": "Nextras Community", + "homepage": "https://github.com/nextras/form-components/graphs/contributors" + } + ], + "require": { + "php": ">=7.1", + "nette/application": "~3.0", + "nette/component-model": "~3.0", + "nette/forms": "~3.0", + "nette/utils": "~3.0" + }, + "require-dev": { + "latte/latte": "~3.0", + "nette/bootstrap": "~3.0", + "nette/di": "~3.0", + "nette/robot-loader": "~3.0", + "nette/tester": "~2.1", + "phpstan/phpstan-nette": "0.11", + "phpstan/phpstan-shim": "0.11.1", + "tracy/tracy": "~2.5" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "minimum-stability": "beta", + "prefer-stable": true, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { "Nextras\\FormComponents\\": "src/" } + } +} diff --git a/license.md b/license.md new file mode 100644 index 0000000..a5d0ef4 --- /dev/null +++ b/license.md @@ -0,0 +1,75 @@ +Licenses +======== + +**License for the library except files extracted from Nette Framework:** + +MIT License +----------- + +Copyright (c) 2014 Nextras Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +**License for `src/Fragments/Traits/*` files extracted from Nette Framework:** + +Good news! You may use Nette Framework under the terms of either +the New BSD License or the GNU General Public License (GPL) version 2 or 3. + +The BSD License is recommended for most projects. It is easy to understand and it +places almost no restrictions on what you can do with the framework. If the GPL +fits better to your project, you can use the framework under this license. + +You don't have to notify anyone which license you are using. You can freely +use Nette Framework in commercial projects as long as the copyright header +remains intact. + +Please be advised that the name "Nette Framework" is a protected trademark and its +usage has some limitations. So please do not use word "Nette" in the name of your +project or top-level domain, and choose a name that stands on its own merits. +If your stuff is good, it will not take long to establish a reputation for yourselves. + + +New BSD License +--------------- + +Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of "Nette Framework" nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are +disclaimed. In no event shall the copyright owner or contributors be liable for +any direct, indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused and on +any theory of liability, whether in contract, strict liability, or tort +(including negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + + +GNU General Public License +-------------------------- + +GPL licenses are very very long, so instead of including them here we offer +you URLs with full text: + +- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) +- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..96e1c80 --- /dev/null +++ b/readme.md @@ -0,0 +1,35 @@ +Nextras Form Components +======================== + +[![Build Status](https://travis-ci.org/nextras/form-components.svg?branch=master)](https://travis-ci.org/nextras/form-components) +[![Downloads this Month](https://img.shields.io/packagist/dm/nextras/form-components.svg?style=flat)](https://packagist.org/packages/nextras/form-components) +[![Stable version](http://img.shields.io/packagist/v/nextras/form-components.svg?style=flat)](https://packagist.org/packages/nextras/form-components) +[![Code coverage](https://img.shields.io/coveralls/nextras/form-components.svg?style=flat)](https://coveralls.io/r/nextras/form-components) + +This package provides architecture and UI components for building Nette forms. + +Architecture components provide Nette Forms' BaseControl in two flavors: +- BaseControl that inherits from `Nette\Application\UI\Component` - form control with support for signal & state handling; +- BaseControl that inherits from `Nette\Application\UI\Control` - form control with support for template rendering + same feature as in UI\Component; + +UI components: +- *AutocompleteControl* - text input with support for autocomplete signal handling; +- *DatePicker* - date picker - text input returning `DateTimeImmutable` instance; +- *DateTimePicker* - date picker - text input returning `DateTimeImmutable` instance; + +### Installation + +The best way to install is using [Composer](http://getcomposer.org/): + +```sh +$ composer require nextras/form-components +``` + +### Documentation + +See examples directory. + + +### License + +Combined MIT and Nette's . See full [license](license.md). diff --git a/src/Fragments/Traits/BaseControlTrait.php b/src/Fragments/Traits/BaseControlTrait.php new file mode 100644 index 0000000..e64af48 --- /dev/null +++ b/src/Fragments/Traits/BaseControlTrait.php @@ -0,0 +1,584 @@ +control = Html::el('input', ['type' => null, 'name' => null]); + $this->label = Html::el('label'); + $this->caption = $caption; + $this->rules = new Rules($this); + if (self::$autoOptional) { + $this->setRequired(false); + } + $this->setValue(null); + $this->monitor(Form::class, function (Form $form): void { + if (!$this->isDisabled() && $form->isAnchored() && $form->isSubmitted()) { + $this->loadHttpData(); + } + }); + } + + + /** + * Sets textual caption or label. + * @param object|string $caption + * @return static + */ + public function setCaption($caption) + { + $this->caption = $caption; + return $this; + } + + + /** + * @return object|string + */ + public function getCaption() + { + return $this->caption; + } + + + /** + * Returns form. + */ + public function getForm(bool $throw = true): ?Form + { + return $this->lookup(Form::class, $throw); + } + + + /** + * Loads HTTP data. + */ + public function loadHttpData(): void + { + $this->setValue($this->getHttpData(Form::DATA_TEXT)); + } + + + /** + * Loads HTTP data. + * @return mixed + */ + protected function getHttpData($type, string $htmlTail = null) + { + return $this->getForm()->getHttpData($type, $this->getHtmlName() . $htmlTail); + } + + + /** + * Returns HTML name of control. + */ + public function getHtmlName(): string + { + return Nette\Forms\Helpers::generateHtmlName($this->lookupPath(Form::class)); + } + + + /********************* interface IControl ****************d*g**/ + + + /** + * Sets control's value. + * @return static + * @internal + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + + /** + * Returns control's value. + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + + /** + * Is control filled? + */ + public function isFilled(): bool + { + $value = $this->getValue(); + return $value !== null && $value !== [] && $value !== ''; + } + + + /** + * Sets control's default value. + * @return static + */ + public function setDefaultValue($value) + { + $form = $this->getForm(false); + if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) { + $this->setValue($value); + } + return $this; + } + + + /** + * Disables or enables control. + * @return static + */ + public function setDisabled(/*bool*/ $value = true) + { + if ($this->disabled = (bool) $value) { + $this->setValue(null); + } elseif (($form = $this->getForm(false)) && $form->isAnchored() && $form->isSubmitted()) { + $this->loadHttpData(); + } + return $this; + } + + + /** + * Is control disabled? + */ + public function isDisabled(): bool + { + return $this->disabled === true; + } + + + /** + * Sets whether control value is excluded from $form->getValues() result. + * @return static + */ + public function setOmitted(bool $value = true) + { + $this->omitted = $value; + return $this; + } + + + /** + * Is control value excluded from $form->getValues() result? + */ + public function isOmitted(): bool + { + return $this->omitted || ($this->isDisabled() && $this->omitted === null); + } + + + /********************* rendering ****************d*g**/ + + + /** + * Generates control's HTML element. + * @return Html|string + */ + public function getControl() + { + $this->setOption('rendered', true); + $el = clone $this->control; + return $el->addAttributes([ + 'name' => $this->getHtmlName(), + 'id' => $this->getHtmlId(), + 'required' => $this->isRequired(), + 'disabled' => $this->isDisabled(), + 'data-nette-rules' => Nette\Forms\Helpers::exportRules($this->rules) ?: null, + ]); + } + + + /** + * Generates label's HTML element. + * @param string|object $caption + * @return Html|string + */ + public function getLabel($caption = null) + { + $label = clone $this->label; + $label->for = $this->getHtmlId(); + $caption = $caption === null ? $this->caption : $caption; + $translator = $this->getForm()->getTranslator(); + $label->setText($translator ? $translator->translate($caption) : $caption); + return $label; + } + + + public function getControlPart(): ?Html + { + return $this->getControl(); + } + + + public function getLabelPart(): ?Html + { + return $this->getLabel(); + } + + + /** + * Returns control's HTML element template. + */ + public function getControlPrototype(): Html + { + return $this->control; + } + + + /** + * Returns label's HTML element template. + */ + public function getLabelPrototype(): Html + { + return $this->label; + } + + + /** + * Changes control's HTML id. + * @param string|bool|null $id + * @return static + */ + public function setHtmlId($id) + { + $this->control->id = $id; + return $this; + } + + + /** + * Returns control's HTML id. + * @return mixed + */ + public function getHtmlId() + { + if (!isset($this->control->id)) { + $form = $this->getForm(); + $prefix = $form instanceof Nette\Application\UI\Form || $form->getName() === null + ? '' + : $form->getName() . '-'; + $this->control->id = sprintf(self::$idMask, $prefix . $this->lookupPath()); + } + return $this->control->id; + } + + + /** + * Changes control's HTML attribute. + * @return static + */ + public function setHtmlAttribute(string $name, $value = true) + { + $this->control->$name = $value; + return $this; + } + + + /** + * @deprecated use setHtmlAttribute() + * @return static + */ + public function setAttribute(string $name, $value = true) + { + return $this->setHtmlAttribute($name, $value); + } + + + /********************* translator ****************d*g**/ + + + /** + * Sets translate adapter. + * @return static + */ + public function setTranslator(?Nette\Localization\ITranslator $translator) + { + $this->translator = $translator; + return $this; + } + + + /** + * Returns translate adapter. + */ + public function getTranslator(): ?Nette\Localization\ITranslator + { + if ($this->translator === true) { + return $this->getForm(false) ? $this->getForm()->getTranslator() : null; + } + return $this->translator; + } + + + /** + * Returns translated string. + * @return mixed + */ + public function translate($value, ...$parameters) + { + if ($translator = $this->getTranslator()) { + $tmp = is_array($value) ? [&$value] : [[&$value]]; + foreach ($tmp[0] as &$v) { + if ($v != null && !$v instanceof Html) { // intentionally == + $v = $translator->translate($v, ...$parameters); + } + } + } + return $value; + } + + + /********************* rules ****************d*g**/ + + + /** + * Adds a validation rule. + * @param callable|string $validator + * @param string|object $errorMessage + * @return static + */ + public function addRule($validator, $errorMessage = null, $arg = null) + { + $this->rules->addRule($validator, $errorMessage, $arg); + return $this; + } + + + /** + * Adds a validation condition a returns new branch. + * @return Rules new branch + */ + public function addCondition($validator, $value = null): Rules + { + return $this->rules->addCondition($validator, $value); + } + + + /** + * Adds a validation condition based on another control a returns new branch. + * @return Rules new branch + */ + public function addConditionOn(IControl $control, $validator, $value = null): Rules + { + return $this->rules->addConditionOn($control, $validator, $value); + } + + + public function getRules(): Rules + { + return $this->rules; + } + + + /** + * Makes control mandatory. + * @param bool|string|object $value + * @return static + */ + public function setRequired($value = true) + { + $this->rules->setRequired($value); + return $this; + } + + + /** + * Is control mandatory? + */ + public function isRequired(): bool + { + return $this->rules->isRequired(); + } + + + /** + * Performs the server side validation. + */ + public function validate(): void + { + if ($this->isDisabled()) { + return; + } + $this->cleanErrors(); + $this->rules->validate(); + } + + + /** + * Adds error message to the list. + * @param string|object $message + */ + public function addError($message, bool $translate = true): void + { + $this->errors[] = $translate ? $this->translate($message) : $message; + } + + + /** + * Returns errors corresponding to control. + */ + public function getError(): ?string + { + return $this->errors ? implode(' ', array_unique($this->errors)) : null; + } + + + /** + * Returns errors corresponding to control. + */ + public function getErrors(): array + { + return array_unique($this->errors); + } + + + public function hasErrors(): bool + { + return (bool) $this->errors; + } + + + public function cleanErrors(): void + { + $this->errors = []; + } + + + /** + * Globally enables new required/optional behavior. + * This method will be deprecated in next version. + */ + public static function enableAutoOptionalMode(): void + { + self::$autoOptional = true; + } + + + /********************* user data ****************d*g**/ + + + /** + * Sets user-specific option. + * @return static + */ + public function setOption($key, $value) + { + if ($value === null) { + unset($this->options[$key]); + } else { + $this->options[$key] = $value; + } + return $this; + } + + + /** + * Returns user-specific option. + * @return mixed + */ + public function getOption($key, $default = null) + { + return $this->options[$key] ?? $default; + } + + + /** + * Returns user-specific options. + */ + public function getOptions(): array + { + return $this->options; + } + + + /********************* extension methods ****************d*g**/ + + + public function __call(string $name, array $args) + { + $class = static::class; + do { + if (isset(self::$extMethods[$name][$class])) { + return (self::$extMethods[$name][$class])($this, ...$args); + } + $class = get_parent_class($class); + } while ($class); + return parent::__call($name, $args); + } + + + public static function extensionMethod(string $name, /*callable*/ $callback): void + { + if (strpos($name, '::') !== false) { // back compatibility + [, $name] = explode('::', $name); + } + self::$extMethods[$name][static::class] = $callback; + } +} diff --git a/src/Fragments/Traits/TextBaseTrait.php b/src/Fragments/Traits/TextBaseTrait.php new file mode 100644 index 0000000..f66c87b --- /dev/null +++ b/src/Fragments/Traits/TextBaseTrait.php @@ -0,0 +1,149 @@ +name)); + } + $this->value = $value; + $this->rawValue = (string) $value; + return $this; + } + + + /** + * Returns control's value. + * @return mixed + */ + public function getValue() + { + $value = $this->value === Strings::trim($this->translate($this->emptyValue)) ? '' : $this->value; + return $this->nullable && $value === '' ? null : $value; + } + + + /** + * Sets whether getValue() returns null instead of empty string. + * @return static + */ + public function setNullable(bool $value = true) + { + $this->nullable = $value; + return $this; + } + + + /** + * Sets the special value which is treated as empty string. + * @return static + */ + public function setEmptyValue(string $value) + { + $this->emptyValue = $value; + return $this; + } + + + /** + * Returns the special value which is treated as empty string. + */ + public function getEmptyValue(): string + { + return $this->emptyValue; + } + + + /** + * Sets the maximum number of allowed characters. + * @return static + */ + public function setMaxLength(int $length) + { + $this->control->maxlength = $length; + return $this; + } + + + /** + * Appends input string filter callback. + * @return static + */ + public function addFilter(callable $filter) + { + $this->getRules()->addFilter($filter); + return $this; + } + + + public function getControl(): Nette\Utils\Html + { + $el = parent::getControl(); + if ($this->emptyValue !== '') { + $el->attrs['data-nette-empty-value'] = Strings::trim($this->translate($this->emptyValue)); + } + if (isset($el->placeholder)) { + $el->placeholder = $this->translate($el->placeholder); + } + return $el; + } + + + protected function getRenderedValue(): ?string + { + return $this->rawValue === '' + ? ($this->emptyValue === '' ? null : $this->translate($this->emptyValue)) + : $this->rawValue; + } + + + /** + * @return static + */ + public function addRule($validator, $errorMessage = null, $arg = null) + { + if ($validator === Form::LENGTH || $validator === Form::MAX_LENGTH) { + $tmp = is_array($arg) ? $arg[1] : $arg; + if (is_scalar($tmp)) { + $this->control->maxlength = isset($this->control->maxlength) ? min($this->control->maxlength, $tmp) : $tmp; + } + } + return parent::addRule($validator, $errorMessage, $arg); + } +} diff --git a/src/Fragments/Traits/TextInputTrait.php b/src/Fragments/Traits/TextInputTrait.php new file mode 100644 index 0000000..9ceaa06 --- /dev/null +++ b/src/Fragments/Traits/TextInputTrait.php @@ -0,0 +1,112 @@ +control->maxlength = $maxLength; + $this->setOption('type', 'text'); + } + + + /** + * Loads HTTP data. + */ + public function loadHttpData(): void + { + $this->setValue($this->getHttpData(Form::DATA_LINE)); + } + + + /** + * Changes control's type attribute. + * @return static + */ + public function setHtmlType(string $type) + { + $this->control->type = $type; + return $this; + } + + + /** + * @deprecated use setHtmlType() + * @return static + */ + public function setType(string $type) + { + return $this->setHtmlType($type); + } + + + /** + * Generates control's HTML element. + */ + public function getControl(): Nette\Utils\Html + { + return parent::getControl()->addAttributes([ + 'value' => $this->control->type === 'password' ? $this->control->value : $this->getRenderedValue(), + 'type' => $this->control->type ?: 'text', + ]); + } + + + /** + * @return static + */ + public function addRule($validator, $errorMessage = null, $arg = null) + { + if ($this->control->type === null && in_array($validator, [Form::EMAIL, Form::URL, Form::INTEGER], true)) { + static $types = [Form::EMAIL => 'email', Form::URL => 'url', Form::INTEGER => 'number']; + $this->control->type = $types[$validator]; + + } elseif ( + in_array($validator, [Form::MIN, Form::MAX, Form::RANGE], true) + && in_array($this->control->type, ['number', 'range', 'datetime-local', 'datetime', 'date', 'month', 'week', 'time'], true) + ) { + if ($validator === Form::MIN) { + $range = [$arg, null]; + } elseif ($validator === Form::MAX) { + $range = [null, $arg]; + } else { + $range = $arg; + } + if (isset($range[0]) && is_scalar($range[0])) { + $this->control->min = isset($this->control->min) ? max($this->control->min, $range[0]) : $range[0]; + } + if (isset($range[1]) && is_scalar($range[1])) { + $this->control->max = isset($this->control->max) ? min($this->control->max, $range[1]) : $range[1]; + } + + } elseif ( + $validator === Form::PATTERN + && is_scalar($arg) + && in_array($this->control->type, [null, 'text', 'search', 'tel', 'url', 'email', 'password'], true) + ) { + $this->control->pattern = $arg; + } + + return parent::addRule($validator, $errorMessage, $arg); + } +} diff --git a/src/Fragments/UIComponent/BaseControl.php b/src/Fragments/UIComponent/BaseControl.php new file mode 100644 index 0000000..67c205d --- /dev/null +++ b/src/Fragments/UIComponent/BaseControl.php @@ -0,0 +1,20 @@ +