Skip to content

Commit

Permalink
Bootstrap 4: add support for rendering errors in tooltips for all modes
Browse files Browse the repository at this point in the history
  • Loading branch information
xificurk committed May 2, 2020
1 parent 5b44cad commit c79a848
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -104,6 +104,8 @@ $renderer->setHorizontalMode(4, 8); // Horizontal form (you can optionally set t

Use `$renderer->setRenderValidState(true)` to enable/disable rendering of "valid" form control state for filled inputs after form submission.

In inline mode the error messages are always rendered as tooltips. In the other modes you can switch between standard and tooltip rendering by calling `$renderer->setUseErrorTooltips(true)`.

You can enable the use of [custom form controls](https://getbootstrap.com/docs/4.4/components/forms/#custom-forms) by `$renderer->setUseCustomControls(true)`.

To render a checkbox as a switch, you need to set type option: `$checkboxInput->setOption('type', 'switch')`.
Expand All @@ -119,6 +121,7 @@ formRenderer:
bootstrap4:
mode: horizontal
renderValidState: true
useErrorTooltips: true
useCustomControls: true
imports:
- %appDir%/templates/@form-extras.latte
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/FormRendererDI/FormRendererExtension.php
Expand Up @@ -45,6 +45,7 @@ public function getConfigSchema(): Nette\Schema\Schema
)->default(Bootstrap4Renderer::MODE_BASIC),
'useCustomControls' => Nette\Schema\Expect::bool(false),
'renderValidState' => Nette\Schema\Expect::bool(false),
'useErrorTooltips' => Nette\Schema\Expect::bool(false),
]);

