Skip to content

Commit

Permalink
Add Bootstrap 5 renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
jtojnar committed Jun 4, 2021
1 parent 480e6a5 commit db073f5
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 12 deletions.
38 changes: 28 additions & 10 deletions examples/renderers/RenderersPresenter.latte
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>
Expand Down
24 changes: 22 additions & 2 deletions examples/renderers/RenderersPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand All @@ -17,25 +18,38 @@ 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;
}


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');
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
219 changes: 219 additions & 0 deletions src/Renderers/Bs5FormRenderer.php
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');
}
}
}
}
}

0 comments on commit db073f5

Please sign in to comment.