Home > Python, Web > Render Bootstrap 3 forms with WTForms and Jinja

Render Bootstrap 3 forms with WTForms and Jinja

November 10th, 2013 Leave a comment Go to comments

We all know amazing libraries Jinja and WTForms which simplify our life in rendering web pages for user. By default, they don’t provide any advanced formatting or styling. One of the ways to solve this is to use Bootstrap library. This post will show how to integrate this three libraries all together using forms as example.

NOTE: Code for this post is embed at the end of the post or you can jump directly to gist in the new tab (recommended): https://gist.github.com/bearz/7394681

HTML Forms is the main mechanism of user interaction with website. Even now, on modern websites people fill forms – changed only the way how forms are submitted. Lifecycle of form is straightforward: form rendered by web server, user fills form and submits it to server, server validates the form and do action that it supposes to do if form is correct. It is a good idea to consolidate code of rendering, processing and validation into some common component and this is what actually WTForms library is doing. It is very well designed and easy extendable library (which we will see on the bootstrap example below) and you should definitely consider using it in your projects. Here is how basic form looks:

class LoginForm(Form):
    email = fields.TextField(u'Email', validators=[validators.required()])
    password = fields.PasswordField(u'Password', validators=[validators.required()])
    remember_me = fields.BooleanField(u'Remember Me')

Pretty easy, isn’t it? This code allow you to render form to client, validate that fields were sent (there are other validators available and you can write your own). Library is very declarative and removes a lot of boilerplate code that otherwise you need to write yourself.

In order to create HTML for your page you need just to call form in your template and it will render html according to standard widget assigned to this field. Obviously, wtforms by default will generate the most simple bullet-proof html. If we want to have fancy bootstrap formatting we need to change somehow output of wtforms render. There are two ways to do this: write custom widget or call fields separately (or in loop) in your template using ability to specify parameters for field renderer. There are pros and cons for both approaches. I selected second since I want to keep presentation details away from code logic. I don’t want to find myself in situation when form was changed and all my presentation appear to be broken. Users won’t be happy to see broken page.

Render engine of WTForms will render basic html tags like

<input type="text"/>

and you have ability to specify kwargs for fields and they will be added as a html element attributes. Here is the doc for this. I found myself convenient to write strings like this in template and control every argument on presentation level:

{{ macros.render_field(form.email, label_visible=false, placeholder='Email', 
type='email') }}

To achieve this I wrote a simple Jinja2 macros

{% macro render_field(field, label_visible=true) -%}
     <div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
        {% if field.type != 'HiddenField' and label_visible %}
            <label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
        {% endif %}
        {{ field(class_='form-control', **kwargs) }}
        {% if field.errors %}
            {% for e in field.errors %}
                <p class="help-block">{{ e }}</p>
            {% endfor %}
        {% endif %}
    </div>
{%- endmacro %}

Code here is pretty straightforward – it just wrap element into bootstrap structure and handling errors presentation if needed. Also, there is additional parameter for showing/hiding labels. In gist you can find additional functions for rendering checkboxes and radio fields and have a good understanding how to build additional field render components.

Now, it is a time to remove boilerplate of wrapping fields into form. This is done by following macros.

{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%}
 
    <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}">
        {{ form.hidden_tag() if form.hidden_tag }}
        {% if caller %}
            {{ caller() }}
        {% else %}
            {% for f in form %}
                {% if f.type == 'BooleanField' %}
                    {{ render_checkbox_field(f) }}
                {% elif f.type == 'RadioField' %}
                    {{ render_radio_field(f) }}
                {% else %}
                    {{ render_field(f) }}
                {% endif %}
            {% endfor %}
        {% endif %}
        <button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
    </form>
{%- endmacro %}

This macros adds submit button and form wrapper. It can be a good idea to add more elements to macros, but I put only this for simplicity. There are two ways of using this function: call macros to render form with all fields (good if you don’t have any special parameters for this form, it will just go through all fields of form and render them) or call macros using caller jinja function. In latter option you will be able to replace whole body of form with fields you want to draw and specify their order. For rendering fields you can use render_field macros I’ve described before. Both examples of using form_render macros attached to gist, as well as described in macros comments.

Hope this will help somebody and I will be happy to answer any question you have.

Gist with the code:

