Skip to content

Commit 8776b68

Browse files
authored
Merge pull request #4 from jtojnar/bs5
Add Bootstrap 5 renderer
2 parents 480e6a5 + db073f5 commit 8776b68

File tree

4 files changed

+270
-12
lines changed

4 files changed

+270
-12
lines changed

examples/renderers/RenderersPresenter.latte

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,54 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
{if $renderer === 'bs3'}
4+
{if preg_match('(^bs5)', $renderer)}
55
<link
66
rel="stylesheet"
7-
href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
8-
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu"
7+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
8+
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
99
crossorigin="anonymous">
10-
{else}
10+
{elseif preg_match('(^bs4)', $renderer)}
1111
<link
1212
rel="stylesheet"
1313
href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
1414
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS"
1515
crossorigin="anonymous">
16+
{else}
17+
<link
18+
rel="stylesheet"
19+
href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
20+
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu"
21+
crossorigin="anonymous">
1622
{/if}
1723
<title>Renderer demo</title>
1824
</head>
1925
<body>
2026
<div class="container">
2127
<h1>Forms Bootstrap Rendering</h1>
2228
<ul class="nav nav-pills">
23-
<li n:class="nav-item, $renderer === bs3 ? active" >
24-
<a n:class="nav-link, $renderer === bs3 ? active" n:href="this renderer => bs3">BS 3</a>
29+
<li n:class="nav-item, $renderer === bs3 ? active">
30+
<a n:class="nav-link, $renderer === bs3 ? active" n:href="this renderer => bs3, showBulky => true">BS 3</a>
2531
</li>
26-
<li n:class="nav-item, $renderer === bs4h ? active" >
27-
<a n:class="nav-link, $renderer === bs4h ? active" n:href="this renderer => bs4h">BS 4 horizontal</a>
32+
<li n:class="nav-item, $renderer === bs4h ? active">
33+
<a n:class="nav-link, $renderer === bs4h ? active" n:href="this renderer => bs4h, showBulky => true">BS 4 horizontal</a>
2834
</li>
2935
<li n:class="nav-item, $renderer === bs4v ? active">
30-
<a n:class="nav-link, $renderer === bs4v ? active" n:href="this renderer => bs4v">BS 4 vertical</a>
36+
<a n:class="nav-link, $renderer === bs4v ? active" n:href="this renderer => bs4v, showBulky => true">BS 4 vertical</a>
3137
</li>
3238
<li n:class="nav-item, $renderer === bs4i ? active">
33-
<a n:class="nav-link, $renderer === bs4i ? active" n:href="this renderer => bs4i">BS 4 inline</a>
39+
<a n:class="nav-link, $renderer === bs4i ? active" n:href="this renderer => bs4i, showBulky => false">BS 4 inline</a>
40+
</li>
41+
<li n:class="nav-item, $renderer === bs5h ? active">
42+
<a n:class="nav-link, $renderer === bs5h ? active" n:href="this renderer => bs5h, showBulky => true">BS 5 horizontal</a>
43+
</li>
44+
<li n:class="nav-item, $renderer === bs5v ? active">
45+
<a n:class="nav-link, $renderer === bs5v ? active" n:href="this renderer => bs5v, showBulky => true">BS 5 vertical</a>
46+
</li>
47+
<li n:class="nav-item, $renderer === bs5i ? active">
48+
<a n:class="nav-link, $renderer === bs5i ? active" n:href="this renderer => bs5i, showBulky => false">BS 5 inline</a>
49+
</li>
50+
<li n:class="nav-item" n:if="!$showBulky">
51+
<a n:class="nav-link" n:href="this showBulky => true">Show bulky elements</a>
3452
</li>
3553
</ul>
3654
<hr>

examples/renderers/RenderersPresenter.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Nette\Application\UI\Presenter;
77
use Nextras\FormsRendering\Renderers\Bs3FormRenderer;
88
use Nextras\FormsRendering\Renderers\Bs4FormRenderer;
9+
use Nextras\FormsRendering\Renderers\Bs5FormRenderer;
910
use Nextras\FormsRendering\Renderers\FormLayout;
1011

