Skip to content

Add wrapper=False to render a field without its wrapper - #882

Open
shreyanshj10 wants to merge 7 commits into
zostera:mainfrom
shreyanshj10:fix/issue-438-optional-wrapper
Open

shreyanshj10 wants to merge 7 commits into
zostera:mainfrom
shreyanshj10:fix/issue-438-optional-wrapper

Conversation

@shreyanshj10

Copy link
Copy Markdown
Contributor

Two fields cannot share one input-group today, because each field renders its own wrapper div between the group and the widgets. wrapper_class="" does not help: it still renders <div class="">.

This implements the wrapper-less mode from #438. wrapper=False renders the label, widget, errors and help text with no element around them, so the fields can sit directly in an input-group of your own:

<div class="input-group">
    {% bootstrap_field form.field_a wrapper=False %}
    {% bootstrap_field form.field_b wrapper=False %}
</div>

The issue deliberately left the markup open, so this is a proposal as much as an implementation. Four decisions, each cheap to change if you want different output:

  1. The opt-out is a new wrapper argument, default True, on bootstrap_field and, like every other renderer keyword, on bootstrap_form and bootstrap_formset. I did not use wrapper_class=None for it: in a Django template None resolves to '' (the existing test_wrapper_class shows this), so it cannot be told apart from wrapper_class="", and that output stays exactly as it is. A boolean passed as False matches show_help, show_label and server_side_validation. I did not add a BOOTSTRAP5 setting, because dropping the wrapper for every field in a project looks more like an accident than an intent, but it is a one-line addition if you would rather have one.
  2. Help text is still rendered, unchanged, in the position it has now.
  3. The error block is still rendered, unchanged, in the position it has now. invalid-feedback stays a sibling of the is-invalid widget, which is what Bootstrap wants inside an input group. Adding has-validation to the group is left to you, as it already is when you write an input group by hand.
  4. The wrapper's classes are dropped: wrapper_class, the row of the horizontal layout, and required_css_class / error_css_class / success_css_class. There is no element left to carry them, and the element that takes the wrapper's place is yours, so it can carry whatever it needs. The is-valid / is-invalid classes on the widget itself are untouched. The one exception is form-floating, which is functional rather than layout: with layout="floating" the widget and its label keep their own form-floating element, the same nesting used for floating labels inside an input group with add-ons.

Rendering is byte-identical for anyone who does not pass wrapper.

Tests added in tests/test_bootstrap_field_parameters.py: the default output is unchanged, wrapper=False on its own, two fields sharing one input-group, errors and help text surviving on a bound form with the indicator classes gone, form-floating surviving with layout="floating", and wrapper=False reaching fields through bootstrap_form. Docs and CHANGELOG.md are updated.

Fixes #438

I used an AI assistant while writing this. The four decisions above and the code are mine, and I can explain and defend either.

@shreyanshj10
shreyanshj10 marked this pull request as ready for review September 29, 2026 13:00
@shreyanshj10
shreyanshj10 marked this pull request as draft September 29, 2026 13:12
@shreyanshj10
shreyanshj10 marked this pull request as ready for review September 30, 2026 04:58
@dyve dyve mentioned this pull request Oct 3, 2026
@shreyanshj10
shreyanshj10 force-pushed the fix/issue-438-optional-wrapper branch from 669fa6a to 0004da0 Compare October 3, 2026 15:36

@dyve dyve left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on the branch rather than from the description. Two fields share one input-group with no wrapper divs, errors and help text survive on a bound form, and it propagates from bootstrap_form wrapper=False. The design answers the question #438 was parked on, and the docstring saying which classes are dropped and why is the part that makes it reviewable.

One problem with the documented example, suggested inline.

Bootstrap does not put <label> inside .input-group. Every child of an input group is laid out as a flex item, so a form-label between the controls is stretched into the row rather than sitting above it. Bootstrap's own examples keep the label outside the group. Someone copying this example gets labels wedged between the two inputs.

