Собственные элементы форм
*************************
.[perex]
Nette предлагает широкую палитру [встроенных элементов форм |controls]. Но когда вы столкнётесь с требованием, которого среди них нет, ничего обходить и склеивать не придётся: вы напишете собственный элемент. Он будет уметь всё то же, что и встроенные - проверять, переводиться, отрисовываться - и использоваться будет точно так же.
Покажем это на практическом примере: элемент для ввода даты тремя полями - день, месяц и год. Попутно вы узнаете всё, что нужно знать о написании элементов.
Когда писать собственный элемент, а когда нет
=============================================
Собственный элемент - самый мощный инструмент, который предлагают формы. И, как всякий мощный инструмент, он должен быть последним выбором, а не первым. Многие ситуации решаются более простыми средствами:
- **Изменение значения** решает [addFilter() |validation#Изменение введённых значений]. Хотите допускать пробелы в почтовом индексе или строчные буквы в коде? Фильтр - это несколько строк.
- **Повторяющуюся настройку** оборачивают в собственный метод добавления. Добавляете поле почтового индекса с одинаковой проверкой в десяти местах? Сделайте для них именованное сокращение, [покажем это в конце |#Собственный метод добавления].
- **Группе связанных полей** служит [контейнер |controls#addContainer()]. Адресу из улицы, города и индекса собственный элемент не нужен, хватит контейнера с тремя текстовыми полями.
- **Другой внешний вид** достигается через [setHtmlType() |controls#addText()] и HTML-атрибуты либо через [прототипы |rendering#Прототипы].
Собственный элемент имеет смысл в тот момент, когда вам нужно **собственное значение**: элемент, который снаружи ведёт себя как одно поле с одним значением, но внутри состоит из нескольких инпутов или хранит значение иначе, чем показывает. Дата из трёх полей. Координаты, выбранные щелчком по карте. Ввод тегов с автодополнением.
Анатомия элемента
=================
Каждый собственный элемент наследуется от абстрактного класса [api:Nette\Forms\Controls\BaseControl]. От него он получает огромное количество готовой функциональности: хранение значения, правила и условия проверки, сообщения об ошибках, переводы, HTML-атрибуты, метку и связь с отрисовкой. Вы пишете только то, чем ваш элемент отличается.
Минимальный работающий элемент на удивление короток:
```php
use Nette\Forms\Form;
use Nette\Forms\Helpers;
use Nette\Utils\Html;
class SimpleInput extends Nette\Forms\Controls\BaseControl
{
public function loadHttpData(): void
{
$this->setValue($this->getHttpData(Form::DataLine));
}
public function getControl(): Html
{
return Html::el('input', [
'type' => 'text',
'name' => $this->getHtmlName(),
'id' => $this->getHtmlId(),
'value' => $this->getValue(),
'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null,
]);
}
}
```
Два метода: один говорит, как получить значение из отправленных данных, второй - как элемент отрисовать. На оба мы через минуту посмотрим подробно. Всё остальное - `setRequired()`, `addRule()`, `setDefaultValue()`, переводы - работает уже само.
В форму элемент добавляют методом `addComponent()` или короче, через квадратные скобки:
```php
$form['nickname'] = new SimpleInput('Ник:');
```
Жизненный цикл элемента
=======================
Прежде чем перейти к более интересному элементу, полезно знать, что и когда с элементом происходит. Форма и её элементы - это [компоненты |component-model:], образующие дерево. У этого есть одно приятное следствие: элементу не нужно ничего выяснять самому, обо всём важном в нужный момент позаботится фреймворк:
1) В момент, когда вы присоедините элемент к отправленной форме, сама форма вызовет у него `loadHttpData()`. В нём элемент считывает своё отправленное значение, как мы сейчас покажем. Он никогда не работает напрямую с `$_POST` и ему совершенно неважно, вложен ли он в контейнеры.
2) При отправке формы происходит проверка: вычисляются правила, добавленные через `addRule()`, и работают они со значением из `getValue()`.
3) Тот, кто затем вызовет `$form->getValues()` или `getValue()` у элемента, получит чистое типизированное значение - например, объект `DateTimeImmutable`, а не тройку строк из формы.
А при отрисовке вызывается `getControl()`, а для метки - `getLabel()`.
Чтение отправленного значения
=============================
В методе `loadHttpData()` элемент запрашивает своё отправленное значение методом `getHttpData()`. Его параметр - тип, определяющий, как значение нужно очистить:
| тип | значение
|-------
| `Form::DataLine` | однострочный текст: заменяет переводы строк пробелами, обрезает пробелы
| `Form::DataText` | многострочный текст: приводит окончания строк к `\n`
| `Form::DataFile` | загрузка файла, экземпляр `Nette\Http\FileUpload`
Как бы злоумышленник ни старался, результатом всегда будет корректная UTF-8-строка без управляющих символов (либо объект загрузки, либо `null`). Именно поэтому мы никогда не читаем значение прямо из `$_POST`: мы потеряли бы все эти гарантии.
Элемент, состоящий из нескольких инпутов, как наша дата, передаёт часть HTML-имени вторым параметром и так считывает свои отдельные подзначения. Он хранит их в собственных свойствах `$day`, `$month` и `$year` типа string:
```php
public function loadHttpData(): void
{
$this->day = $this->getHttpData(Form::DataLine, '[day]') ?? '';
$this->month = $this->getHttpData(Form::DataLine, '[month]') ?? '';
$this->year = $this->getHttpData(Form::DataLine, '[year]') ?? '';
}
```
Если HTML-имя заканчивается на `[]`, возвращается массив значений. В сочетании с типом `Form::DataKeys` (то есть `Form::DataLine | Form::DataKeys`) вы сохраните и его ключи:
```php
$tags = $this->getHttpData(Form::DataLine, '[tags][]');
```
Отсутствующее значение - это `null` (для массивов пустой массив). Запрос вообще может не содержать данных элемента, ничто не мешает злоумышленнику отправить что угодно - именно поэтому в примере мы дописываем `?? ''`, и именно поэтому такой вариант нужно учитывать всегда.
Значение элемента
=================
Элемент хранит своё значение и предоставляет его через тройку методов, договорённостей которых стоит придерживаться.
Метод `setValue()` принимает значение от программиста - этим же путём идут `setDefaultValue()` и `$form->setDefaults()`. Он должен принимать всё, что имеет смысл, преобразовывать значение во внутреннюю форму и выбрасывать исключение на бессмысленном вводе, чтобы ошибка проявилась сразу, а не через загадочное поведение формы. Наша дата принимает `DateTimeInterface`, строку, метку времени или `null` и раскладывает их на три поля:
```php
public function setValue(mixed $value): static
{
if ($value === null) {
$this->day = $this->month = $this->year = '';
} else {
$date = Nette\Utils\DateTime::from($value); // бессмыслица выбросит исключение
$this->day = $date->format('j');
$this->month = $date->format('n');
$this->year = $date->format('Y');
}
return $this;
}
```
Метод `getValue()`, наоборот, собирает чистое типизированное значение - единственное, что увидит пользователь вашего элемента. Если значение некорректно, он возвращает `null`. Статический метод `validateDate()` просто проверяет, что три поля складываются в существующую дату:
```php
public function getValue(): ?DateTimeImmutable
{
return self::validateDate($this)
? (new DateTimeImmutable)->setDate((int) $this->year, (int) $this->month, (int) $this->day)->setTime(0, 0)
: null;
}
```
А метод `isFilled()` говорит, заполнил ли пользователь элемент; его использует правило `setRequired()`. Реализации по умолчанию (непустое значение) часто хватает, но для составного элемента переопределите его согласно его логике:
```php
public function isFilled(): bool
{
return $this->day !== '' || $this->year !== '';
}
```
Отрисовка
=========
Метод `getControl()` возвращает HTML-вид элемента, обычно как объект [Html |utils:html-elements], но подойдёт и обычная строка - это неважно. К объекту Html мы обращаемся в основном при сборке кода, потому что он позволяет строить итоговую разметку безопасно и с приятным API. В вашем распоряжении несколько помощников:
- `getHtmlName()` возвращает HTML-атрибут `name`, включая возможную вложенность в контейнеры (например, `invoice[date]`). Для составного элемента вы дописываете к нему части имён отдельных инпутов: `$name . '[day]'`.
- `getHtmlId()` возвращает атрибут `id`, связанный с меткой.
- `Helpers::exportRules($this->getRules())` экспортирует правила проверки для атрибута `data-nette-rules`, благодаря чему для вашего элемента заработает и [проверка на JavaScript |validation#Проверка на JavaScript]. Атрибут ставится на первый инпут элемента.
- `Helpers::createSelectBox($items, $optionAttrs, $selected)` собирает элемент `<select>` из массива пунктов (вложенные массивы отрисовываются как `<optgroup>`) и возвращает его как `Html` - удобно для поля месяца в нашей дате.
- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` порождает список элементов `<input>`, обёрнутых в `<label>` (радиокнопки или флажки), и возвращает его строкой.
Первое поле нашей даты создаётся, стало быть, так:
```php
public function getControl(): Html
{
$name = $this->getHtmlName();
return Html::el()
->addHtml(Html::el('input', [
'name' => $name . '[day]',
'id' => $this->getHtmlId(),
'value' => $this->day,
'type' => 'number',
'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null,
]))
->addHtml(/* ... select для месяца и input для года ... */);
}
```
Метку отрисовывает `getLabel()`, и её реализация по умолчанию обычно подходит. Только осторожно: у составного элемента её атрибут `for` указывает на `getHtmlId()`, поэтому этот id дайте первому инпуту - ровно как в примере.
Чтобы составной элемент можно было отрисовывать в шаблоне по частям (например, `{input birthdate:day}`), переопределите методы `getControlPart($key)` и `getLabelPart($key)`, возвращающие элемент `Html` для данной части - точно так же, как это делают `CheckboxList` и `RadioList`.
.[note]
Если вы переопределяете `getControl()`, помните, что `BaseControl::getControl()` ещё и помечает элемент как отрисованный через `setOption('rendered', true)`. Вызовите его тоже (или вызовите `parent::getControl()`), когда в одной форме сочетаете ручную и автоматическую отрисовку, чтобы элемент не отрисовался дважды. (Пример `DateInput` выше опускает это для краткости.)
Полный пример: DateInput
========================
Все описанные части вместе, дополненные выпадающим списком для выбора месяца, вы найдёте в готовом элементе `DateInput` среди [примеров прямо в репозитории |https://github.com/nette/forms/blob/master/examples/custom-control.php].
Обратите внимание, что в конструкторе элемент добавляет себе правило проверки, которое следит за осмысленностью даты. Бессмысленный ввод, например 31 февраля, тем самым проявляется как обычная ошибка проверки формы:
```php
public function __construct($label = null)
{
parent::__construct($label);
$this->addRule(self::validateDate(...), 'Дата некорректна.');
}
```
А использование? Точно как у встроенных элементов:
```php
$form['birthdate'] = (new DateInput('Дата рождения:'))
->setDefaultValue(new DateTime('2000-01-01'))
->setRequired('Когда вы родились?');
$date = $form->getValues()->birthdate; // ?DateTimeImmutable
```
В шаблоне Latte вы отрисуете его привычным тегом `{input birthdate}` или `{label birthdate /}`, как и любой другой элемент.
Проверка
========
Встроенные правила проверки работают с собственным элементом сразу - они действуют со значением из `getValue()`. Наш `DateInput` может, например, использовать `Form::Min` для самой ранней допустимой даты. О том, как написать собственные правила вместе с их JavaScript-двойником, рассказано в главе [Собственные правила и условия |validation#Собственные правила и условия].
Собственный метод добавления
============================
Встроенные элементы мы добавляем удобными методами `$form->addText()` и им подобными. У собственного элемента такого метода нет, поэтому вы добавляете его обычным присваиванием - это одинаково работает в форме и в контейнере, и это понимают редакторы и статический анализ:
```php
$form['birthdate'] = new DateInput('Дата рождения:');
```
Если вы хотите сократить добавление, сохранив автодополнение, пригодится статический фабричный метод прямо на элементе. Он работает и во вложенных контейнерах, чего метод на потомке класса `Form` не смог бы: вложенные контейнеры о нём не знают:
```php
class DateInput extends Nette\Forms\Controls\BaseControl
{
public static function addTo(
Nette\Forms\Container $container,
string $name,
?string $label = null,
): self {
return $container[$name] = new self($label);
}
}
// работает в форме и в любом контейнере:
DateInput::addTo($form, 'birthdate', 'Дата рождения:');
```
Тот же подход работает и как именованное сокращение для повторяющейся настройки встроенного элемента:
```php
final class ZipInput
{
public static function addTo(
Nette\Forms\Container $container,
string $name,
?string $label = null,
): Nette\Forms\Controls\TextInput {
return $container->addText($name, $label)
->addRule(Nette\Forms\Form::Pattern, 'Почтовый индекс должен состоять ровно из 5 цифр', '[0-9]{5}');
}
}
ZipInput::addTo($form, 'zip', 'Почтовый индекс:');
```
Nette предлагает широкую палитру встроенных элементов форм. Но когда вы
столкнётесь с требованием, которого среди них нет, ничего обходить и
склеивать не придётся: вы напишете собственный элемент. Он будет уметь
всё то же, что и встроенные – проверять, переводиться,
отрисовываться – и использоваться будет точно так же.
Покажем это на практическом примере: элемент для ввода даты тремя
полями – день, месяц и год. Попутно вы узнаете всё, что нужно знать о
написании элементов.
Когда писать собственный элемент, а
когда нет
Собственный элемент – самый мощный инструмент, который предлагают
формы. И, как всякий мощный инструмент, он должен быть последним
выбором, а не первым. Многие ситуации решаются более простыми
средствами:
- Изменение значения решает addFilter(). Хотите допускать
пробелы в почтовом индексе или строчные буквы в коде? Фильтр – это
несколько строк.
- Повторяющуюся настройку оборачивают в собственный метод
добавления. Добавляете поле почтового индекса с одинаковой проверкой
в десяти местах? Сделайте для них именованное сокращение, покажем это в конце.
- Группе связанных полей служит контейнер. Адресу из улицы, города
и индекса собственный элемент не нужен, хватит контейнера с тремя
текстовыми полями.
- Другой внешний вид достигается через setHtmlType() и HTML-атрибуты либо через прототипы.
Собственный элемент имеет смысл в тот момент, когда вам нужно
собственное значение: элемент, который снаружи ведёт себя как одно
поле с одним значением, но внутри состоит из нескольких инпутов или
хранит значение иначе, чем показывает. Дата из трёх полей. Координаты,
выбранные щелчком по карте. Ввод тегов с автодополнением.
Анатомия элемента
Каждый собственный элемент наследуется от абстрактного класса Nette\Forms\Controls\BaseControl. От
него он получает огромное количество готовой функциональности:
хранение значения, правила и условия проверки, сообщения об ошибках,
переводы, HTML-атрибуты, метку и связь с отрисовкой. Вы пишете только то,
чем ваш элемент отличается.
Минимальный работающий элемент на удивление короток:
use Nette\Forms\Form;
use Nette\Forms\Helpers;
use Nette\Utils\Html;
class SimpleInput extends Nette\Forms\Controls\BaseControl
{
public function loadHttpData(): void
{
$this->setValue($this->getHttpData(Form::DataLine));
}
public function getControl(): Html
{
return Html::el('input', [
'type' => 'text',
'name' => $this->getHtmlName(),
'id' => $this->getHtmlId(),
'value' => $this->getValue(),
'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null,
]);
}
}
Два метода: один говорит, как получить значение из отправленных
данных, второй – как элемент отрисовать. На оба мы через минуту
посмотрим подробно. Всё остальное – setRequired(), addRule(),
setDefaultValue(), переводы – работает уже само.
В форму элемент добавляют методом addComponent() или короче, через
квадратные скобки:
$form['nickname'] = new SimpleInput('Ник:');
Жизненный цикл элемента
Прежде чем перейти к более интересному элементу, полезно знать, что и
когда с элементом происходит. Форма и её элементы – это компоненты, образующие дерево. У этого
есть одно приятное следствие: элементу не нужно ничего выяснять
самому, обо всём важном в нужный момент позаботится фреймворк:
- В момент, когда вы присоедините элемент к отправленной форме, сама
форма вызовет у него
loadHttpData(). В нём элемент считывает своё
отправленное значение, как мы сейчас покажем. Он никогда не работает
напрямую с $_POST и ему совершенно неважно, вложен ли он в
контейнеры.
- При отправке формы происходит проверка: вычисляются правила,
добавленные через
addRule(), и работают они со значением из
getValue().
- Тот, кто затем вызовет
$form->getValues() или getValue() у
элемента, получит чистое типизированное значение – например, объект
DateTimeImmutable, а не тройку строк из формы.
А при отрисовке вызывается getControl(), а для метки –
getLabel().
Чтение отправленного значения
В методе loadHttpData() элемент запрашивает своё отправленное
значение методом getHttpData(). Его параметр – тип, определяющий, как
значение нужно очистить:
| тип |
значение |
Form::DataLine |
однострочный текст: заменяет переводы строк пробелами, обрезает
пробелы |
Form::DataText |
многострочный текст: приводит окончания строк к \n |
Form::DataFile |
загрузка файла, экземпляр Nette\Http\FileUpload |
Как бы злоумышленник ни старался, результатом всегда будет
корректная UTF-8-строка без управляющих символов (либо объект загрузки,
либо null). Именно поэтому мы никогда не читаем значение прямо из
$_POST: мы потеряли бы все эти гарантии.
Элемент, состоящий из нескольких инпутов, как наша дата, передаёт
часть HTML-имени вторым параметром и так считывает свои отдельные
подзначения. Он хранит их в собственных свойствах $day, $month
и $year типа string:
public function loadHttpData(): void
{
$this->day = $this->getHttpData(Form::DataLine, '[day]') ?? '';
$this->month = $this->getHttpData(Form::DataLine, '[month]') ?? '';
$this->year = $this->getHttpData(Form::DataLine, '[year]') ?? '';
}
Если HTML-имя заканчивается на [], возвращается массив значений.
В сочетании с типом Form::DataKeys (то есть Form::DataLine | Form::DataKeys) вы
сохраните и его ключи:
$tags = $this->getHttpData(Form::DataLine, '[tags][]');
Отсутствующее значение – это null (для массивов пустой массив).
Запрос вообще может не содержать данных элемента, ничто не мешает
злоумышленнику отправить что угодно – именно поэтому в примере мы
дописываем ?? '', и именно поэтому такой вариант нужно учитывать
всегда.
Значение элемента
Элемент хранит своё значение и предоставляет его через тройку
методов, договорённостей которых стоит придерживаться.
Метод setValue() принимает значение от программиста – этим же
путём идут setDefaultValue() и $form->setDefaults(). Он должен принимать
всё, что имеет смысл, преобразовывать значение во внутреннюю форму и
выбрасывать исключение на бессмысленном вводе, чтобы ошибка
проявилась сразу, а не через загадочное поведение формы. Наша дата
принимает DateTimeInterface, строку, метку времени или null и
раскладывает их на три поля:
public function setValue(mixed $value): static
{
if ($value === null) {
$this->day = $this->month = $this->year = '';
} else {
$date = Nette\Utils\DateTime::from($value); // бессмыслица выбросит исключение
$this->day = $date->format('j');
$this->month = $date->format('n');
$this->year = $date->format('Y');
}
return $this;
}
Метод getValue(), наоборот, собирает чистое типизированное
значение – единственное, что увидит пользователь вашего элемента.
Если значение некорректно, он возвращает null. Статический метод
validateDate() просто проверяет, что три поля складываются в
существующую дату:
public function getValue(): ?DateTimeImmutable
{
return self::validateDate($this)
? (new DateTimeImmutable)->setDate((int) $this->year, (int) $this->month, (int) $this->day)->setTime(0, 0)
: null;
}
А метод isFilled() говорит, заполнил ли пользователь элемент; его
использует правило setRequired(). Реализации по умолчанию (непустое
значение) часто хватает, но для составного элемента переопределите его
согласно его логике:
public function isFilled(): bool
{
return $this->day !== '' || $this->year !== '';
}
Отрисовка
Метод getControl() возвращает HTML-вид элемента, обычно как объект Html, но подойдёт и обычная строка – это
неважно. К объекту Html мы обращаемся в основном при сборке кода, потому
что он позволяет строить итоговую разметку безопасно и с приятным API. В
вашем распоряжении несколько помощников:
getHtmlName() возвращает HTML-атрибут name, включая возможную
вложенность в контейнеры (например, invoice[date]). Для составного
элемента вы дописываете к нему части имён отдельных инпутов:
$name . '[day]'.
getHtmlId() возвращает атрибут id, связанный с меткой.
Helpers::exportRules($this->getRules()) экспортирует правила проверки для
атрибута data-nette-rules, благодаря чему для вашего элемента
заработает и проверка на
JavaScript. Атрибут ставится на первый инпут элемента.
Helpers::createSelectBox($items, $optionAttrs, $selected) собирает элемент
<select> из массива пунктов (вложенные массивы отрисовываются
как <optgroup>) и возвращает его как Html – удобно для поля
месяца в нашей дате.
Helpers::createInputList($items, $inputAttrs, $labelAttrs) порождает список элементов
<input>, обёрнутых в <label> (радиокнопки или флажки), и
возвращает его строкой.
Первое поле нашей даты создаётся, стало быть, так:
public function getControl(): Html
{
$name = $this->getHtmlName();
return Html::el()
->addHtml(Html::el('input', [
'name' => $name . '[day]',
'id' => $this->getHtmlId(),
'value' => $this->day,
'type' => 'number',
'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null,
]))
->addHtml(/* ... select для месяца и input для года ... */);
}
Метку отрисовывает getLabel(), и её реализация по умолчанию обычно
подходит. Только осторожно: у составного элемента её атрибут for
указывает на getHtmlId(), поэтому этот id дайте первому инпуту –
ровно как в примере.
Чтобы составной элемент можно было отрисовывать в шаблоне по частям
(например, {input birthdate:day}), переопределите методы
getControlPart($key) и getLabelPart($key), возвращающие элемент Html
для данной части – точно так же, как это делают CheckboxList и
RadioList.
Если вы переопределяете getControl(), помните, что
BaseControl::getControl() ещё и помечает элемент как отрисованный через
setOption('rendered', true). Вызовите его тоже (или вызовите
parent::getControl()), когда в одной форме сочетаете ручную и
автоматическую отрисовку, чтобы элемент не отрисовался дважды. (Пример
DateInput выше опускает это для краткости.)
Все описанные части вместе, дополненные выпадающим списком для
выбора месяца, вы найдёте в готовом элементе DateInput среди примеров прямо в
репозитории.
Обратите внимание, что в конструкторе элемент добавляет себе правило
проверки, которое следит за осмысленностью даты. Бессмысленный ввод,
например 31 февраля, тем самым проявляется как обычная ошибка
проверки формы:
public function __construct($label = null)
{
parent::__construct($label);
$this->addRule(self::validateDate(...), 'Дата некорректна.');
}
А использование? Точно как у встроенных элементов:
$form['birthdate'] = (new DateInput('Дата рождения:'))
->setDefaultValue(new DateTime('2000-01-01'))
->setRequired('Когда вы родились?');
$date = $form->getValues()->birthdate; // ?DateTimeImmutable
В шаблоне Latte вы отрисуете его привычным тегом {input birthdate} или
{label birthdate /}, как и любой другой элемент.
Проверка
Встроенные правила проверки работают с собственным элементом
сразу – они действуют со значением из getValue(). Наш DateInput
может, например, использовать Form::Min для самой ранней допустимой
даты. О том, как написать собственные правила вместе с их
JavaScript-двойником, рассказано в главе Собственные правила и
условия.
Собственный метод добавления
Встроенные элементы мы добавляем удобными методами
$form->addText() и им подобными. У собственного элемента такого
метода нет, поэтому вы добавляете его обычным присваиванием – это
одинаково работает в форме и в контейнере, и это понимают редакторы и
статический анализ:
$form['birthdate'] = new DateInput('Дата рождения:');
Если вы хотите сократить добавление, сохранив автодополнение,
пригодится статический фабричный метод прямо на элементе. Он работает
и во вложенных контейнерах, чего метод на потомке класса Form не
смог бы: вложенные контейнеры о нём не знают:
class DateInput extends Nette\Forms\Controls\BaseControl
{
public static function addTo(
Nette\Forms\Container $container,
string $name,
?string $label = null,
): self {
return $container[$name] = new self($label);
}
}
// работает в форме и в любом контейнере:
DateInput::addTo($form, 'birthdate', 'Дата рождения:');
Тот же подход работает и как именованное сокращение для
повторяющейся настройки встроенного элемента:
final class ZipInput
{
public static function addTo(
Nette\Forms\Container $container,
string $name,
?string $label = null,
): Nette\Forms\Controls\TextInput {
return $container->addText($name, $label)
->addRule(Nette\Forms\Form::Pattern, 'Почтовый индекс должен состоять ровно из 5 цифр', '[0-9]{5}');
}
}
ZipInput::addTo($form, 'zip', 'Почтовый индекс:');