Validating Ajax Requests With WTForms in Flask
posted by Carson Evans · Aug 20, 2019
The Traditional Way
First let's start with the traditional way of doing things, where a form submit will cause a page reload.
The Form
We're going to define a username which must be at least 3 but no longer than 15 characters long, and an age which must be at least 14.
class Form(FlaskForm):
username = StringField('Username', [
Length(min=3, max=15, message='Username must be at least %(min)s but no more than %(max)s characters')
])
age = IntegerField('Age', [
NumberRange(min=14, message='You must be %(min)s or older to sign up')
])
The Endpoint
This is the pattern I like to use with wtforms and traditional submits. The
form.validate_on_submit
function will evaluate true if the request method
was POST
and validation passed. If the request method was POST
but did not
pass validation, the page will re-render, and the form instance will contain
error messages.
@app.route('/traditional', methods=['GET', 'POST'])
def traditional():
form = Form()
if form.validate_on_submit():
return 'WOOT WOOT!'
return render_template('traditional.html', form=form)
The Template
Now let's render the form fields, with their labels. We're using bootstrap
styles for demonstration purposes. We add the is-invalid
class to the inputs
if there is an error message for the input, which automatically renders the
inputs in an error state for us.
<form method="POST">
{{ form.csrf_token }}
<div class="form-group">
{% set class = 'form-control is-invalid' if form.username.errors else 'form-control' %}
{{ form.username.label }}
{{ form.username(class=class) }}
{% if form.username.errors %}
<div class="invalid-feedback">
{{ form.username.errors[0] }}
</div>
{% endif %}
</div>
<div class="form-group">
{% set class = 'form-control is-invalid' if form.age.errors else 'form-control' %}
{{ form.age.label }}
{{ form.age(class=class) }}
{% if form.age.errors %}
<div class="invalid-feedback">
{{ form.age.errors[0] }}
</div>
{% endif %}
</div>
<button class="btn btn-primary">Signup</button>
</form>
Submit that form empty, or with data that doesn't meet the validation we defined and the page will "reload". The inputs will be in an error state, with their first error message shown.
Now With AJAX
There's nothing we need to change with the form. We can use it completely unchanged. We will of course need to change the endpoint to respond with a JSON structure containing the error messages for each input, or a success message. We will no longer be returning the template in the case of validation errors.
The New Endpoint
The new endpoint will work a little different. Instead of using the
validate_on_submit
function, we're going to check if request.method
was
POST and call the validate
function. If that returns True
then respond
with a success message. Otherwise we use jsonify
(imported from flask) to
serialize the form errors in to a JSON response.
@app.route('/withajax', methods=['GET', 'POST'])
def withajax():
form = Form()
if request.method == 'POST':
if form.validate():
return 'WOOT WOOT!'
return jsonify(form.errors), 400
return render_template('withajax.html', form=form)
We no longer need the jinja conditions to show the inputs in an invalid state, as we will be doing that logic in JavaScript now.
The New Template
<form id="form" method="POST">
{{ form.csrf_token }}
<div class="text-danger my-2" id="csrf_token-error">
</div>
<div class="form-group">
{{ form.username.label }}
{{ form.username(class='form-control') }}
<div id="username-error" class="invalid-feedback"></div>
</div>
<div class="form-group">
{{ form.age.label }}
{{ form.age(class='form-control') }}
<div id="age-error" class="invalid-feedback"></div>
</div>
<button class="btn btn-primary">Signup</button>
</form>
Submit that form empty, and the browser will load a new page containing a JSON structure. Now obviously we can't leave it that way, so let's add some JavaScript that overrides what happens on form submit.
The New Template With JavaScript
First let's get use getElementById
to get references to all the elements we
will need.
<script>
const form = document.getElementById("form");
const successMessage = document.getElementById("success-message");
const fields = {
csrf_token: {
input: document.getElementById("csrf_token"),
error: document.getElementById("csrf_token-error"),
},
username: {
input: document.getElementById("username"),
error: document.getElementById("username-error"),
},
age: {
input: document.getElementById("age"),
error: document.getElementById("age-error"),
},
};
</script>
Now we could have simply stored references in to individual variables instead of an object structure, but doing it like this will be make things easier.
Next let's override the form submit behaviour.
<script>
...
form.addEventListener('submit', async (e) => {
e.preventDefault();
const response = await fetch('/withajax', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
csrf_token: fields.csrf_token.input.value,
username: fields.username.input.value,
age: fields.age.input.value
})
});
if (response.ok) {
successMessage.innerHTML = await response.text();
form.style.display = 'none';
successMessage.style.display = 'block';
} else {
const errors = await response.json();
Object.keys(errors).forEach((key) => {
fields[key].input.classList.add('is-invalid');
fields[key].error.innerHTML = errors[key][0];
});
}
});
</script>
So you'll see in the else
of the if (response.ok)
check we actually
iterate over the keys of the response object. This is what I meant earlier
when I said storing references to our inputs in an object structure would make
out lives easier. We can simply iterate the keys in the validation errors,
and use those keys to refer to the input that needs to be updated to be in an
error state.
One last thing we need before this will work is to create the div used to display the success message when validation passes.
<div id="success-message" style="display: none;"></div>
Here's the final template
<form id="form" method="POST">
{{ form.csrf_token }}
<div class="text-danger my-2" id="csrf_token-error">
</div>
<div class="form-group">
{{ form.username.label }}
{{ form.username(class='form-control') }}
<div id="username-error" class="invalid-feedback"></div>
</div>
<div class="form-group">
{{ form.age.label }}
{{ form.age(class='form-control') }}
<div id="age-error" class="invalid-feedback"></div>
</div>
<button class="btn btn-primary">Signup</button>
</form>
<div id="success-message" style="display: none;"></div>
<script>
const form = document.getElementById("form");
const successMessage = document.getElementById("success-message");
const fields = {
csrf_token: {
input: document.getElementById("csrf_token"),
error: document.getElementById("csrf_token-error"),
},
username: {
input: document.getElementById("username"),
error: document.getElementById("username-error"),
},
age: {
input: document.getElementById("age"),
error: document.getElementById("age-error"),
},
};
form.addEventListener("submit", async (e) => {
e.preventDefault();
const response = await fetch("/withajax", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
csrf_token: fields.csrf_token.input.value,
username: fields.username.input.value,
age: fields.age.input.value,
}),
});
if (response.ok) {
successMessage.innerHTML = await response.text();
form.style.display = "none";
successMessage.style.display = "block";
} else {
const errors = await response.json();
Object.keys(errors).forEach((key) => {
fields[key].input.classList.add("is-invalid");
fields[key].error.innerHTML = errors[key][0];
});
}
});
</script>
Submit that with invalid data, and the fields will change to an error state with error messages. Submit valid data, and you'll get a success message.
If you would like to reference a complete working flask app demonstrating all this, I have one right here on github.