For a group holding several fields there is no sensible place for two separate visible labels, so the fields themselves should not render one. show_label='visually-hidden' would be the accessible answer, keeping the label for screen readers while leaving the layout alone, but it is currently a no-op on bootstrap_field: renderers.py line 392 tests if not self.show_label, and the string is truthy. I raised that on #879, which adds the same values to bootstrap_label and needs a one-line fix in the same place.

So there are two ways to sequence this:

  • merge #879 with that one-line fix first, then use show_label='visually-hidden' here, which is the accessible version; or
  • take the suggestion below now, using 'skip', and tighten it to 'visually-hidden' once #879 lands.

Either way the example should not show labels inside the group. Flagging the accessibility tradeoff explicitly because 'skip' removes the label entirely, which is fine when your own markup supplies one and not fine when it does not.

Comment thread docs/forms.rst
Comment on lines +58 to +61
<div class="input-group">
{% bootstrap_field form.field_a wrapper=False %}
{% bootstrap_field form.field_b wrapper=False %}
</div>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div class="input-group">
{% bootstrap_field form.field_a wrapper=False %}
{% bootstrap_field form.field_b wrapper=False %}
</div>
<div class="input-group">
{% bootstrap_field form.field_a wrapper=False show_label='skip' %}
{% bootstrap_field form.field_b wrapper=False show_label='skip' %}
</div>

The group needs a label of its own, outside it, since the fields no longer render one. Once #879 lands with the renderers.py fix, show_label='visually-hidden' is the better value here: it keeps a per-field label for screen readers without disturbing the flex layout.

Comment thread docs/forms.rst
Comment on lines +67 to +72
<div class="input-group">
<label for="id_field_a" class="form-label">Field a</label>
<input type="text" name="field_a" class="form-control" id="id_field_a">
<label for="id_field_b" class="form-label">Field b</label>
<input type="text" name="field_b" class="form-control" id="id_field_b">
</div>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div class="input-group">
<label for="id_field_a" class="form-label">Field a</label>
<input type="text" name="field_a" class="form-control" id="id_field_a">
<label for="id_field_b" class="form-label">Field b</label>
<input type="text" name="field_b" class="form-control" id="id_field_b">
</div>
<div class="input-group">
<input type="text" name="field_a" class="form-control" id="id_field_a">
<input type="text" name="field_b" class="form-control" id="id_field_b">
</div>

Matching the input above, and matching what Bootstrap's input-group examples actually look like.

dyve and others added 3 commits October 4, 2026 09:00
Bootstrap lays out every child of an input group as a flex item, so a
field's label is stretched into the row instead of sitting above it, and
Bootstrap's own examples keep the label outside the group. The example
now renders the fields with show_label='skip' and says the group takes a
label of your own, outside it.
@dyve

dyve commented Oct 7, 2026

Copy link
Copy Markdown
Member

The code is good and the tests pass locally, 202 of them. I checked the two behaviour claims in the new docs against this branch rather than taking them on trust, and both hold: wrapper=False show_label='skip' renders just the widget, no wrapper div and no label, and layout="floating" keeps its own form-floating element inside the group.

One thing about the recommendation itself.

A single label outside the group names only one of the fields

The example puts two fields in one group with show_label='skip' and suggests one label of your own outside it. A <label for="..."> points at one control, so the second field has no label associated with it. What it falls back on is the placeholder, which this package sets from the label by default, and Bootstrap is explicit that this is a last resort: "If none of these are present, assistive technologies may resort to using the placeholder attribute as a fallback", and a placeholder disappears as soon as the field has content.

Bootstrap's own answer for exactly this case, from forms/input-group.md:

<div class="input-group">
  <span class="input-group-text">First and last name</span>
  <input type="text" aria-label="First name" class="form-control">
  <input type="text" aria-label="Last name" class="form-control">
</div>

Each input carries its own aria-label, and the shared human-readable text sits in an .input-group-text. Across that whole page there is no input without its own accessible name.

bootstrap_field has no attrs kwarg, so the route is the widget in the form definition. I verified this passes through:

first = forms.CharField(widget=forms.TextInput(attrs={"aria-label": "First name"}))
<input type="text" name="first" aria-label="First name" class="form-control" ... >