1112

@@ -17,25 +18,38 @@ class RenderersPresenter extends Presenter
1718
*/
1819
public $renderer = 'bs3';
1920

21+
/**
22+
* @var bool
23+
* @persistent
24+
*/
25+
public $showBulky = true;
26+
2027

2128
public function actionDefault()
2229
{
2330
$this->template->renderer = $this->renderer;
31+
$this->template->showBulky = $this->showBulky;
2432
}
2533

2634

2735
public function createComponentForm()
2836
{
2937
$form = new Form();
3038
$form->addText('text', 'Name');
39+
$form->addText('color', 'Color')->setHtmlType('color');
3140
$form->addCheckbox('checkbox', 'Do you agree?');
3241
$form->addCheckboxList('checkbox_list', 'CheckboxList', ['A', 'B', 'C']);
3342
$form->addInteger('integer', 'How much?');
34-
$form->addMultiSelect('multi_select', 'MultiSelect', ['A', 'B', 'C']);
43+
$form->addInteger('range', 'Up to eleven?')->setHtmlType('range');
44+
if ($this->showBulky) {
45+
$form->addMultiSelect('multi_select', 'MultiSelect', ['A', 'B', 'C']);
46+
}
3547
$form->addPassword('password', 'Password');
3648
$form->addRadioList('radio_list', 'RadioList', ['1', '2', '3']);
3749
$form->addSelect('select', 'Select', ['Y', 'X', 'C']);
38-
$form->addTextArea('textarea', 'Textarea');
50+
if ($this->showBulky) {
51+
$form->addTextArea('textarea', 'Textarea');
52+
}
3953
$form->addMultiUpload('multi_upload', 'MultiUpload');
4054
$form->addSubmit('save', 'Send');
4155
$form->addSubmit('secondary', 'Secondary');
@@ -48,6 +62,12 @@ public function createComponentForm()
4862
$form->setRenderer(new Bs4FormRenderer(FormLayout::VERTICAL));
4963
} elseif ($this->renderer === 'bs4i') {
5064
$form->setRenderer(new Bs4FormRenderer(FormLayout::INLINE));
65+
} elseif ($this->renderer === 'bs5h') {
66+
$form->setRenderer(new Bs5FormRenderer(FormLayout::HORIZONTAL));
67+
} elseif ($this->renderer === 'bs5v') {
68+
$form->setRenderer(new Bs5FormRenderer(FormLayout::VERTICAL));
69+
} elseif ($this->renderer === 'bs5i') {
70+
$form->setRenderer(new Bs5FormRenderer(FormLayout::INLINE));
5171
}
5272

