onError[] = function(Nette\Forms\Form $form) { if ($form->getPresenter()->isAjax()) { static::makeBootstrap($form); } static::sendErrorPayload($form); }; $form->onRender[] = function(Nette\Forms\Form $form) { static::makeBootstrap($form); }; $form->getElementPrototype()->setAttribute('data-adt-submit-form', true); $this->form = $form; } public function renderLabel(Nette\Forms\Control $control): Html { if ($control->getLabel() && $control->getLabel()->getHtml()) { return parent::renderLabel($control); } return Html::el(); } public function renderControl(Nette\Forms\Control $control): Html { $body = $this->getWrapper('control container'); if ($this->counter % 2) { $body->class($this->getValue('control .odd'), true); } if (!$this->getWrapper('pair container')->getName()) { $body->class($control->getOption('class'), true); $body->id = $control->getOption('id'); } $description = $control->getOption('description'); if ($description instanceof Nette\HtmlStringable) { $description = ' ' . $description; } elseif ($description != null) { // intentionally == if ($control instanceof Nette\Forms\Controls\BaseControl) { $description = $control->translate($description); } $description = ' ' . $this->getWrapper('control description')->setText($description); } else { $description = ''; } if ($control->isRequired()) { $description = $this->getValue('control requiredsuffix') . $description; } $prepend = $control->getOption('prepend') ?: ''; if ($prepend instanceof Nette\HtmlStringable) { } elseif ($prepend != null) { // intentionally == if ($control instanceof Nette\Forms\Controls\BaseControl) { $prepend = $control->translate($prepend); } } if ($prepend) { $prepend = '' . $prepend . ''; } $append = $control->getOption('append') ?: ''; if ($append instanceof Nette\HtmlStringable) { } elseif ($append != null) { // intentionally == if ($control instanceof Nette\Forms\Controls\BaseControl) { $append = $control->translate($append); } } if ($append) { $append = '' . $append . ''; } $inputGroupStart = $inputGroupEnd = ''; if ($prepend || $append) { $inputGroupStart = '
'; $inputGroupEnd = '
'; } $control->setOption('rendered', true); $el = $control->getControl(); if ($el instanceof Html) { if ($el->getName() === 'input') { $el->class($this->getValue("control .$el->type"), true); } $el->class($this->getValue('control .error'), $control->hasErrors()); } $el = $body->setHtml($inputGroupStart . $prepend . $el . $append . $this->renderErrors($control) . $description . $inputGroupEnd); // Is this an instance of a RadioList or CheckboxList? if ( $control instanceof Nette\Forms\Controls\RadioList || $control instanceof Nette\Forms\Controls\CheckboxList || $control instanceof Nette\Forms\Controls\Checkbox ) { // Get original container $container = $control->getContainerPrototype(); $container->removeChildren(); // Create an empty Html container object $el = Html::el(); // Get all the child items $items = $control instanceof Nette\Forms\Controls\Checkbox ? [$control] : $control->getItems(); // For each child item, add the appropriate control part and label part after one another foreach($items as $key => $item) { $_container = clone $container; $_container->addHtml($control->getControlPart($key)); $_container->addHtml($control->getLabelPart($key)); $el->addHtml($_container); } if ($control instanceof Nette\Forms\Controls\Checkbox) { $_container->addHtml($this->doRenderErrors($control->getErrors(), true, $control->getHtmlId())); } else { $el->addHtml($this->doRenderErrors($control->getErrors(), true, $control->getHtmlId())); } } elseif ($control instanceof PhoneNumberInput) { $el = Html::el('div') ->setAttribute('class', static::$version === self::VERSION_4 ? 'form-row' : 'row g-2') ->addHtml('
' . $control->getControlPart(PhoneNumberInput::CONTROL_COUNTRY_CODE)->setAttribute('class', static::$version === self::VERSION_4 ? 'form-control' : 'form-select') . '
') ->addHtml('
' . $control->getControlPart(PhoneNumberInput::CONTROL_NATIONAL_NUMBER)->setAttribute('class', 'form-control') . $description . $this->renderErrors($control) . '
'); } return $el; } /** * Renders validation errors (per form or per control). */ public function renderErrors(?Nette\Forms\Control $control = null, bool $own = true): string { $errors = $control ? $control->getErrors() : ($own ? $this->form->getOwnErrors() : $this->form->getErrors()); return $this->doRenderErrors($errors, (bool) $control, $control ? $control->getHtmlId() : $this->form->getElementPrototype()->getId()); } /** * We want to render erros if * @param array $errors * @param bool $control * @param string|null $elId * @return string */ public function doRenderErrors(array $errors, bool $control = false, ?string $elId = null): string { $container = $this->getWrapper($control ? 'control errorcontainer' : 'error container'); $item = $this->getWrapper($control ? 'control erroritem' : 'error item'); foreach ($errors as $error) { $item = clone $item; if ($error instanceof IHtmlString) { $item->addHtml($error); } else { $item->setText($error); } $container->addHtml($item); } if ($elId) { $errorElId = 'snippet-' . $elId . '-errors'; // we want to render container for errors even if there are no errors // to be able to redraw it on ajax call $container ->setAttribute('id', $errorElId); if ($errors) { $el = 'document.getElementById("' . $elId . '")'; $container->addHtml(" "); } } return $control ? "\n\t" . $container->render() : "\n" . $container->render(0); } public static function makeBootstrap(Nette\Forms\Container $container) { if (static::$version === self::VERSION_4) { static::bootstrap4($container); } elseif (static::$version === self::VERSION_5) { static::bootstrap5($container); } else { throw new \Exception('Unsupported Bootstrap version.'); } } /** * @throws Nette\Application\AbortException */ public static function sendErrorPayload(Nette\Application\UI\Form $form) { if ($form->getPresenter()->isAjax()) { $renderer = $form->getRenderer(); $presenter = $form->getPresenter(); $renderer->wrappers['error']['container'] = null; $presenter->payload->snippets['snippet-' . $form->getElementPrototype()->getAttribute('id') . '-errors'] = $renderer->renderErrors(); $renderer->wrappers['control']['errorcontainer'] = null; /** @var Nette\Forms\Control $control */ foreach ($form->getControls() as $control) { if ($control->getErrors()) { $presenter->payload->snippets['snippet-' . $control->getHtmlId() . '-errors'] = $renderer->renderErrors($control); $presenter->payload->hasErrors = true; } } if (!$form->getPresenter()->isControlInvalid()) { $presenter->sendPayload(); } } } protected static function bootstrap4(Nette\Forms\Container $container): void { $renderer = $container->getForm()->getRenderer(); $renderer->wrappers['error']['container'] = 'div'; $renderer->wrappers['error']['item'] = 'div class="alert alert-danger"'; $renderer->wrappers['controls']['container'] = null; $renderer->wrappers['group']['container'] = null; $renderer->wrappers['pair']['container'] = 'div class=form-group'; $renderer->wrappers['label']['container'] = null; $renderer->wrappers['control']['container'] = null; $renderer->wrappers['control']['.error'] = 'is-invalid'; $renderer->wrappers['control']['.file'] = 'form-control-file'; $renderer->wrappers['control']['errorcontainer'] = 'div class=invalid-feedback'; $renderer->wrappers['control']['erroritem'] = 'div'; $renderer->wrappers['control']['description'] = 'small class="form-text text-muted"'; // we need to create a template container for DynamicContainer // to apply bootstrap4 styles below /** @var DynamicContainer $_dynamicContainer */ foreach ($container->getComponents(true, DynamicContainer::class) as $_dynamicContainer) { if ($_dynamicContainer->isAllowAdding()) { $_dynamicContainer->getTemplate(); } } /** @var Nette\Forms\Controls\BaseControl $control */ foreach ($container->getControls() as $control) { $type = $control->getOption('type'); if ($control instanceof Nette\Forms\Controls\Button) { $control->renderAsButton(); if ($control->getValidationScope() !== null) { $control->getControlPrototype()->addClass('btn btn-outline-secondary'); } else { $control->getControlPrototype()->addClass(empty($usedPrimary) ? 'btn btn-primary' : 'btn btn-outline-secondary'); $usedPrimary = true; } } elseif (in_array($type, ['checkbox', 'radio'], true)) { if ($control->getOption('switch') === true) { $control->getContainerPrototype() ->setName('div') ->addClass('form-check form-switch'); $control->getControlPrototype() ->addClass('form-check-input'); } elseif ($control instanceof Nette\Forms\Controls\Checkbox) { $control->getLabelPrototype()->addClass('form-check-label'); } else { $control->getItemLabelPrototype()->addClass('form-check-label'); } $control->getControlPrototype()->addClass('form-check-input'); $control->getContainerPrototype()->setName('div')->addClass('form-check'); } elseif ($control instanceof PhoneNumberInput) { $control->getControlPrototype(PhoneNumberInput::CONTROL_COUNTRY_CODE)->addClass('form-control'); $control->getControlPrototype(PhoneNumberInput::CONTROL_NATIONAL_NUMBER)->addClass('form-control'); } elseif ($type !== 'hidden') { $control->getControlPrototype()->addClass('form-control'); } } } protected static function bootstrap5(Nette\Forms\Container $container): void { static::bootstrap4($container); $renderer = $container->getForm()->getRenderer(); $renderer->wrappers['pair']['container'] = 'div class=mb-3'; $renderer->wrappers['control']['.file'] = 'form-control'; foreach ($container->getControls() as $control) { $type = $control->getOption('type'); if (!in_array($type, ['checkbox', 'radio'], true)) { $control->getLabelPrototype()->addClass('form-label'); } if ($control instanceof SelectBox) { $control->getControlPrototype()->removeClass('form-control')->addClass('form-select'); } } } }