return Nette\Schema\Expect::structure([
Expand Down Expand Up @@ -119,6 +120,7 @@ private function setupBootstrap4Renderer(\stdClass $config): void
$resultDefinition = $factory->getResultDefinition();
$resultDefinition->setArguments(['templateRendererFactory' => $this->prefix('@templateRendererFactory')]);
$resultDefinition->addSetup('setRenderValidState', [$config->renderValidState]);
$resultDefinition->addSetup('setUseErrorTooltips', [$config->useErrorTooltips]);
$resultDefinition->addSetup('setUseCustomControls', [$config->useCustomControls]);
foreach ($config->imports as $templateFile) {
$resultDefinition->addSetup('importTemplate', [$templateFile]);
Expand Down
13 changes: 13 additions & 0 deletions src/FormRenderer/Bootstrap4Renderer.php
Expand Up @@ -28,6 +28,8 @@ class Bootstrap4Renderer implements Nette\Forms\IFormRenderer

private bool $renderValidState = false;

private bool $useErrorTooltips = false;

private bool $useCustomControls = false;

private string $mode = self::MODE_BASIC;
Expand All @@ -51,6 +53,11 @@ public function setRenderValidState(bool $renderValidState = true): void
$this->renderValidState = $renderValidState;
}

public function setUseErrorTooltips(bool $useErrorTooltips = true): void
{
$this->useErrorTooltips = $useErrorTooltips;
}

public function setUseCustomControls(bool $useCustomControls = true): void
{
$this->useCustomControls = $useCustomControls;
Expand Down Expand Up @@ -80,6 +87,7 @@ public function render(Form $form): string
$templateRenderer = $this->getTemplateRenderer();
$template = $templateRenderer->getTemplate();
$template->addFilter('validationClass', new ValidationClassFilter('is-invalid', $this->shouldRenderValidState($form) ? 'is-valid' : null));
$template->useErrorTooltips = $this->shouldUseErrorTooltips();
$template->useCustomControls = $this->useCustomControls;
$template->mode = $this->mode;
$template->gridOffsetClass = $this->mode === self::MODE_HORIZONTAL ? sprintf('offset-sm-%d', $this->labelCols) : null;
Expand Down Expand Up @@ -145,4 +153,9 @@ protected function shouldRenderValidState(Form $form): bool
return $this->renderValidState && (bool) $form->isSubmitted();
}

protected function shouldUseErrorTooltips(): bool
{
return $this->mode === self::MODE_INLINE || $this->useErrorTooltips;
}

}
14 changes: 7 additions & 7 deletions src/FormRenderer/templates/bootstrap4.latte
Expand Up @@ -88,29 +88,29 @@
{/define}

{define #control-type-default}
<div n:tag-if="$mode === inline" n:class="position-relative, $inlineSpacingClasses">
<div n:tag-if="$useErrorTooltips" n:class="position-relative, $mode === inline ? $inlineSpacingClasses">
{include #helpers-override-split, blockNamePrefix => input, control => $control}
{if $mode === inline}{include #control-errors, control => $control}{/if}
{if $useErrorTooltips}{include #control-errors, control => $control}{/if}
</div>
{include #control-help, control => $control}
{if $mode !== inline}{include #control-errors, control => $control}{/if}
{if !$useErrorTooltips}{include #control-errors, control => $control}{/if}
{/define}

{define #control-type-checkboxlist}
<div n:class="$mode === inline ? 'position-relative form-inline'">
<div n:class="$useErrorTooltips ? position-relative, $mode === inline ? form-inline">
{include #helpers-override-split, blockNamePrefix => input, control => $control}
{if $mode === inline}{include #control-errors, control => $control}{/if}
{if $useErrorTooltips}{include #control-errors, control => $control}{/if}
</div>
{include #control-help, control => $control}
{if $mode !== inline}{include #control-errors, control => $control}{/if}
{if !$useErrorTooltips}{include #control-errors, control => $control}{/if}
{/define}

{define #control-type-radio}
{include #control-type-checkboxlist, control => $control}
{/define}

{define #control-errors}
{if $mode === inline}
{if $useErrorTooltips}
<div n:if="count($control->getErrors()) > 0" class="d-block invalid-tooltip" n:inner-foreach="$control->getErrors() as $error">{$error}{sep} {/sep}</div>
{else}
<div n:foreach="$control->getErrors() as $error" class="d-block invalid-feedback">{$error}</div>
Expand Down
1 change: 1 addition & 0 deletions tests/Bridges/FormRendererDI/fixtures/config.neon
Expand Up @@ -13,6 +13,7 @@ formRenderer:
bootstrap4:
mode: horizontal
useCustomControls: false
useErrorTooltips: false
renderValidState: false
imports:
- %fixturesDir%/form.latte
Expand Down
5 changes: 5 additions & 0 deletions tests/FormRenderer/Bootstrap4RendererTest.phpt
Expand Up @@ -115,6 +115,11 @@ class Bootstrap4RendererTest extends TestCase
$form->setRenderer($renderer);

HtmlAssert::matchFile(__DIR__ . "/expected/bootstrap4-{$mode}-errors.html", $form->__toString());

if ($mode !== FormRenderer\Bootstrap4Renderer::MODE_INLINE) {
$renderer->setUseErrorTooltips();
HtmlAssert::matchFile(__DIR__ . "/expected/bootstrap4-{$mode}-errors-tooltips.html", $form->__toString());
}
}

/**
Expand Down
144 changes: 144 additions & 0 deletions tests/FormRenderer/expected/bootstrap4-basic-errors-tooltips.html
@@ -0,0 +1,144 @@
<form class="form-class1 form-class2" action="#" method="post" enctype="multipart/form-data">
<div class="alert alert-danger">
Form error 1.
</div>
<div class="alert alert-danger">
Form error 2.
</div>
<div class="alert alert-danger">
Control hidden error 1.
</div>
<div class="alert alert-danger">
Control hidden error 2.
</div>
<fieldset id="custom-group-id">
<legend>Group 1</legend>
<p>Group 1 description.</p>
<div class="form-group">
<label for="frm-text">Text</label>
<div class="position-relative">
<input type="text" name="text" id="frm-text" class="form-control is-invalid">
<div class="d-block invalid-tooltip">Control text error 1. Control text error 2.</div>
</div>
</div>
<div class="form-group">
<label for="frm-textarea">TextArea</label>
<div class="position-relative">
<textarea class="form-control is-invalid" name="textarea" id="frm-textarea">Lorem ipsum</textarea>
<div class="d-block invalid-tooltip">Control textarea error 1. Control textarea error 2.</div>
</div>
</div>
<fieldset>
<legend><span>Group 2 label</span></legend>
<p><span>Group 2 description.</span></p>
<div class="form-group">
<div class="position-relative">
<div class="form-check">
<input type="checkbox" name="container[checkbox]" id="frm-container-checkbox" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-container-checkbox">Checkbox</label>
</div>
<div class="d-block invalid-tooltip">Control checkbox error 1. Control checkbox error 2.</div>
</div>
</div>
<div class="form-group">
<label>CheckBoxList</label>
<div class="position-relative">
<div class="form-check">
<input type="checkbox" name="container[checkboxlist][]" id="frm-container-checkboxlist-1" value="1" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-container-checkboxlist-1">one</label>
</div>
<div class="form-check">
<input type="checkbox" name="container[checkboxlist][]" id="frm-container-checkboxlist-2" value="2" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-container-checkboxlist-2">two</label>
</div>
<div class="d-block invalid-tooltip">Control checkboxlist error 1. Control checkboxlist error 2.</div>
</div>
</div>
<div class="form-group">
<label>RadioList</label>
<div class="position-relative">
<div class="form-check">
<input type="radio" name="container[radiolist]" id="frm-container-radiolist-3" value="3" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-container-radiolist-3">three</label>
</div>
<div class="form-check">
<input type="radio" name="container[radiolist]" id="frm-container-radiolist-4" value="4" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-container-radiolist-4">four</label>
</div>
<div class="d-block invalid-tooltip">Control radiolist error 1. Control radiolist error 2.</div>
</div>
</div>
<div class="form-group">
<label for="frm-container-innerContainer-selectbox">Selectbox</label>
<div class="position-relative">
<select class="form-control is-invalid" name="container[innerContainer][selectbox]" id="frm-container-innerContainer-selectbox"><option value=""></option><option value="5">five</option><option value="6">six</option></select>
<div class="d-block invalid-tooltip">Control selectbox error 1. Control selectbox error 2.</div>
</div>
</div>
<div class="form-group">
<label for="frm-container-innerContainer-upload">Upload</label>
<div class="position-relative">
<input type="file" name="container[innerContainer][upload]" id="frm-container-innerContainer-upload" data-nette-rules='[{"op":":fileSize","msg":"The size of the uploaded file can be up to 1048576 bytes.","arg":1048576}]' class="form-control-file is-invalid">
<div class="d-block invalid-tooltip">Control upload error 1. Control upload error 2.</div>
</div>
</div>
</fieldset>
</fieldset>
<div class="form-group">
<label class="label-class1 label-class2" for="frm-foo">Foo</label>
<div class="position-relative">
<div data-foo="foo" class="control-class1 control-class2"><input name="foo" id="frm-foo"><span>foo</span></div>
<div class="d-block invalid-tooltip">Control foo error 1. Control foo error 2.</div>
</div>
</div>
<div class="form-group">
<input type="submit" name="send" class="btn btn-primary" value="SubmitButton">
<input type="button" name="button" class="btn btn-secondary" value="Button">
<input type="button" name="warning" class="btn btn-warning">
</div>
<div class="form-group">
<label>InlineCheckboxList</label>
<div class="position-relative">
<div class="form-check form-check-inline">
<input type="checkbox" name="inlinecheckboxlist[]" id="frm-inlinecheckboxlist-0" disabled value="0" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-inlinecheckboxlist-0">foo</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" name="inlinecheckboxlist[]" id="frm-inlinecheckboxlist-1" value="1" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-inlinecheckboxlist-1">bar</label>
</div>
<div class="d-block invalid-tooltip">Control inlinecheckboxlist error 1. Control inlinecheckboxlist error 2.</div>
</div>
</div>
<div class="form-group">
<label>InlineRadioList</label>
<div class="position-relative">
<div class="form-check form-check-inline">
<input type="radio" name="inlineradiolist" id="frm-inlineradiolist-0" disabled value="0" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-inlineradiolist-0">foo</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" name="inlineradiolist" id="frm-inlineradiolist-1" value="1" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-inlineradiolist-1">bar</label>
</div>
<div class="d-block invalid-tooltip">Control inlineradiolist error 1. Control inlineradiolist error 2.</div>
</div>
</div>
<div class="form-group">
<label for="frm-range">Range</label>
<div class="position-relative">
<input type="range" name="range" id="frm-range" class="form-control-range is-invalid">
<div class="d-block invalid-tooltip">Control range error 1. Control range error 2.</div>
</div>
</div>
<div class="form-group">
<div class="position-relative">
<div class="form-check">
<input type="checkbox" name="switch" id="frm-switch" class="form-check-input is-invalid">
<label class="form-check-label" for="frm-switch">Switch</label>
</div>
<div class="d-block invalid-tooltip">Control switch error 1. Control switch error 2.</div>
</div>
<small class="form-text text-muted">Switch description</small>
</div>
<input type="hidden" name="hidden" value=""> </form>

0 comments on commit c79a848

Please sign in to comment.