5373
$form->onSuccess[] = function ($form, $values) {

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This package provides rendering helpers Nette Forms.
1010
Form renderers:
1111
- *Bs3Renderer* - renderer for Bootstrap 3 with horizontal mode only;
1212
- *Bs4Renderer* - renderer for Bootstrap 4 with support for horizontal, vertial and inline mode;
13+
- *Bs5Renderer* - renderer for Bootstrap 5 with support for horizontal, vertial and inline mode;
1314

1415
Latte Macros renderers:
1516
- *Bs3InputMacros* - modifies Form Macros to add Bootstrap 3 classes automatically;

src/Renderers/Bs5FormRenderer.php

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<?php declare(strict_types = 1);
2+
3+
/**
4+
* This file is part of the Nextras community extensions of Nette Framework
5+
*
6+
* @license MIT
7+
* @link https://github.com/nextras/forms-rendering
8+
*/
9+
10+
namespace Nextras\FormsRendering\Renderers;
11+
12+
use Nette\Forms\Controls;
13+
use Nette\Forms\Form;
14+
use Nette\Forms\IControl;
15+
use Nette\Forms\Rendering\DefaultFormRenderer;
16+
use Nette\Utils\Html;
17+
18+
19+
/**
20+
* Form renderer for Bootstrap 5.
21+
*/
22+
class Bs5FormRenderer extends DefaultFormRenderer
23+
{
24+
/** @var Controls\Button */
25+
public $primaryButton;
26+
27+
/** @var bool */
28+
private $controlsInit = false;
29+
30+
/** @var string */
31+
private $layout;
32+
33+
34+
public function __construct($layout = FormLayout::HORIZONTAL)
35+
{
36+
$this->layout = $layout;
37+
38+
if ($layout === FormLayout::HORIZONTAL) {
39+
$groupClasses = 'mb-3 row';
40+
} elseif ($layout === FormLayout::INLINE) {
41+
// Will be overridden by `.row-cols-lg-auto` from the form on large-enough screens.
42+
$groupClasses = 'col-12';
43+
} else {
44+
$groupClasses = 'mb-3';
45+
}
46+
47+
$this->wrappers['controls']['container'] = null;
48+
$this->wrappers['pair']['container'] = 'div class="' . $groupClasses . '"';
49+
$this->wrappers['control']['container'] = $layout === FormLayout::HORIZONTAL ? 'div class=col-sm-9' : null;
50+
$this->wrappers['label']['container'] = $layout === FormLayout::HORIZONTAL ? 'div class="col-sm-3 col-form-label"' : null;
51+
$this->wrappers['control']['description'] = 'small class="form-text text-muted"';
52+
$this->wrappers['control']['errorcontainer'] = 'div class=invalid-feedback';
53+
$this->wrappers['control']['.error'] = 'is-invalid';
54+
$this->wrappers['control']['.file'] = 'form-control';
55+
$this->wrappers['error']['container'] = null;
56+
$this->wrappers['error']['item'] = 'div class="alert alert-danger" role=alert';
57+
58+
if ($layout === FormLayout::INLINE) {
59+
$this->wrappers['group']['container'] = null;
60+
$this->wrappers['group']['label'] = 'h2';
61+
}
62+
}
63+
64+
65+
public function render(Form $form, string $mode = null): string
66+
{
67+
if ($this->form !== $form) {
68+
$this->controlsInit = false;
69+
}
70+
71+
return parent::render($form, $mode);
72+
}
73+
74+
75+
public function renderBegin(): string
76+
{
77+
$this->controlsInit();
78+
return parent::renderBegin();
79+
}
80+
81+
82+
public function renderEnd(): string
83+
{
84+
$this->controlsInit();
85+
return parent::renderEnd();
86+
}
87+
88+
89+
public function renderBody(): string
90+
{
91+
$this->controlsInit();
92+
return parent::renderBody();
93+
}
94+
95+
96+
public function renderControls($parent): string
97+
{
98+
$this->controlsInit();
99+
return parent::renderControls($parent);
100+
}
101+
102+
103+
public function renderPair(IControl $control): string
104+
{
105+
$this->controlsInit();
106+
return parent::renderPair($control);
107+
}
108+
109+
110+
public function renderPairMulti(array $controls): string
111+
{
112+
$this->controlsInit();
113+
return parent::renderPairMulti($controls);
114+
}
115+
116+
117+
public function renderLabel(IControl $control): Html
118+
{
119+
$this->controlsInit();
120+
return parent::renderLabel($control);
121+
}
122+
123+
124+
public function renderControl(IControl $control): Html
125+
{
126+
$this->controlsInit();
127+
return parent::renderControl($control);
128+
}
129+
130+
131+
private function controlsInit()
132+
{
133+
if ($this->controlsInit) {
134+
return;
135+
}
136+
137+
$this->controlsInit = true;
138+
139+
if ($this->layout === FormLayout::INLINE) {
140+
// Unlike previous versions, Bootstrap 5 has no special class for inline forms.
141+
// Instead, upstream recommends a wrapping flexbox row with auto-sized columns.
142+
// https://getbootstrap.com/docs/5.0/forms/layout/#inline-forms
143+
$this->form->getElementPrototype()->addClass('row row-cols-lg-auto g-3 align-items-center');
144+
}
145+
146+
foreach ($this->form->getControls() as $control) {
147+
if ($this->layout === FormLayout::INLINE) {
148+
// Unfortunately, the aforementioned solution does not seem to expect labels
149+
// so we need to add some hacks. Notably, `.form-control`, `.form-select` and
150+
// others add `display: block`, forcing the control onto a next line.
151+
// The checkboxes are exception since they have their own inline class.
152+
153+
if (!$control instanceof Controls\Checkbox && !$control instanceof Controls\CheckboxList && !$control instanceof Controls\RadioList) {
154+
$control->getControlPrototype()->addClass('d-inline-block');
155+
156+
// But setting `display: inline-block` is not enough since the widgets will inherit
157+
// `width: 100%` from `.form-control` and end up wrapped anyway.
158+
// Let’s counter that using `width: auto`.
159+
$control->getControlPrototype()->addClass('w-auto');
160+
if ($control instanceof Controls\TextBase && $control->control->type === 'color') {
161+
// `input[type=color]` is a special case since `width: auto` would make it squish.
162+
$control->getControlPrototype()->addStyle('min-width', '3rem');
163+
}
164+
}
165+
166+
// Also, we need to add some spacing between the label and the control.
167+
$control->getLabelPrototype()->addClass('me-2');
168+
}
169+
170+
if ($control instanceof Controls\Button) {
171+
// Mark first form button (or the one provided) as primary.
172+
$markAsPrimary = $control === $this->primaryButton || (!isset($this->primaryButton) && $control->parent instanceof Form);
173+
if ($markAsPrimary) {
174+
$class = 'btn btn-primary';
175+
$this->primaryButton = $control;
176+
} else {
177+
$class = 'btn btn-secondary';
178+
}
179+
$control->getControlPrototype()->addClass($class);
180+
} elseif ($control instanceof Controls\TextBase) {
181+
// `input` is generally a `.form-control`, except for `[type=range]`.
182+
if ($control->control->type === 'range') {
183+
$control->getControlPrototype()->addClass('form-range');
184+
} else {
185+
$control->getControlPrototype()->addClass('form-control');
186+
}
187+
188+
// `input[type=color]` needs an extra class.
189+
if ($control->control->type === 'color') {
190+
$control->getControlPrototype()->addClass('form-control-color');
191+
}
192+
} elseif ($control instanceof Controls\SelectBox || $control instanceof Controls\MultiSelectBox) {
193+
// `select` needs a custom class.
194+
$control->getControlPrototype()->addClass('form-select');
195+
} elseif ($control instanceof Controls\Checkbox || $control instanceof Controls\CheckboxList || $control instanceof Controls\RadioList) {
196+
// `input[type=checkbox]` and `input[type=radio]` need a custom class.
197+
$control->getControlPrototype()->addClass('form-check-input');
198+
199+
// They also need to be individually wrapped in `div.form-check`.
200+
$control->getSeparatorPrototype()
201+
->setName('div')
202+
->appendAttribute('class', 'form-check')
203+
// They support being displayed inline with `.form-check-inline`.
204+
// https://getbootstrap.com/docs/5.0/forms/checks-radios/
205+
// But do not add the class for `Controls\Checkbox` since a single checkbox
206+
// can be inlined just fine and the class adds unnecessary `margin-right`.
207+
->appendAttribute('class', 'form-check-inline', $this->layout === FormLayout::INLINE && !$control instanceof Controls\Checkbox);
208+
209+
// Labels of individual checkboxes/radios also need a special class.
210+
if ($control instanceof Controls\Checkbox) {
211+
// For `Controls\Checkbox` there is only the label of the control.
212+
$control->getLabelPrototype()->addClass('form-check-label');
213+
} else {
214+
$control->getItemLabelPrototype()->addClass('form-check-label');
215+
}
216+
}
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)