# Render a WTForms form with Jinja and HTML

# Create a new Flask route for adding movies

We want to allow users to create new movies using our form, so the first thing to do is create a Flask route that will display our form, so our users can access it.

In routes.py, let's import MovieForm:

from movie_library.forms import MovieForm

And create our route:

@pages.route("/add", methods=["GET", "POST"])
def add_movie():
    form = MovieForm()

    if request.method == "POST":
        pass

    return render_template(
        "new_movie.html", title="Movies Watchlist - Add Movie", form=form
    )

Note that I've made it be able to receive GET and POST requests. The template will be rendered in response to a GET request, and the form data will be received and validated in response to a POST request.

For now, the POST request does nothing. Let's render our form first, and handle receiving the data in the next lecture.

# How to render a WTForms field

First things first, let's create our templates/new_movie.html file. This is where our form will live:

{% extends "layout.html" %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            
        </div>
    </form>
{% endblock %}

Here I've added a form element, which contains div. The inner div will be used purely for styling.

Let's render our first field:

{% extends "layout.html" %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            <div class="form__group">
                {{ field.label(class_="form__label") }}
                {{ field(class_="form__field")}}
            
                {%- for error in field.errors %}
                    <span class="form__error">{{ error }}</span>
                {% endfor %}
            </div>
        </div>
    </form>
{% endblock %}

This is what we've added:

<div class="form__group">
    {{ field.label(class_="form__label") }}
    {{ field(class_="form__field")}}

    {%- for error in field.errors %}
        <span class="form__error">{{ error }}</span>
    {% endfor %}
</div>

A field consists of three parts:

  • The label, whose HTML is generated by WTForms when we do {{ field.label() }}
  • The field input itself, generated by {{ field() }}
  • The errors, which are stored in field.errors. We iterate over them and display a span element for each validation error (if any). If there are no errors, then this won't show.

You can pass CSS classes to the label and field with the class_ keyword argument.

# Extract the rendering of a field into a macro

We have three fields in our form: title, director, and year. All three fields will be rendered in exactly the same way: with field.label(), field(), and a list of errors.

Therefore this is a perfect candidate for a Jinja macro!

Let's create templates/macros/fields.html and place this inside it:

{% macro render_text_field(field) %}
<div class="form__group">
    {{ field.label(class_="form__label") }}
    {{ field(class_="form__field")}}

    {%- for error in field.errors %}
        <span class="form__error">{{ error }}</span>
    {% endfor %}
</div>
{% endmacro %}

Now we can just import that from templates/new_movie.html and use it three times, once per field:

{% from "macros/fields.html" import render_text_field %}

{% extends "layout.html" %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            {{ render_text_field(form.title) }}
            {{ render_text_field(form.director) }}
            {{ render_text_field(form.year) }}
        </div>
    </form>
{% endblock %}

# Render all the form fields (including CSRF field)

There are two more fields we need to include in our form: the CSRF protection field, and the submit button.

Fortunately with Flask-WTF, including CSRF protection is super easy. Every form class already has a field defined, which we just need to render:








 







{% from "macros/fields.html" import render_text_field %}

{% extends "layout.html" %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            {{ form.hidden_tag() }}
            {{ render_text_field(form.title) }}
            {{ render_text_field(form.director) }}
            {{ render_text_field(form.year) }}
        </div>
    </form>
{% endblock %}

The submit button is a label-less field, so we will render it just as we have done the input fields: using {{ form.submit() }}. Remember we can pass a class to it:













 
 
 




{% from "macros/fields.html" import render_text_field %}

{% extends "layout.html" %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            {{ form.hidden_tag() }}
            {{ render_text_field(form.title) }}
            {{ render_text_field(form.director) }}
            {{ render_text_field(form.year) }}
    
            <div>
                {{ form.submit(class_="button button--form") }}
            </div>
        </div>
    </form>
{% endblock %}

# Style the form using CSS

The last bit to do is our form styling! To do that, let's create a CSS file in movie_library/static/css/forms.css, and link it to our template:





 
 
 
















{% from "macros/fields.html" import render_text_field %}

{% extends "layout.html" %}

{%- block head_content %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/forms.css') }}" />
{% endblock %}

{% block main_content %}
    <form name="add_movie" method="post" novalidate class="form">
        <div class="form__container">
            {{ form.hidden_tag() }}
            {{ render_text_field(form.title) }}
            {{ render_text_field(form.director) }}
            {{ render_text_field(form.year) }}
    
            <div>
                {{ form.submit(class_="button button--form") }}
            </div>
        </div>
    </form>
{% endblock %}

There's quite a bit of styling to add to make it look like the design!

Let's work on the form container first. We want to limit its width, center it on the page, and add some border:

.form {
  margin: 0 auto;
  max-width: 30rem;
  border: var(--border);
  font-size: 1.2rem;

  /* An explicit background is required here, as it's actually transparent by default, and we
       don't want to see the shadow element behind */
  background: var(--background-color);
}

Adding the sharp shadow is a bit trickier! We need to play with the box-shadow property a bit to make it to what we want:

.form {
  margin: 0 auto;
  max-width: 30rem;
  border: var(--border);
  font-size: 1.2rem;

  /* An explicit background is required here, as it's actually transparent by default, and we
       don't want to see the shadow element behind */
  background: var(--background-color);
  
  /* This shadow is separated from the edges by 0.75rem, and shrunk by 0.2rem on the
  top and bottom. */
  box-shadow: 0.75rem 0.75rem 0 -0.2rem var(--accent-colour);
}

Then, let's add padding into our form__container so that there's room around the fields up to the border:

.form__container {
  padding: 2.5rem 1.5rem 1.5rem 1.5rem;
}

/* The following media queries allow for more padding inside the form as the window
   size increases */
@media screen and (min-width: 24.75em) {
  .form__container {
    padding-left: 2rem;
  }
}

@media screen and (min-width: 30em) {
  .form__container {
    padding-left: 2.5rem;
  }
}

Next up, let's start styling the .form__group and .form__label classes. By displaying the group using flex, we can make it a column so the label will appear on top of the field, and the errors below the field:

.form__group {
  /* Surrounds the label and input fields, placing the label above the input */
  display: flex;
  flex-direction: column;
  margin-bottom: 1.5rem;
}

.form__label {
  margin-bottom: 0.5rem;
}

We've got some styling to do in the fields also! Nothing much new here:

.form__field {
  /* Removes the border and the outline highlight when the text field is in focus */
  outline: none;
  border: none;

  /* Re-adds a bottom border which will be replaces when the field is in focus. This
       is going to prevent any jumping when we add the border later */
  border-bottom: 3px solid #fff;

  /* We have to be explicit about our text fields inheriting font properties */
  font-size: inherit;
  font-family: inherit;

  padding: 0.75rem 0.5rem;
  background: var(--background-color);
}

/* When the field is in focus, we change the border colour at the bottom to the accent colour */
.form__field:focus {
  border-bottom: var(--border);
}

I will also add a few other pieces of styling that we will use in other forms in the application. To make life a bit easier for you, I'll add them now:

.form__small {
  font-size: 0.83rem;
  color: var(--text-muted);
}

.form__link {
  text-decoration: none;
  color: var(--accent-colour);
}

.form__link:hover {
  color: #d05656;
}

.form__error {
  margin-top: 0.5rem;
}

.form__error,
.form__flash {
  display: block;
  padding: 0.5rem;
  color: var(--text);
}

.form__error,
.form__flash--danger {
  background: var(--accent-colour);
}

.form__flash {
  margin: 0.5rem;
}

.form__flash--success {
  background: var(--accent-colour-2);
}

Most of these are to do with flashed messages that we will be using in our authentication forms.

Finally, we need to add button styling:

/* Styles specific to the form buttons */
.button--form {
  margin: 2rem 0 0 auto;
  padding: 0.75rem 3rem;
  border: none;
  background: var(--background-color);
}

.button--form:hover {
  background: var(--background-color-hover);
}

And with that, we're done! Quite a lot of CSS, but not many new properties that you haven't seen before.

As usual, I recommend you code along with me and play around with the CSS properties we're using. Make the design your own by changing certain parts, and truly understand what each property is doing!