{% extends 'base.html' %}
{% import 'macros.html' as macros %}
{% block content %}
    <div class="row">
        <div class="col-xs-12 col-md-3 col-sm-4 col-sm-offset-4 col-md-offset-4 col-lg-3 col-lg-offset-4">
            <div class="login-message">
                Login to AwesomeService!
            </div>
            {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
                                        class_='login-form') %}
                {{ macros.render_field(form.email, label_visible=false, placeholder='Email', type='email') }}
                {{ macros.render_field(form.password, label_visible=false, placeholder='Password', type='password') }}
                {{ macros.render_checkbox_field(form.remember_me) }}
            {% endcall %}
        </div>
    </div>
{% endblock content %}
{# Renders field for bootstrap 3 standards.

    Params:
        field - WTForm field
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
#}
{% macro render_field(field, label_visible=true) -%}

    <div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
        {% if (field.type != 'HiddenField' and field.type !='CSRFTokenField') and label_visible %}
            <label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
        {% endif %}
        {{ field(class_='form-control', **kwargs) }}
        {% if field.errors %}
            {% for e in field.errors %}
                <p class="help-block">{{ e }}</p>
            {% endfor %}
        {% endif %}
    </div>
{%- endmacro %}

{# Renders checkbox fields since they are represented differently in bootstrap
    Params:
        field - WTForm field (there are no check, but you should put here only BooleanField.
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_checkbox_field(form.remember_me) }}
 #}
{% macro render_checkbox_field(field) -%}
    <div class="checkbox">
        <label>
            {{ field(type='checkbox', **kwargs) }} {{ field.label }}
        </label>
    </div>
{%- endmacro %}

{# Renders radio field
    Params:
        field - WTForm field (there are no check, but you should put here only BooleanField.
        kwargs - pass any arguments you want in order to put them into the html attributes.
        There are few exceptions: for - for_, class - class_, class__ - class_

    Example usage:
        {{ macros.render_radio_field(form.answers) }}
 #}
{% macro render_radio_field(field) -%}
    {% for value, label, _ in field.iter_choices() %}
        <div class="radio">
            <label>
                <input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }}
            </label>
        </div>
    {% endfor %}
{%- endmacro %}

{# Renders WTForm in bootstrap way. There are two ways to call function:
     - as macros: it will render all field forms using cycle to iterate over them
     - as call: it will insert form fields as you specify:
     e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
                                        class_='login-form') %}
                {{ macros.render_field(form.email, placeholder='Input email', type='email') }}
                {{ macros.render_field(form.password, placeholder='Input password', type='password') }}
                {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
            {% endcall %}

     Params:
        form - WTForm class
        action_url - url where to submit this form
        action_text - text of submit button
        class_ - sets a class for form
    #}
{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%}

    <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}">
        {{ form.hidden_tag() if form.hidden_tag }}
        {% if caller %}
            {{ caller() }}
        {% else %}
            {% for f in form %}
                {% if f.type == 'BooleanField' %}
                    {{ render_checkbox_field(f) }}
                {% elif f.type == 'RadioField' %}
                    {{ render_radio_field(f) }}
                {% else %}
                    {{ render_field(f) }}
                {% endif %}
            {% endfor %}
        {% endif %}
        <button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
    </form>
{%- endmacro %}
{% extends 'base.html' %}
{% from 'macros.html' import render_form %}
{% block content %}
    <div class="your-form">
        {{ render_form(your_form, action_url='/submit_url', action_text='Submit Form') }}
    </div>
{% endblock content %}

Categories: Python, Web Tags: , ,
  1. Christopher Lee
    May 22nd, 2014 at 07:18 | #1

    Thanks for the macro snippets; they were a great starting point.

    A few minor bugs: You seem to have missed handling the default values in radio buttons, and closing the input tag in a compliant way:

    {% for value, text, checked in field.iter_choices() %}

    {{ text }}

  2. Christopher Lee
    May 22nd, 2014 at 07:20 | #2

    Looks like your comments don’t escape HTML. The {{ text }} line above should be:

    <input type=”radio” … {% if checked %}checked=”true”{% endif %} />{{ text }}

  3. bear-z
    May 22nd, 2014 at 15:18 | #3

    @Christopher Lee
    Good addition, thanks. It will be “field.checked”, not just checked. I’ll take a look on WTForms documentation to get exact property that put default value for radiobox and fix the code.

  4. November 27th, 2014 at 12:00 | #4

    Great snippet! I have just one question – would it make sense to wrap the subit button in the render_field() as well?

  5. dP
    December 6th, 2014 at 21:25 | #5

    field.type != ‘HiddenField’ or field.type !=’CSRFTokenField’

    Should be “and” there

  6. Matt
    January 13th, 2015 at 22:00 | #6

    Small bug: Line 14 should be this:

    {% if (field.type != ‘HiddenField’ and field.type !=’CSRFTokenField’) and label_visible %}

    The or causes it to display the csrf token label

  7. January 20th, 2015 at 16:34 | #7

    Is there a reason you don’t allow for the form to have an id?

  8. Thomas
    February 7th, 2015 at 19:51 | #8

    Thanks for this. How do you handle textarea fields?

  9. Robin Ramael
    March 26th, 2015 at 12:03 | #9

    Hi,
    thanks for the macro, it’s very useful, but I think you have a small bug when (not) rendering the labels hidden fields or csrftokenfields:

    in render_field,
    {% if (field.type != ‘HiddenField’ or field.type !=’CSRFTokenField’) and label_visible %}
    should be
    {% if field.type !=’CSRFTokenField’ and field.type != ‘HiddenField’ and label_visible %}

    otherwise, the hidden or csrftokenfield label will still be rendered.

  10. bear-z
    March 26th, 2015 at 16:48 | #10

    Thanks guys for the feedback, I’ve fixed it.

  11. Marin
    March 31st, 2015 at 20:27 | #11

    Hi,

    A small fix:
    {% if (field.type != ‘HiddenField’ or field.type !=’CSRFTokenField’) and label_visible %}
    should be
    {% if (field.widget.input_type != ‘hidden’) and label_visible %}

    In this way the following definition will render correctly:
    id = IntegerField(‘id’, widget=HiddenInput())

  12. bear-z
    March 31st, 2015 at 21:21 | #12

    Hi, Marin!

    Can you send me a link to the doc where all the input_type listed? I checked the WTForms official doc on Field and don’t see this param.

    Thanks for suggestion,
    Sergey

  13. Marin
    April 1st, 2015 at 09:03 | #13

    @bear-z
    Hi,

    It occurred to me that my solution might not work for every case. All fields can have a widget (it defaults to None on the Filed base class), and that widget can have a filed named input_type as this is the case for e.g. TextInput, HiddenInput, CheckboxInput but not for TextArea. Maybe some additional logic is required for this cases:
    if getattr(form.widget, ‘input_type’, None) != ‘hidden’

    Don’t read the docs, read the code :)

    Regards,
    Marin

  14. bear-z
    April 1st, 2015 at 16:53 | #14

    It is usually safer to relate on the documented fields since they are more likely to be supported for a longer term.

    I’ll try to take a deeper look over the weekend since it seems that your suggestion has a wider application then mine.

    Thanks

  1. No trackbacks yet.