Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rendering labels and description with classes #349

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ Bootstrap-Flask, simply use the built-in class ``SwitchField()`` instead of
``BooleanField()``. See also the example application.


.. _inputcustomization:

Form Input Customization
------------------------

Rendering Label
~~~~~~~~~~~~~~~

Bootstrap offers control for rendering
`text <https://getbootstrap.com/docs/5.3/utilities/text/>`_. This is supported
for inputs of a form by adding ``render_kw={'label_class': '... ...'}`` to the
field constructor. In order to control the rendering of the label of a field,
use ``render_kw={'label_class': '... ...'}``. See also the example application.

Rendering Radio Label
~~~~~~~~~~~~~~~~~~~~~

Similar support exists for the rendering of the labels of options of a
``RadioField()` with ``render_kw={'radio_class': '... ...'}``. See also the
example application.

Rendering Description
~~~~~~~~~~~~~~~~~~~~~

Use ``render_kw={'descr_class': '... ...'}`` for controlling the rendering of a
field's description. See also the example application.


.. _bootswatch_theme:

Bootswatch Themes
Expand Down
2 changes: 1 addition & 1 deletion docs/macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ API
form group classes, it will read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first
(the default value is ``mb-3``).

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


render_form()
Expand Down
36 changes: 27 additions & 9 deletions examples/bootstrap5/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@

class ExampleForm(FlaskForm):
"""An example form that contains all the supported bootstrap style form fields."""
date = DateField(description="We'll never share your email with anyone else.") # add help text with `description`
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder'}) # add HTML attribute with `render_kw`
date = DateField(description='Your memorable date') # add help text with `description`
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder', 'class': 'text-decoration-line-through'}) # add HTML attribute with `render_kw`
datetime_local = DateTimeLocalField()
time = TimeField()
time = TimeField(description='This is private', render_kw={'descr_class': 'fst-italic text-decoration-underline'})
month = MonthField()
color = ColorField()
floating = FloatField()
Expand All @@ -45,27 +45,43 @@ class ExampleForm(FlaskForm):
url = URLField()
telephone = TelField()
image = FileField(render_kw={'class': 'my-class'}, validators=[Regexp('.+\.jpg$')]) # add your class
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline', 'radio_class': 'text-decoration-line-through'})
select = SelectField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
select_multiple = SelectMultipleField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
bio = TextAreaField()
search = SearchField() # will autocapitalize on mobile
title = StringField() # will not autocapitalize on mobile
search = SearchField() # will autocapitalize on mobile
title = StringField() # will not autocapitalize on mobile
secret = PasswordField()
remember = BooleanField('Remember me')
submit = SubmitField()


class ExampleFormInline(FlaskForm):
"""An example inline form."""
floating = FloatField(description='a float', render_kw={'label_class': 'text-decoration-underline'})
integer = IntegerField(description='an int', render_kw={'descr_class': 'text-decoration-line-through'})
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'})
submit = SubmitField()


class ExampleFormHorizontal(FlaskForm):
"""An example horizontal form."""
floating = FloatField(description='a float', render_kw={'label_class': 'text-decoration-underline'})
integer = IntegerField(description='an int', render_kw={'descr_class': 'text-decoration-line-through'})
option = RadioField(description='choose 1', choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline'})
submit = SubmitField()


class HelloForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
password = PasswordField('Password', validators=[DataRequired(), Length(8, 150)])
remember = BooleanField('Remember me')
remember = BooleanField('Remember me', description='Rember me on my next visit', render_kw={'descr_class': 'fw-bold text-decoration-line-through'})
submit = SubmitField()


class ButtonForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(1, 20)])
confirm = SwitchField('Confirmation')
confirm = SwitchField('Confirmation', description='Are you sure?', render_kw={'label_class': 'font-monospace text-decoration-underline'})
submit = SubmitField()
delete = SubmitField()
cancel = SubmitField()
Expand Down Expand Up @@ -190,7 +206,9 @@ def test_form():
contact_form=ContactForm(),
im_form=IMForm(),
button_form=ButtonForm(),
example_form=ExampleForm()
example_form=ExampleForm(),
inline_form=ExampleFormInline(),
horizontal_form=ExampleFormHorizontal()
)


Expand Down
20 changes: 14 additions & 6 deletions examples/bootstrap5/templates/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<h2>Example Form</h2>
<pre>
class ExampleForm(FlaskForm):
date = DateField(description="We'll never share your email with anyone else.") # add help text with `description`
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder'}) # add HTML attribute with `render_kw`
date = DateField(description='Your memorable date') # add help text with `description`
datetime = DateTimeField(render_kw={'placeholder': 'this is a placeholder', 'class': 'fst-italic'}) # add HTML attribute with `render_kw`
datetimelocal = DateTimeLocalField()
time = TimeField()
time = TimeField(description='This is private', render_kw={'descr_class': 'fs-1 text-decoration-underline'})
month = MonthField()
color = ColorField()
floating = FloatField()
Expand All @@ -20,17 +20,25 @@ <h2>Example Form</h2>
url = URLField()
telephone = TelField()
image = FileField(render_kw={'class': 'my-class'}, validators=[Regexp('.+\.jpg$')]) # add your class
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
option = RadioField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')], render_kw={'label_class': 'text-decoration-underline', 'radio_class': 'text-decoration-line-through'})
select = SelectField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
selectmulti = SelectMultipleField(choices=[('dog', 'Dog'), ('cat', 'Cat'), ('bird', 'Bird'), ('alien', 'Alien')])
bio = TextAreaField()
search = SearchField() # will autocapitalize on mobile
title = StringField() # will not autocapitalize on mobile
search = SearchField() # will autocapitalize on mobile
title = StringField() # will not autocapitalize on mobile
secret = PasswordField()
remember = BooleanField('Remember me')
submit = SubmitField()</pre>
{{ render_form(example_form) }}

<h2>Inline form</h2>
<pre>{% raw %}{{ render_form(inline_form, form_type='inline') }}{% endraw %}</pre>
{{ render_form(inline_form, form_type='inline') }}

<h2>Horizontal form</h2>
<pre>{% raw %}{{ render_form(horizontal_form, form_type='horizontal') }}{% endraw %}</pre>
{{ render_form(horizontal_form, form_type='horizontal') }}

<h2>Render a form with render_form</h2>
<pre>{% raw %}{{ render_form(form) }}{% endraw %}</pre>
{{ render_form(form) }}
Expand Down
54 changes: 42 additions & 12 deletions flask_bootstrap/templates/bootstrap5/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@

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

{# 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 #}
{%- set label_class = '' -%}
{%- if field.render_kw.label_class -%}
{% set label_class = field.render_kw.pop('label_class', '') -%}
{% set label_classes = ' ' + label_class -%}
{%- endif -%}
{%- set radio_class = '' -%}
{%- if field.render_kw.radio_class -%}
{% set radio_class = field.render_kw.pop('radio_class', '') -%}
{% set radio_classes = ' ' + radio_class -%}
{%- endif -%}
{%- set descr_class = '' -%}
{%- if field.render_kw.descr_class -%}
{% set descr_class = field.render_kw.pop('descr_class', '') -%}
{% set descr_classes = ' ' + descr_class -%}
{%- endif -%}

{# combine render_kw class or class/class_ argument with Bootstrap classes #}
{% set render_kw_class = ' ' + field.render_kw.class if field.render_kw.class else '' %}
{% set class = kwargs.pop('class', '') or kwargs.pop('class_', '') %}
Expand All @@ -68,14 +85,14 @@
{%- else -%}
{{ field(class="form-check-input%s" % extra_classes, **field_kwargs)|safe }}
{%- endif %}
{{ field.label(class="form-check-label", for=field.id)|safe }}
{{ field.label(class="form-check-label%s" % label_classes, for=field.id)|safe }}
{%- if field.errors %}
{%- for error in field.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{%- endfor %}
{%- elif field.description -%}
{% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
{% endcall %}
{%- endif %}
</div>
Expand All @@ -85,12 +102,13 @@
this is just a hack for now, until I can think of something better #}
<div class="{{ form_group_classes }} {% if form_type == 'horizontal' %}row{% endif %}{% if required %} required{% endif %}">
{%- if form_type == "inline" %}
{{ field.label(class="visually-hidden")|safe }}
{{ field.label(class="visually-hidden%s" % label_classes)|safe }}
{% elif form_type == "horizontal" %}
{{ field.label(class="col-form-label" + (
" col-%s-%s" % horizontal_columns[0:2]))|safe }}
" col-%s-%s" % horizontal_columns[0:2]) + (
"%s" % label_classes))|safe }}
{%- else -%}
{{ field.label(class="form-label")|safe }}
{{ field.label(class="form-label%s" % label_classes)|safe }}
{% endif %}
{% if form_type == 'horizontal' %}
<div class="col-{{ horizontal_columns[0] }}-{{ horizontal_columns[2] }}">
Expand All @@ -99,7 +117,7 @@
{% for item in field -%}
<div class="form-check{% if form_type == "inline" %} form-check-inline{% endif %}">
{{ item(class="form-check-input")|safe }}
{{ item.label(class="form-check-label", for=item.id)|safe }}
{{ item.label(class="form-check-label%s" % radio_classes, for=item.id)|safe }}
</div>
{% endfor %}
{#% endcall %#}
Expand All @@ -111,7 +129,7 @@
<div class="invalid-feedback d-block">{{ error }}</div>
{%- endfor %}
{%- elif field.description -%}
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
{%- endif %}
</div>
{%- elif field.type == 'SubmitField' -%}
Expand Down Expand Up @@ -145,7 +163,7 @@
<div class="{{ form_group_classes }}{%- if form_type == "horizontal" %} row{% endif -%}
{%- if field.flags.required %} required{% endif -%}">
{%- if form_type == "inline" %}
{{ field.label(class="visually-hidden")|safe }}
{{ field.label(class="visually-hidden%s" % label_classes)|safe }}
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
{% if field.errors %}
{{ field(class="form-range is-invalid%s" % extra_classes, **kwargs)|safe }}
Expand All @@ -166,7 +184,9 @@
{% endif %}
{%- endif %}
{% elif form_type == "horizontal" %}
{{ field.label(class="col-form-label" + (" col-%s-%s" % horizontal_columns[0:2]))|safe }}
{{ field.label(class="col-form-label" + (
" col-%s-%s" % horizontal_columns[0:2]) + (
"%s" % label_classes))|safe }}
<div class="col-{{ horizontal_columns[0] }}-{{ horizontal_columns[2] }}">
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
{% if field.errors %}
Expand Down Expand Up @@ -196,11 +216,11 @@
{%- endfor %}
{%- elif field.description -%}
{% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
{% endcall %}
{%- endif %}
{%- else -%}
{{ field.label(class="form-label")|safe }}
{{ field.label(class="form-label%s" % label_classes)|safe }}
{%- if field.type in ['DecimalRangeField', 'IntegerRangeField'] %}
{% if field.errors %}
{{ field(class="form-range is-invalid%s" % extra_classes, **kwargs)|safe }}
Expand All @@ -225,11 +245,21 @@
<div class="invalid-feedback d-block">{{ error }}</div>
{%- endfor %}
{%- elif field.description -%}
<small class="form-text text-body-secondary">{{ field.description|safe }}</small>
<small class="form-text text-body-secondary{{ descr_classes|safe }}">{{ field.description|safe }}</small>
{%- endif %}
{%- endif %}
</div>
{% endif %}

{%- if label_class -%}
{%- set _ = field.render_kw.update({'label_class': label_class}) -%}
{%- endif -%}
{%- if radio_class -%}
{%- set _ = field.render_kw.update({'radio_class': radio_class}) -%}
{%- endif -%}
{%- if descr_class -%}
{%- set _ = field.render_kw.update({'descr_class': descr_class}) -%}
{%- endif -%}
{% endmacro %}

{# valid form types are "basic", "inline" and "horizontal" #}
Expand Down
22 changes: 21 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, StringField, SubmitField, HiddenField
from wtforms import BooleanField, PasswordField, StringField, SubmitField, HiddenField, IntegerField, RadioField
from wtforms.validators import DataRequired, Length


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


class ClassForm(FlaskForm):
boolean = BooleanField('Bool label', description="Bool descr",
render_kw={'label_class': 'text-decoration-underline',
'descr_class': 'text-decoration-line-through'})
integer = IntegerField('Int label', description="Int descr",
render_kw={'label_class': 'text-decoration-underline',
'descr_class': 'text-decoration-line-through'})
option = RadioField('Rad label',
description='Rad descr',
choices=[('one', 'One'), ('two', 'Two')],
render_kw={'label_class': 'text-uppercase',
'radio_class': 'text-decoration-line-through',
'descr_class': 'text-decoration-underline'})


@pytest.fixture
def hello_form():
return HelloForm


@pytest.fixture
def class_form():
return ClassForm


@pytest.fixture(autouse=True)
def app():
app = Flask(__name__)
Expand Down
Loading
Loading