Skip to content

Commit a6d0cb9

Browse files
PanderMusubigreyli
andauthored
rendering labels and description with classes (#349)
* rendering labels and description with classes * refactored descr_class to description_class --------- Co-authored-by: Grey Li <[email protected]>
1 parent 2c1f431 commit a6d0cb9

File tree

7 files changed

+223
-25
lines changed

7 files changed

+223
-25
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={'description_class': '... ...'}`` for controlling the
168+
rendering of a 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
@@ -133,7 +133,7 @@ API
133133
form group classes, it will read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first
134134
(the default value is ``mb-3``).
135135

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

138138

139139
render_form()

examples/bootstrap5/app.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ExampleForm(FlaskForm):
3838
date = DateField(description="We'll never share your email with anyone else.") # add help text with `description`
3939
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder'}) # add HTML attribute with `render_kw`
4040
datetime_local = DateTimeLocalField()
41-
time = TimeField()
41+
time = TimeField(description='This is private', render_kw={'description_class': 'fst-italic text-decoration-underline'})
4242
month = MonthField()
4343
color = ColorField()
4444
floating = FloatField()
@@ -49,7 +49,7 @@ class ExampleForm(FlaskForm):
4949
url = URLField()
5050
telephone = TelField()
5151
image = FileField(render_kw={'class': 'my-class'}, validators=[Regexp('.+\.jpg$')]) # add your class
52-
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
52+
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline', 'radio_class': 'text-decoration-line-through'})
5353
select = SelectField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
5454
select_multiple = SelectMultipleField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
5555
bio = TextAreaField()
@@ -60,16 +60,32 @@ class ExampleForm(FlaskForm):
6060
submit = SubmitField()
6161

6262

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

6985

7086
class ButtonForm(FlaskForm):
7187
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
72-
confirm = SwitchField('Confirmation')
88+
confirm = SwitchField('Confirmation', description='Are you sure?', render_kw={'label_class': 'font-monospace text-decoration-underline'})
7389
submit = SubmitField()
7490
delete = SubmitField()
7591
cancel = SubmitField()
@@ -196,7 +212,9 @@ def test_form():
196212
contact_form=ContactForm(),
197213
im_form=IMForm(),
198214
button_form=ButtonForm(),
199-
example_form=ExampleForm()
215+
example_form=ExampleForm(),
216+
inline_form=ExampleFormInline(),
217+
horizontal_form=ExampleFormHorizontal()
200218
)
201219

202220

examples/bootstrap5/templates/form.html

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
<h2>Example Form</h2>
77
<pre>
88
class ExampleForm(FlaskForm):
9-
date = DateField(description="We'll never share your email with anyone else.") # add help text with `description`
10-
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder'}) # add HTML attribute with `render_kw`
9+
date = DateField(description='Your memorable date') # add help text with `description`
10+
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder', 'class': 'fst-italic'}) # add HTML attribute with `render_kw`
1111
datetimelocal = DateTimeLocalField()
12-
time = TimeField()
12+
time = TimeField(description='This is private', render_kw={'description_class': 'fs-1 text-decoration-underline'})
1313
month = MonthField()
1414
color = ColorField()
1515
floating = FloatField()
@@ -20,17 +20,25 @@ <h2>Example Form</h2>
2020
url = URLField()
2121
telephone = TelField()
2222
image = FileField(render_kw={'class': 'my-class'}, validators=[Regexp('.+\.jpg$')]) # add your class
23-
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
23+
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline', 'radio_class': 'text-decoration-line-through'})
2424
select = SelectField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
2525
selectmulti = SelectMultipleField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
2626
bio = TextAreaField()
27-
search = SearchField() # will autocapitalize on mobile
28-
title = StringField() # will not autocapitalize on mobile
27+
search = SearchField() # will autocapitalize on mobile
28+
title = StringField() # will not autocapitalize on mobile
2929
secret = PasswordField()
3030
remember = BooleanField('Remember me')
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, radio_class and description_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 description_class = '' -%}
60+
{%- if field.render_kw.description_class -%}
61+
{% set description_class = field.render_kw.pop('description_class', '') -%}
62+
{% set description_classes = ' ' + description_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{{ description_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{{ description_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{{ description_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{{ description_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 description_class -%}
261+
{%- set _ = field.render_kw.update({'description_class': description_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+
'description_class': 'text-decoration-line-through'})
21+
integer = IntegerField('Int label', description="Int descr",
22+
render_kw={'label_class': 'text-decoration-underline',
23+
'description_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+
'description_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__)

0 commit comments

Comments
 (0)