diff --git a/examples/renderers/RenderersPresenter.latte b/examples/renderers/RenderersPresenter.latte index e250b01..1b84846 100644 --- a/examples/renderers/RenderersPresenter.latte +++ b/examples/renderers/RenderersPresenter.latte @@ -1,18 +1,24 @@ - {if $renderer === 'bs3'} + {if preg_match('(^bs5)', $renderer)} - {else} + {elseif preg_match('(^bs4)', $renderer)} + {else} + {/if} Renderer demo @@ -20,17 +26,29 @@

Forms Bootstrap Rendering


diff --git a/examples/renderers/RenderersPresenter.php b/examples/renderers/RenderersPresenter.php index 38ec7f0..dc343db 100644 --- a/examples/renderers/RenderersPresenter.php +++ b/examples/renderers/RenderersPresenter.php @@ -6,6 +6,7 @@ use Nette\Application\UI\Presenter; use Nextras\FormsRendering\Renderers\Bs3FormRenderer; use Nextras\FormsRendering\Renderers\Bs4FormRenderer; +use Nextras\FormsRendering\Renderers\Bs5FormRenderer; use Nextras\FormsRendering\Renderers\FormLayout; @@ -17,10 +18,17 @@ class RenderersPresenter extends Presenter */ public $renderer = 'bs3'; + /** + * @var bool + * @persistent + */ + public $showBulky = true; + public function actionDefault() { $this->template->renderer = $this->renderer; + $this->template->showBulky = $this->showBulky; } @@ -28,14 +36,20 @@ public function createComponentForm() { $form = new Form(); $form->addText('text', 'Name'); + $form->addText('color', 'Color')->setHtmlType('color'); $form->addCheckbox('checkbox', 'Do you agree?'); $form->addCheckboxList('checkbox_list', 'CheckboxList', ['A', 'B', 'C']); $form->addInteger('integer', 'How much?'); - $form->addMultiSelect('multi_select', 'MultiSelect', ['A', 'B', 'C']); + $form->addInteger('range', 'Up to eleven?')->setHtmlType('range'); + if ($this->showBulky) { + $form->addMultiSelect('multi_select', 'MultiSelect', ['A', 'B', 'C']); + } $form->addPassword('password', 'Password'); $form->addRadioList('radio_list', 'RadioList', ['1', '2', '3']); $form->addSelect('select', 'Select', ['Y', 'X', 'C']); - $form->addTextArea('textarea', 'Textarea'); + if ($this->showBulky) { + $form->addTextArea('textarea', 'Textarea'); + } $form->addMultiUpload('multi_upload', 'MultiUpload'); $form->addSubmit('save', 'Send'); $form->addSubmit('secondary', 'Secondary'); @@ -48,6 +62,12 @@ public function createComponentForm() $form->setRenderer(new Bs4FormRenderer(FormLayout::VERTICAL)); } elseif ($this->renderer === 'bs4i') { $form->setRenderer(new Bs4FormRenderer(FormLayout::INLINE)); + } elseif ($this->renderer === 'bs5h') { + $form->setRenderer(new Bs5FormRenderer(FormLayout::HORIZONTAL)); + } elseif ($this->renderer === 'bs5v') { + $form->setRenderer(new Bs5FormRenderer(FormLayout::VERTICAL)); + } elseif ($this->renderer === 'bs5i') { + $form->setRenderer(new Bs5FormRenderer(FormLayout::INLINE)); } $form->onSuccess[] = function ($form, $values) { diff --git a/readme.md b/readme.md index e62e972..a2316c2 100644 --- a/readme.md +++ b/readme.md @@ -10,6 +10,7 @@ This package provides rendering helpers Nette Forms. Form renderers: - *Bs3Renderer* - renderer for Bootstrap 3 with horizontal mode only; - *Bs4Renderer* - renderer for Bootstrap 4 with support for horizontal, vertial and inline mode; +- *Bs5Renderer* - renderer for Bootstrap 5 with support for horizontal, vertial and inline mode; Latte Macros renderers: - *Bs3InputMacros* - modifies Form Macros to add Bootstrap 3 classes automatically; diff --git a/src/Renderers/Bs5FormRenderer.php b/src/Renderers/Bs5FormRenderer.php new file mode 100644 index 0000000..4fbb69d --- /dev/null +++ b/src/Renderers/Bs5FormRenderer.php @@ -0,0 +1,219 @@ +layout = $layout; + + if ($layout === FormLayout::HORIZONTAL) { + $groupClasses = 'mb-3 row'; + } elseif ($layout === FormLayout::INLINE) { + // Will be overridden by `.row-cols-lg-auto` from the form on large-enough screens. + $groupClasses = 'col-12'; + } else { + $groupClasses = 'mb-3'; + } + + $this->wrappers['controls']['container'] = null; + $this->wrappers['pair']['container'] = 'div class="' . $groupClasses . '"'; + $this->wrappers['control']['container'] = $layout === FormLayout::HORIZONTAL ? 'div class=col-sm-9' : null; + $this->wrappers['label']['container'] = $layout === FormLayout::HORIZONTAL ? 'div class="col-sm-3 col-form-label"' : null; + $this->wrappers['control']['description'] = 'small class="form-text text-muted"'; + $this->wrappers['control']['errorcontainer'] = 'div class=invalid-feedback'; + $this->wrappers['control']['.error'] = 'is-invalid'; + $this->wrappers['control']['.file'] = 'form-control'; + $this->wrappers['error']['container'] = null; + $this->wrappers['error']['item'] = 'div class="alert alert-danger" role=alert'; + + if ($layout === FormLayout::INLINE) { + $this->wrappers['group']['container'] = null; + $this->wrappers['group']['label'] = 'h2'; + } + } + + + public function render(Form $form, string $mode = null): string + { + if ($this->form !== $form) { + $this->controlsInit = false; + } + + return parent::render($form, $mode); + } + + + public function renderBegin(): string + { + $this->controlsInit(); + return parent::renderBegin(); + } + + + public function renderEnd(): string + { + $this->controlsInit(); + return parent::renderEnd(); + } + + + public function renderBody(): string + { + $this->controlsInit(); + return parent::renderBody(); + } + + + public function renderControls($parent): string + { + $this->controlsInit(); + return parent::renderControls($parent); + } + + + public function renderPair(IControl $control): string + { + $this->controlsInit(); + return parent::renderPair($control); + } + + + public function renderPairMulti(array $controls): string + { + $this->controlsInit(); + return parent::renderPairMulti($controls); + } + + + public function renderLabel(IControl $control): Html + { + $this->controlsInit(); + return parent::renderLabel($control); + } + + + public function renderControl(IControl $control): Html + { + $this->controlsInit(); + return parent::renderControl($control); + } + + + private function controlsInit() + { + if ($this->controlsInit) { + return; + } + + $this->controlsInit = true; + + if ($this->layout === FormLayout::INLINE) { + // Unlike previous versions, Bootstrap 5 has no special class for inline forms. + // Instead, upstream recommends a wrapping flexbox row with auto-sized columns. + // https://getbootstrap.com/docs/5.0/forms/layout/#inline-forms + $this->form->getElementPrototype()->addClass('row row-cols-lg-auto g-3 align-items-center'); + } + + foreach ($this->form->getControls() as $control) { + if ($this->layout === FormLayout::INLINE) { + // Unfortunately, the aforementioned solution does not seem to expect labels + // so we need to add some hacks. Notably, `.form-control`, `.form-select` and + // others add `display: block`, forcing the control onto a next line. + // The checkboxes are exception since they have their own inline class. + + if (!$control instanceof Controls\Checkbox && !$control instanceof Controls\CheckboxList && !$control instanceof Controls\RadioList) { + $control->getControlPrototype()->addClass('d-inline-block'); + + // But setting `display: inline-block` is not enough since the widgets will inherit + // `width: 100%` from `.form-control` and end up wrapped anyway. + // Let’s counter that using `width: auto`. + $control->getControlPrototype()->addClass('w-auto'); + if ($control instanceof Controls\TextBase && $control->control->type === 'color') { + // `input[type=color]` is a special case since `width: auto` would make it squish. + $control->getControlPrototype()->addStyle('min-width', '3rem'); + } + } + + // Also, we need to add some spacing between the label and the control. + $control->getLabelPrototype()->addClass('me-2'); + } + + if ($control instanceof Controls\Button) { + // Mark first form button (or the one provided) as primary. + $markAsPrimary = $control === $this->primaryButton || (!isset($this->primaryButton) && $control->parent instanceof Form); + if ($markAsPrimary) { + $class = 'btn btn-primary'; + $this->primaryButton = $control; + } else { + $class = 'btn btn-secondary'; + } + $control->getControlPrototype()->addClass($class); + } elseif ($control instanceof Controls\TextBase) { + // `input` is generally a `.form-control`, except for `[type=range]`. + if ($control->control->type === 'range') { + $control->getControlPrototype()->addClass('form-range'); + } else { + $control->getControlPrototype()->addClass('form-control'); + } + + // `input[type=color]` needs an extra class. + if ($control->control->type === 'color') { + $control->getControlPrototype()->addClass('form-control-color'); + } + } elseif ($control instanceof Controls\SelectBox || $control instanceof Controls\MultiSelectBox) { + // `select` needs a custom class. + $control->getControlPrototype()->addClass('form-select'); + } elseif ($control instanceof Controls\Checkbox || $control instanceof Controls\CheckboxList || $control instanceof Controls\RadioList) { + // `input[type=checkbox]` and `input[type=radio]` need a custom class. + $control->getControlPrototype()->addClass('form-check-input'); + + // They also need to be individually wrapped in `div.form-check`. + $control->getSeparatorPrototype() + ->setName('div') + ->appendAttribute('class', 'form-check') + // They support being displayed inline with `.form-check-inline`. + // https://getbootstrap.com/docs/5.0/forms/checks-radios/ + // But do not add the class for `Controls\Checkbox` since a single checkbox + // can be inlined just fine and the class adds unnecessary `margin-right`. + ->appendAttribute('class', 'form-check-inline', $this->layout === FormLayout::INLINE && !$control instanceof Controls\Checkbox); + + // Labels of individual checkboxes/radios also need a special class. + if ($control instanceof Controls\Checkbox) { + // For `Controls\Checkbox` there is only the label of the control. + $control->getLabelPrototype()->addClass('form-check-label'); + } else { + $control->getItemLabelPrototype()->addClass('form-check-label'); + } + } + } + } +}