Skip to content

Commit 17c3e84

Browse files
committed
rendering labels and description with classes
1 parent e048e1a commit 17c3e84

File tree

7 files changed

+219
-21
lines changed

7 files changed

+219
-21
lines changed

docs/advanced.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,34 @@ Bootstrap-Flask, simply use the built-in class ``SwitchField()`` instead of
140140
``BooleanField()``. See also the example application.
141141

142142

143+
.. _inputcustomization:
144+
145+
Form Input Customization
146+
------------------------
147+
148+
Rendering Label
149+
~~~~~~~~~~~~~~~
150+
151+
Bootstrap offers control for rendering
152+
`text <https://getbootstrap.com/docs/5.3/utilities/text/>`_. This is supported
153+
for inputs of a form by adding ``render_kw={'label_class': '... ...'}`` to the
154+
field constructor. In order to control the rendering of the label of a field,
155+
use ``render_kw={'label_class': '... ...'}``. See also the example application.
156+
157+
Rendering Radio Label
158+
~~~~~~~~~~~~~~~~~~~~~
159+
160+
Similar support exists for the rendering of the labels of options of a
161+
``RadioField()` with ``render_kw={'radio_class': '... ...'}``. See also the
162+
example application.
163+
164+
Rendering Description
165+
~~~~~~~~~~~~~~~~~~~~~
166+
167+
Use ``render_kw={'descr_class': '... ...'}`` for controlling the rendering of a
168+
field's description. See also the example application.
169+
170+
143171
.. _bootswatch_theme:
144172

145173
Bootswatch Themes

docs/macros.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ API
131131
form group classes, it will read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first
132132
(the default value is ``mb-3``).
133133

134-
.. tip:: See :ref:`button_customization` and :ref:`checkbox_customization` to learn more on customizations.
134+
.. tip:: See :ref:`button_customization`, :ref:`checkbox_customization` and :ref:`input_customization` to learn more on customizations.
135135

136136

137137
render_form()

examples/bootstrap5/app.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131

3232
class ExampleForm(FlaskForm):
3333
"""An example form that contains all the supported bootstrap style form fields."""
34-
date = DateField(description="We'll never share your email with anyone else.") # add help text with `description`
35-
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder'}) # add HTML attribute with `render_kw`
34+
date = DateField()
35+
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder', 'class': 'fst-italic'})
3636
datetime_local = DateTimeLocalField()
37-
time = TimeField()
37+
time = TimeField(description="This isn't shared", render_kw={'descr_class': 'fs-1 text-decoration-underline'})
3838
month = MonthField()
3939
color = ColorField()
4040
floating = FloatField()
@@ -45,7 +45,7 @@ class ExampleForm(FlaskForm):
4545
url = URLField()
4646
telephone = TelField()
4747
image = FileField(render_kw={'class': 'my-class'}, validators=[Regexp('.+\.jpg$')]) # add your class
48-
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
48+
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline', 'radio_class': 'text-decoration-line-through'})
4949
select = SelectField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
5050
select_multiple = SelectMultipleField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
5151
bio = TextAreaField()
@@ -56,16 +56,32 @@ class ExampleForm(FlaskForm):
5656
submit = SubmitField()
5757

5858

59+
class ExampleFormInline(FlaskForm):
60+
"""An example inline form."""
61+
floating = FloatField(description='a float', render_kw={'label_class': 'text-decoration-underline'})
62+
integer = IntegerField(description='an int', render_kw={'descr_class': 'text-decoration-line-through'})
63+
option = RadioField(description='Choose one', choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'radio_class': 'text-decoration-line-through', 'descr_class': 'fw-bold'})
64+
submit = SubmitField()
65+
66+
67+
class ExampleFormHorizontal(FlaskForm):
68+
"""An example horizontal form."""
69+
floating = FloatField(description='a float', render_kw={'label_class': 'text-decoration-underline'})
70+
integer = IntegerField(description='an int', render_kw={'descr_class': 'text-decoration-line-through'})
71+
option = RadioField(description='choose 1', choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline'})
72+
submit = SubmitField()
73+
74+
5975
class HelloForm(FlaskForm):
6076
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
6177
password = PasswordField('Password', validators=[DataRequired(), Length(8, 150)])
62-
remember = BooleanField('Remember me')
78+
remember = BooleanField('Remember me', description='Rember me on my next visit', render_kw={'descr_class': 'fw-bold text-decoration-line-through'})
6379
submit = SubmitField()
6480

6581

6682
class ButtonForm(FlaskForm):
6783
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
68-
confirm = SwitchField('Confirmation')
84+
confirm = SwitchField('Confirmation', description='Are you sure?', render_kw={'label_class': 'font-monospace text-decoration-underline'})
6985
submit = SubmitField()
7086
delete = SubmitField()
7187
cancel = SubmitField()
@@ -190,7 +206,9 @@ def test_form():
190206
contact_form=ContactForm(),
191207
im_form=IMForm(),
192208
button_form=ButtonForm(),
193-
example_form=ExampleForm()
209+
example_form=ExampleForm(),
210+
inline_form=ExampleFormInline(),
211+
horizontal_form=ExampleFormHorizontal()
194212
)
195213

196214

examples/bootstrap5/templates/form.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ <h2>Example Form</h2>
3131
submit = SubmitField()</pre>
3232
{{ render_form(example_form) }}
3333

34+
<h2>Inline form</h2>
35+
<pre>{% raw %}{{ render_form(inline_form, form_type='inline') }}{% endraw %}</pre>
36+
{{ render_form(inline_form, form_type='inline') }}
37+
38+
<h2>Horizontal form</h2>
39+
<pre>{% raw %}{{ render_form(horizontal_form, form_type='horizontal') }}{% endraw %}</pre>
40+
{{ render_form(horizontal_form, form_type='horizontal') }}
41+
3442
<h2>Render a form with render_form</h2>
3543
<pre>{% raw %}{{ render_form(form) }}{% endraw %}</pre>
3644
{{ render_form(form) }}

flask_bootstrap/templates/bootstrap5/form.html

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@
4545

4646
{% set form_group_classes = form_group_classes or config.BOOTSTRAP_FORM_GROUP_CLASSES %}
4747

48+
{# support for label_class and descr_class which are popped, to prevent they are added to input, but restored at the end of this macro for the next rendering #}
49+
{%- set label_class = '' -%}
50+
{%- if field.render_kw.label_class -%}
51+
{% set label_class = field.render_kw.pop('label_class', '') -%}
52+
{% set label_classes = ' ' + label_class -%}
53+
{%- endif -%}
54+
{%- set radio_class = '' -%}
55+
{%- if field.render_kw.radio_class -%}
56+
{% set radio_class = field.render_kw.pop('radio_class', '') -%}
57+
{% set radio_classes = ' ' + radio_class -%}
58+
{%- endif -%}
59+
{%- set descr_class = '' -%}
60+
{%- if field.render_kw.descr_class -%}
61+
{% set descr_class = field.render_kw.pop('descr_class', '') -%}
62+
{% set descr_classes = ' ' + descr_class -%}
63+
{%- endif -%}
64+
4865
{# combine render_kw class or class/class_ argument with Bootstrap classes #}
4966
{% set render_kw_class = ' ' + field.render_kw.class if field.render_kw.class else '' %}
5067
{% set class = kwargs.pop('class', '') or kwargs.pop('class_', '') %}
@@ -68,14 +85,14 @@
6885
{%- else -%}
6986
{{ field(class="form-check-input%s" % extra_classes, **field_kwargs)|safe }}
7087
{%- endif %}
71-
{{ field.label(class="form-check-label", for=field.id)|safe }}
88+
{{ field.label(class="form-check-label%s" % label_classes, for=field.id)|safe }}
7289
{%- if field.errors %}
7390
{%- for error in field.errors %}
7491
<div class="invalid-feedback d-block">{{ error }}</div>
7592
{%- endfor %}
7693
{%- elif field.description -%}
7794
{% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
78-
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
95+
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
7996
{% endcall %}
8097
{%- endif %}
8198
</div>
@@ -85,12 +102,13 @@
85102
this is just a hack for now, until I can think of something better #}
86103
<div class="{{ form_group_classes }} {% if form_type == 'horizontal' %}row{% endif %}{% if required %} required{% endif %}">
87104
{%- if form_type == "inline" %}
88-
{{ field.label(class="visually-hidden")|safe }}
105+
{{ field.label(class="visually-hidden%s" % label_classes)|safe }}
89106
{% elif form_type == "horizontal" %}
90107
{{ field.label(class="col-form-label" + (
91-
" col-%s-%s" % horizontal_columns[0:2]))|safe }}
108+
" col-%s-%s" % horizontal_columns[0:2]) + (
109+
"%s" % label_classes))|safe }}
92110
{%- else -%}
93-
{{ field.label(class="form-label")|safe }}
111+
{{ field.label(class="form-label%s" % label_classes)|safe }}
94112
{% endif %}
95113
{% if form_type == 'horizontal' %}
96114
<div class="col-{{ horizontal_columns[0] }}-{{ horizontal_columns[2] }}">
@@ -99,7 +117,7 @@
99117
{% for item in field -%}
100118
<div class="form-check{% if form_type == "inline" %} form-check-inline{% endif %}">
101119
{{ item(class="form-check-input")|safe }}
102-
{{ item.label(class="form-check-label", for=item.id)|safe }}
120+
{{ item.label(class="form-check-label%s" % radio_classes, for=item.id)|safe }}
103121
</div>
104122
{% endfor %}
105123
{#% endcall %#}
@@ -111,7 +129,7 @@
111129
<div class="invalid-feedback d-block">{{ error }}</div>
112130
{%- endfor %}
113131
{%- elif field.description -%}
114-
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
132+
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
115133
{%- endif %}
116134
</div>
117135
{%- elif field.type == 'SubmitField' -%}
@@ -145,7 +163,7 @@
145163
<div class="{{ form_group_classes }}{%- if form_type == "horizontal" %} row{% endif -%}
146164
{%- if field.flags.required %} required{% endif -%}">
147165
{%- if form_type == "inline" %}
148-
{{ field.label(class="visually-hidden")|safe }}
166+
{{ field.label(class="visually-hidden%s" % label_classes)|safe }}
149167
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
150168
{% if field.errors %}
151169
{{ field(class="form-range is-invalid%s" % extra_classes, **kwargs)|safe }}
@@ -166,7 +184,9 @@
166184
{% endif %}
167185
{%- endif %}
168186
{% elif form_type == "horizontal" %}
169-
{{ field.label(class="col-form-label" + (" col-%s-%s" % horizontal_columns[0:2]))|safe }}
187+
{{ field.label(class="col-form-label" + (
188+
" col-%s-%s" % horizontal_columns[0:2]) + (
189+
"%s" % label_classes))|safe }}
170190
<div class="col-{{ horizontal_columns[0] }}-{{ horizontal_columns[2] }}">
171191
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
172192
{% if field.errors %}
@@ -196,11 +216,11 @@
196216
{%- endfor %}
197217
{%- elif field.description -%}
198218
{% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
199-
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
219+
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
200220
{% endcall %}
201221
{%- endif %}
202222
{%- else -%}
203-
{{ field.label(class="form-label")|safe }}
223+
{{ field.label(class="form-label%s" % label_classes)|safe }}
204224
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
205225
{% if field.errors %}
206226
{{ field(class="form-range is-invalid%s" % extra_classes, **kwargs)|safe }}
@@ -225,11 +245,21 @@
225245
<div class="invalid-feedback d-block">{{ error }}</div>
226246
{%- endfor %}
227247
{%- elif field.description -%}
228-
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
248+
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
229249
{%- endif %}
230250
{%- endif %}
231251
</div>
232252
{% endif %}
253+
254+
{%- if label_class -%}
255+
{%- set _ = field.render_kw.update({'label_class': label_class}) -%}
256+
{%- endif -%}
257+
{%- if radio_class -%}
258+
{%- set _ = field.render_kw.update({'radio_class': radio_class}) -%}
259+
{%- endif -%}
260+
{%- if descr_class -%}
261+
{%- set _ = field.render_kw.update({'descr_class': descr_class}) -%}
262+
{%- endif -%}
233263
{% endmacro %}
234264

235265
{# valid form types are "basic", "inline" and "horizontal" #}

tests/conftest.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from flask import Flask, render_template_string
33
from flask_wtf import FlaskForm
4-
from wtforms import BooleanField, PasswordField, StringField, SubmitField, HiddenField
4+
from wtforms import BooleanField, PasswordField, StringField, SubmitField, HiddenField, IntegerField, RadioField
55
from wtforms.validators import DataRequired, Length
66

77

@@ -14,11 +14,31 @@ class HelloForm(FlaskForm):
1414
submit = SubmitField()
1515

1616

17+
class ClassForm(FlaskForm):
18+
boolean = BooleanField('Bool label', description="Bool descr",
19+
render_kw={'label_class': 'text-decoration-underline',
20+
'descr_class': 'text-decoration-line-through'})
21+
integer = IntegerField('Int label', description="Int descr",
22+
render_kw={'label_class': 'text-decoration-underline',
23+
'descr_class': 'text-decoration-line-through'})
24+
option = RadioField('Rad label',
25+
description='Rad descr',
26+
choices=[('one', 'One'), ('two', 'Two')],
27+
render_kw={'label_class': 'text-uppercase',
28+
'radio_class': 'text-decoration-line-through',
29+
'descr_class': 'text-decoration-underline'})
30+
31+
1732
@pytest.fixture
1833
def hello_form():
1934
return HelloForm
2035

2136

37+
@pytest.fixture
38+
def class_form():
39+
return ClassForm
40+
41+
2242
@pytest.fixture(autouse=True)
2343
def app():
2444
app = Flask(__name__)

tests/test_bootstrap5/test_render_form.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,97 @@ def test_config():
136136
data = response.get_data(as_text=True)
137137
assert 'row row-cols-lg-auto g-3 align-items-center' not in data
138138
assert 'custom-inline-classes' in data
139+
140+
141+
# test render label_class, radio_class and descr_class
142+
def test_class(app, client, class_form):
143+
144+
@app.route('/class')
145+
def test_class():
146+
form = class_form()
147+
return render_template_string('''
148+
{% from 'bootstrap5/form.html' import render_form %}
149+
{{ render_form(form) }}
150+
''', form=form)
151+
152+
response = client.get('/class')
153+
data = response.get_data(as_text=True)
154+
155+
# render_field form.html line 88
156+
assert '<label class="form-check-label text-decoration-underline" for="boolean">Bool label</label>' in data
157+
# render_field form.html line 95
158+
assert '<small class="form-text text-body-secondary text-decoration-line-through">Bool descr</small>' in data
159+
160+
# render_field form.html line 223
161+
assert '<label class="form-label text-decoration-underline" for="integer">Int label</label>' in data
162+
# render_field form.html line 248
163+
assert '<small class="form-text text-body-secondary text-decoration-line-through">Int descr</small>' in data
164+
165+
# render_field form.html line 111
166+
assert '<label class="form-label text-uppercase" for="option">Rad label</label>' in data
167+
# render_field form.html line 120
168+
assert '<label class="form-check-label text-decoration-line-through" for="option-1">Two</label>' in data
169+
# render_field form.html line 132
170+
assert '<small class="form-text text-body-secondary text-decoration-underline">Rad descr</small>' in data
171+
172+
173+
def test_class_inline(app, client, class_form):
174+
175+
@app.route('/class_inline')
176+
def test_class_inline():
177+
form = class_form()
178+
return render_template_string('''
179+
{% from 'bootstrap5/form.html' import render_form %}
180+
{{ render_form(form, form_type='inline') }}
181+
''', form=form)
182+
183+
response = client.get('/class_inline')
184+
data = response.get_data(as_text=True)
185+
186+
# render_field form.html line 88, repeat from other test
187+
assert '<label class="form-check-label text-decoration-underline" for="boolean">Bool label</label>' in data
188+
# render_field form.html line 95, repeat from other test
189+
assert '<small class="form-text text-body-secondary text-decoration-line-through">Bool descr</small>' in data
190+
191+
# render_field form.html line 166, probably not displayed
192+
assert '<label class="visually-hidden text-decoration-underline" for="integer">Int label</label>' in data
193+
# render_field form.html not rendered description
194+
assert '">Int descr</small>' not in data
195+
196+
# render_field form.html line 105, probabaly not displayed
197+
assert '<label class="visually-hidden text-uppercase" for="option">Rad label</label>' in data
198+
# render_field form.html line 120, repeat from other test
199+
assert '<label class="form-check-label text-decoration-line-through" for="option-1">Two</label>' in data
200+
# render_field form.html line 132, repeat from other test
201+
assert '<small class="form-text text-body-secondary text-decoration-underline">Rad descr</small>' in data
202+
203+
204+
def test_class_horizontal(app, client, class_form):
205+
206+
@app.route('/class_horizontal')
207+
def test_class_horizontal():
208+
form = class_form()
209+
return render_template_string('''
210+
{% from 'bootstrap5/form.html' import render_form %}
211+
{{ render_form(form, form_type='horizontal') }}
212+
''', form=form)
213+
214+
response = client.get('/class_horizontal')
215+
data = response.get_data(as_text=True)
216+
217+
# render_field form.html line 88, repeat from other test
218+
assert '<label class="form-check-label text-decoration-underline" for="boolean">Bool label</label>' in data
219+
# render_field form.html line 95, repeat from other test
220+
assert '<small class="form-text text-body-secondary text-decoration-line-through">Bool descr</small>' in data
221+
222+
# render_field form.html line 189
223+
assert '<label class="col-form-label col-lg-2 text-decoration-underline" for="integer">Int label</label>' in data
224+
# render_field form.html line 219
225+
assert '<small class="form-text text-body-secondary text-decoration-line-through">Int descr</small>' in data
226+
227+
# render_field form.html line 109
228+
assert '<label class="col-form-label col-lg-2 text-uppercase" for="option">Rad label</label>' in data
229+
# render_field form.html line 120, repeat from other test
230+
assert '<label class="form-check-label text-decoration-line-through" for="option-1">Two</label>' in data
231+
# render_field form.html line 132, repeat from other test
232+
assert '<small class="form-text text-body-secondary text-decoration-underline">Rad descr</small>' in data

0 commit comments

Comments
 (0)