Could the example in docs/forms.rst do that instead? It would show the pattern Bootstrap shows, and it keeps the docs from recommending something that quietly costs the second field its name.

The one-field case in your text is right

Worth keeping separate, because Bootstrap treats them differently. Where a group holds a single field, Bootstrap puts a visually-hidden label as a sibling before .input-group, not inside it, for example in forms/layout.md:

<div class="col-auto">
  <label class="visually-hidden" for="autoSizingInputGroup">Username</label>
  <div class="input-group">
    <div class="input-group-text">@</div>
    <input type="text" class="form-control" id="autoSizingInputGroup" placeholder="Username">
  </div>
</div>

So "render the field without its label and write the label yourself, outside the group" is exactly right there. It is only the multi-field case where one label cannot cover them all. If the docs said which of the two you are in, the advice would carry further than the example.

Nothing here blocks the code, it is all the one paragraph of prose. The branch is also BEHIND now that main has moved, so it needs an update from main before it can merge.

…name

26.4 shipped while this was open and stamped the section the changelog entry
was written into, so this takes main's CHANGELOG.md and re-adds the entry under
the fresh `## Unreleased`.

The input-group example also needed fixing. It told you to render several
fields with `show_label='skip'` and write one label of your own outside the
group, but a `<label for>` names one control, so every field after the first
was left without an accessible name. What they fall back on is the
`placeholder`, which this package sets from the label by default and which
Bootstrap calls a last resort, since it disappears once the field has content.

The example now gives each field its own `aria-label` and puts the shared,
readable text in an `input-group-text`, which is what Bootstrap's own "Multiple
inputs" example does. `bootstrap_field` takes no `attrs` argument, so the
attribute goes on the widget, and the example shows that.

Added the single-field case as a separate paragraph, because Bootstrap treats
it differently: there it keeps a real `<label class="visually-hidden">` and
places it before the group rather than inside it. The advice this example
replaced was right for that case and only wrong for several fields.

No change to the code in this pull request.

Co-Authored-By: shreyanshj10 <shreyanshj10@users.noreply.github.com>
@dyve

dyve commented Oct 11, 2026

Copy link
Copy Markdown
Member

Picking this up so it does not sit any longer. The code is good and I am taking it as it stands; I have pushed one commit to your branch, since you left maintainer edits on. It changes no code, only the changelog placement and the docs example.

Changelog. 26.4 shipped this morning and stamped the ## Unreleased section your entry was written into. A plain merge would have filed it under a released version with nothing going red, so I took main's CHANGELOG.md and re-added your entry under the fresh heading.

The input-group example. This is the point from my review on 7 October that had not been answered. The example told the reader to render several fields with show_label='skip' and write one label of their own outside the group, but a <label for> names a single control, so every field after the first was left without an accessible name. They fall back on the placeholder, which this package sets from the label by default, and which Bootstrap calls a last resort because it disappears once the field has content.

The example now gives each field its own aria-label and puts the shared readable text in an input-group-text, which is what Bootstrap's own "Multiple inputs" example does. Since bootstrap_field takes no attrs argument, the attribute goes on the widget, and the example shows that too.

Your original advice was not wrong, it was right for a different case, so I kept it as its own paragraph: where a group holds a single field, Bootstrap keeps a real <label class="visually-hidden"> and places it before the group rather than inside it. The docs now say which case you are in.

202 tests pass, coverage holds at 100%, just docs builds clean. Thanks for this one, wrapper=False closes a request that had been open since 2023.

Both pull requests added an entry to the same `## Unreleased` section, which is
the whole conflict. Kept all three, ordered as MAINTAINING.md asks, with the
`show_label` fix above the two features.

`renderers.py` and the template tags merged without conflict, but both pull
requests changed the field renderer, so I checked the combination neither could
have tested on its own. `wrapper=False` with `show_label='visually-hidden'`
renders a `visually-hidden` label and no wrapper, and with `show_label='skip'`
renders the widget alone. 210 tests pass and coverage holds at 100%.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrapper collides with input-group functionality

2 participants