fix: make {formPrint} work with BootstrapRenderer - #132
Open
dakorpar wants to merge 3 commits into
Open
Conversation
Nette\Forms\Blueprint, which backs the {formPrint} macro, renders a dummy
plain Nette form through a clone of the real form's renderer. Four things
made BootstrapRenderer choke on it:
- attachForm() required a BootstrapForm, so render() threw a TypeError on
Blueprint's dummy form. Widened to Form, with the one BootstrapForm-only
access (showValidation) moved behind shouldShowValidation().
- renderControl() assumed getControl() returns Html; the dummy returns a
bare '{input ...}' placeholder string. It is now passed through as-is,
there being no attributes to configure on it.
- renderLabel() dropped the '{label ...}' placeholder, because the dummy
carries no caption. The placeholder already expands to a whole <label>,
so it is emitted raw rather than nested inside another one.
- BootstrapRow, being a fake control, is still walked by Blueprint like a
real one, and had no lookupPath()/getLabel()/isRequired(). Added to
FakeControlTrait.
Grouped controls also came out twice, because the renderer marked them with
its own '_rendered' option while Blueprint's dummy only forwards Nette's
'rendered' key to the wrapped control. RendererOptions::_RENDERED now uses
that same key, which is also the one BaseControl::getControl() sets.
BC note: the constant's *value* changed. Code using RendererOptions::_RENDERED
is unaffected; code using the literal '_rendered' is not.
Also fixes BootstrapRow::getOption() warning on unset options instead of
returning null as documented — reached through the same path.
Closes #63
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #132 +/- ##
============================================
+ Coverage 97.05% 97.53% +0.47%
- Complexity 309 326 +17
============================================
Files 25 25
Lines 986 1014 +28
============================================
+ Hits 957 989 +32
+ Misses 29 25 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Direct coverage for what the {formPrint} fix added, which until now was
only exercised incidentally through one Blueprint case:
- BootstrapRow's lookupPath()/getLabel()/isRequired(), including that the
path a fake control reports matches what a real sibling reports
- getOption() returning null for an unset key without raising a warning
- the renderer drawing a plain Nette\Forms\Form, directly and via a clone
- a Bootstrap 5 blueprint, asserting the v5 wrapper rather than v4's
The plain-form, cloned-renderer and Bootstrap 5 scenarios are adapted from
f3l1x's PR #111, which independently fixed the same Blueprint incompatibility.
Also call BootstrapForm::isShowValidation() rather than reaching for the
magic property, as #111 does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reusing Nette's 'rendered' option to de-duplicate grouped controls under
{formPrint} had a cost that outweighed it: BaseControl::getControl() sets
that key on every fetch, so the renderer could no longer tell "I drew this"
apart from "somebody asked for the html". Assisted manual rendering --
renderControls() without a preceding render(), which attachForm() documents
-- then silently skipped any control whose html had been fetched.
RendererOptions::_RENDERED goes back to '_rendered', so its value is
unchanged from before this branch and nothing outside is affected. The
renderer instead remembers what it drew in an SplObjectStorage, which is
identity-based and therefore immune to Blueprint's dummy controls
forwarding getOption() to the control they wrap. Marking a control rendered
by hand still works, since isRendered() honours the option too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 14, 2026
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #63.
The reported exception (
Runtime::renderformPrint()) was from the Latte 2 era and no longer occurs, but the feature is still broken on current master — it just fails later and differently.Nette\Forms\Blueprint, which backs{formPrint}, renders a dummy plain Nette form through a clone of the real form's renderer, andBootstrapRendererassumes both the form and its controls are ours.Reproduced before touching anything (
Blueprint::generateLatte()is public, so no Latte install needed):What was wrong
attachForm()required aBootstrapFormrenderControl()assumedgetControl()returnsHtml{input …}stringrenderLabel()returns early whengetCaption()is null{label …}silently missing from the blueprintBootstrapRowhas nolookupPath()addRow()The duplication is the interesting one. Blueprint's dummy controls forward
getOption()to the control they wrap, so the renderer's_renderedmark was written to the dummy and read back off a different object — always false, so every grouped control was drawn again by the trailing pass. The renderer now remembers what it drew in anSplObjectStorage, which is identity-based and immune to that forwarding.What changed
attachForm(Form $form); the singleBootstrapForm-only access (showValidation) now goes throughshouldShowValidation(), which calls the real getter rather than the magic property.Htmlcontrol output is passed through untouched — there are no attributes to configure on a placeholder.Html::el(), not nested —{label name/}already expands to a whole<label>. This keepsrenderLabel(): Htmlintact; Add Blueprint/formPrint compatibility to BootstrapRenderer #111 widens the signature toHtml|stringinstead, which works equally well but changes the public return type.markRendered()/isRendered()), with__clone()giving a cloned renderer its own.RendererOptions::_RENDEREDis unchanged — it is still written, and still honoured if you set it by hand.lookupPath()/getLabel()/isRequired()added toFakeControlTrait.BootstrapRow::getOption()warned on unset options instead of returning null as its own docblock promises. Reached through this same path.Risk to existing behaviour
I probed this directly rather than reasoning about it. One earlier revision of this branch reused Nette's own
'rendered'option key to fix the duplication, which turned out to break assisted manual rendering:BaseControl::getControl()sets that key on every fetch, sorenderControls()called without a precedingrender()silently skipped any control whose html had been fetched. That is why the bookkeeping is identity-based now, andRenderedOptionTest::testAssistedRenderingIsNotDisturbedByFetchingControlHtmlfails if anyone tries the shortcut again.What remains, and is unavoidable for this fix: a subclass overriding
attachForm(BootstrapForm $form)becomes a fatal error at class-declaration time, since a child may not narrow a parameter type. #111 has the same break. Anything that only callsattachForm()is fine.renderLabel()now callsgetLabel()before the caption check, so a control with no caption getsgetLabel()called where it previously did not. Nette's own controls have no side effects there, and all 66 snapshot fixtures are byte-identical.Result
Tests
tests/Rendering/BlueprintTest.php— plain forms, side-by-side wrappers, label nesting, group de-duplication, containers + hidden fields, grid rows, Bootstrap 5, and the renderer drawing a plainNette\Forms\Formboth directly and through a clone.tests/Rendering/RenderedOptionTest.php— the bookkeeping: a control whosegetControl()was called still renders (both under full render and under assisted manual rendering), rendering twice is idempotent, a grouped control appears once, and marking a control rendered by hand still skips it.tests/Grid/BootstrapRowTest.php— extended with the fake-control surface:lookupPath()(asserting a fake control reports the same path a real sibling would),getLabel(),isRequired(), andgetOption()returning null for an unset key without raising a warning.Verified against
origin/master's source rather than assumed: 6 of the new tests error there.make qa(PHPStan level 7 + codesniffer) and the full suite pass: 179 tests, 284 assertions.Known limitation
A form built with the Bootstrap grid produces one spurious
{input bootstrap_row_N}line per row. Blueprint walks everything$form->getControls()yields,BootstrapRowmust implementControlto appear in the renderer's own loop, and Blueprint's dummy emits an{input …}placeholder for whatever it wraps — so the row cannot be filtered out without either coupling to Blueprint's anonymous dummy class or reworking how rows sit in the component tree. It no longer crashes; delete the line. Happy to chase it further if you think it's worth it.🤖 Generated with Claude Code