diff --git a/src/BootstrapRenderer.php b/src/BootstrapRenderer.php index 9dd6253..13e1157 100644 --- a/src/BootstrapRenderer.php +++ b/src/BootstrapRenderer.php @@ -14,6 +14,7 @@ use Nette\Forms\Form; use Nette\Forms\FormRenderer; use Nette\Utils\Html; +use SplObjectStorage; /** * Converts a Form into Bootstrap 4 HTML output. @@ -41,7 +42,12 @@ class BootstrapRenderer implements FormRenderer */ protected $gridBreakPoint = 'sm'; - /** @var BootstrapForm */ + /** + * Not necessarily a BootstrapForm: Nette\Forms\Blueprint (the {formPrint} macro) renders + * a plain dummy form through a clone of this renderer. + * + * @var Form + */ protected $form; /** @var int */ @@ -56,16 +62,28 @@ class BootstrapRenderer implements FormRenderer /** @var bool */ private $groupHidden = true; + /** + * What this renderer has already drawn in the current render. + * + * Tracked by identity rather than only through RendererOptions::_RENDERED, because the + * dummy controls Nette\Forms\Blueprint feeds us forward getOption() to the control they + * wrap, so an option written here would be read back off a different object. + * + * @var SplObjectStorage + */ + private $renderedControls; + public function __construct(int $mode = RenderMode::VERTICAL_MODE) { $this->setMode($mode); + $this->renderedControls = new SplObjectStorage(); } /** * Sets the form for which to render. Used only if a specific function of the renderer must be executed * outside of render(), such as during assisted manual rendering. */ - public function attachForm(BootstrapForm $form): void + public function attachForm(Form $form): void { $this->form = $form; } @@ -306,8 +324,6 @@ public function setGroupHidden(bool $groupHidden): BootstrapRenderer /** * Provides complete form rendering. - * - * @param BootstrapForm $form */ public function render(Form $form): string { @@ -327,6 +343,8 @@ public function render(Form $form): string */ public function renderBegin(): string { + $this->renderedControls = new SplObjectStorage(); + foreach ($this->form->getControls() as $control) { if ($control instanceof BaseControl || $control instanceof BootstrapRow) { $control->setOption(RendererOptions::_RENDERED, false); @@ -436,10 +454,16 @@ public function renderBody(): string */ public function renderControl(BaseControl $control): string { - /** @var Html $controlHtml */ $controlHtml = $control->getControl(); - $control->setOption(RendererOptions::_RENDERED, true); - if (($this->form->showValidation || $control->hasErrors()) && $control instanceof IValidationInput) { + $this->markRendered($control); + + // Blueprint's dummy controls return a bare '{input ...}' placeholder, which has + // no attributes to configure — pass it through untouched + if (!$controlHtml instanceof Html) { + return (string) $controlHtml; + } + + if (($this->shouldShowValidation() || $control->hasErrors()) && $control instanceof IValidationInput) { $controlHtml = $control->showValidation($controlHtml); } @@ -468,7 +492,7 @@ public function renderControls($parent): string continue; } - if ($control->getOption(RendererOptions::_RENDERED)) { + if ($this->isRendered($control)) { continue; } @@ -509,12 +533,18 @@ public function renderEnd(): string */ public function renderLabel(BaseControl $control): Html { + $controlLabel = $control->getLabel(); + + // Blueprint's dummy controls carry no caption but do return a '{label ...}' placeholder, + // which already expands to a whole