-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from jtojnar/bs5
Add Bootstrap 5 renderer
- Loading branch information
Showing
4 changed files
with
270 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,54 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
{if $renderer === 'bs3'} | ||
{if preg_match('(^bs5)', $renderer)} | ||
<link | ||
rel="stylesheet" | ||
href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" | ||
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" | ||
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" | ||
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" | ||
crossorigin="anonymous"> | ||
{else} | ||
{elseif preg_match('(^bs4)', $renderer)} | ||
<link | ||
rel="stylesheet" | ||
href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" | ||
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" | ||
crossorigin="anonymous"> | ||
{else} | ||
<link | ||
rel="stylesheet" | ||
href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" | ||
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" | ||
crossorigin="anonymous"> | ||
{/if} | ||
<title>Renderer demo</title> | ||
</head> | ||
<body> | ||
<div class="container"> | ||
<h1>Forms Bootstrap Rendering</h1> | ||
<ul class="nav nav-pills"> | ||
<li n:class="nav-item, $renderer === bs3 ? active" > | ||
<a n:class="nav-link, $renderer === bs3 ? active" n:href="this renderer => bs3">BS 3</a> | ||
<li n:class="nav-item, $renderer === bs3 ? active"> | ||
<a n:class="nav-link, $renderer === bs3 ? active" n:href="this renderer => bs3, showBulky => true">BS 3</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs4h ? active" > | ||
<a n:class="nav-link, $renderer === bs4h ? active" n:href="this renderer => bs4h">BS 4 horizontal</a> | ||
<li n:class="nav-item, $renderer === bs4h ? active"> | ||
<a n:class="nav-link, $renderer === bs4h ? active" n:href="this renderer => bs4h, showBulky => true">BS 4 horizontal</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs4v ? active"> | ||
<a n:class="nav-link, $renderer === bs4v ? active" n:href="this renderer => bs4v">BS 4 vertical</a> | ||
<a n:class="nav-link, $renderer === bs4v ? active" n:href="this renderer => bs4v, showBulky => true">BS 4 vertical</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs4i ? active"> | ||
<a n:class="nav-link, $renderer === bs4i ? active" n:href="this renderer => bs4i">BS 4 inline</a> | ||
<a n:class="nav-link, $renderer === bs4i ? active" n:href="this renderer => bs4i, showBulky => false">BS 4 inline</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs5h ? active"> | ||
<a n:class="nav-link, $renderer === bs5h ? active" n:href="this renderer => bs5h, showBulky => true">BS 5 horizontal</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs5v ? active"> | ||
<a n:class="nav-link, $renderer === bs5v ? active" n:href="this renderer => bs5v, showBulky => true">BS 5 vertical</a> | ||
</li> | ||
<li n:class="nav-item, $renderer === bs5i ? active"> | ||
<a n:class="nav-link, $renderer === bs5i ? active" n:href="this renderer => bs5i, showBulky => false">BS 5 inline</a> | ||
</li> | ||
<li n:class="nav-item" n:if="!$showBulky"> | ||
<a n:class="nav-link" n:href="this showBulky => true">Show bulky elements</a> | ||
</li> | ||
</ul> | ||
<hr> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
/** | ||
* This file is part of the Nextras community extensions of Nette Framework | ||
* | ||
* @license MIT | ||
* @link https://github.com/nextras/forms-rendering | ||
*/ | ||
|
||
namespace Nextras\FormsRendering\Renderers; | ||
|
||
use Nette\Forms\Controls; | ||
use Nette\Forms\Form; | ||
use Nette\Forms\IControl; | ||
use Nette\Forms\Rendering\DefaultFormRenderer; | ||
use Nette\Utils\Html; | ||
|
||
|
||
/** | ||
* Form renderer for Bootstrap 5. | ||
*/ | ||
class Bs5FormRenderer extends DefaultFormRenderer | ||
{ | ||
/** @var Controls\Button */ | ||
public $primaryButton; | ||
|
||
/** @var bool */ | ||
private $controlsInit = false; | ||
|
||
/** @var string */ | ||
private $layout; | ||
|
||
|
||
public function __construct($layout = FormLayout::HORIZONTAL) | ||
{ | ||
$this->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'); | ||
} | ||
} | ||
} | ||
} | ||
} |