The End-to-End Guide to Building Forms in Flask

To start this week hot, let’s talk about something that everybody hates: forms.

The only thing more painful than filling out a web form is creating one, much less a functional one with feedback.

Listen, if you’re into creating pleasant form UI experiences, you’re probably into some freaky shit.

Call me.

Flask’s youth is an advantage in one regard, in that there are only so many libraries available to handle any given task.

In this case, there’s only one: the aptly named WTForms.

If you don’t have an immediate pressing need to create a Flask form and feel like ditching this post to check out Instagram, be my guest, but know this: handling form authentication, data submission, and session management is the pinnacle of app development.

This weird data collection traditional we experience every day actually touches on nearly all aspects of app development.

You could argue that he who creates forms is a harbinger of a golden age: a hero who brings us to the pinnacle of Western technology.

Then again, there’s always Instagram.

The Gist of it AllBefore laying down some code snippets for you to mindlessly copy+paste, it helps to understand conceptually what we’re about to throw down.

At a minimum, creating a form has us working routes, form models, and templates.

Since you already understand the concept of MVC, that entire last sentence was probably redundant, and I should probably just delete it as opposed to continuing to write this second sentence contemplating my own redundancy.

Oh well.

We’ll keep our routes in app.

py for a compact little app.

myproject├─ app.

py├─ config.

py├─ forms.

py├─ db.

py├─ /static│ ├─ /src│ │ ├─ js│ │ └─ less│ └─ /dist│ ├─ js│ ├─ css│ └─ img└─ /templates └─ layout.

html └─ index.

html └─ form.

htmlHere’s the game plan: our form, with all its fields, labels, and potential error messages, will live in forms.

py.

app.

pywill contain the logic of not only routing to the page serving the form, but also validating user input, which covers anything from error checking to session creation.

form.

html will be the presentation layer template which will get loaded into index.

html in this case.

Both of those templates are wrapped by layout.

html which is basically just metadata and shit you already know — we’ve been through this.

Let’s start off by creating our forms.

py.

What The FormWTForms has a nice little monopoly over the Python form handling industry dating back to Django, so at least we aren't burdened with choices here.

Set up your environment and let's install everything we need:pipenv shellpip3 install Flask WTForms Flask-LoginThat’ll hold us over for now.

In forms.

py, we’re going to do two imports:from wtforms import Form, StringField, PasswordField, validators, SubmitField, SelectFieldfrom wtforms.

validators import ValidationError, DataRequired, Email, EqualTo, LengthForms really boil down into these two things: types of input, and validation of said input.

There are plenty more fields and validators available, but this is what we’d need for a user signup form.

Guess what we’re making.

Classy as FormForms in your app will always be defined 1-to-1 with a Python class declaration.

It’s kind of like working with models, except they’re forms.

Just wait until your forms work with models.

from wtforms import Form, StringField, PasswordField, validators, SubmitField, SelectFieldfrom wtforms.

validators import ValidationError, DataRequired, Email, EqualTo, Lengthclass SignupForm(Form): """User Signup Form.

""" name = StringField('Name', [ validators.

DataRequired(message=('Don't be shy!')) ]) email = StringField('Email', [ Length(min=6, message=(u'Little short for an email address?')), Email(message=('That's not a valid email address.

')), DataRequired(message=('That's not a valid email address.

')) ]) password = PasswordField('Password', validators=[ DataRequired(message="Please enter a password.

"), ]) confirm = PasswordField('Repeat Password', validators=[ EqualTo(password, message='Passwords must match.

') ]) website = StringField('Website') submit = SubmitField('Register') def validate_email(self, email): """Email validation.

""" user = User.

query.

filter_by(email=email.

data).

first() if user is not None: raise ValidationError('Please use a different email address.

')As expected, every variable child of our class is a field.

Field types are declared immediately (such as StringField()) and accept a label (name of the field) as well as validators, which are conditions which must be met for the field to be considered valid.

A single field can accept an infinite number of validators, which would come in handy if you’re a stickler for password security.

As well as setting the validators, we also set the error message to pass to the user for that particular field if they are to submit invalid information.

Fields accept other parameters as well, such as placeholder text, if you’re interested in that side of things.

Some validators are more sophisticated enough to handle heavy-lifting: note how Email() is a validator in itself which handles the nonsense of looking for an ‘@’ symbol (or whatever it is PHP guys did in the 90's).

There’s even a regex validator available if you want to go full pro-mode.

Hardcore Validation XXXYou’ll notice the example provided contains a validate_email() function to check the database for a user record match.

We can basically write any custom logic we want to validate forms this way, in case the event that the standard validators just don’t cut it.

Forming the Actual FormAlright, it’s that time.

Jinja’s form handling isn’t as daunting as this next wall of text might seem, once you get in the groove of things:{% block content %}<div class="formwrapper"> <form method=post> <div class="name"> {{ form.

name(placeholder='Joe Blah') }} {{ form.

name.

label }} {% if form.

name.

errors %} <ul class="errors">{% for error in form.

name.

errors %}<li>{{ error }}</li>{% endfor %}</ul> {% endif %} </div> <div class="email"> {{ form.

email }} {{ form.

email.

label }} {% if form.

email.

errors %} <ul class="errors">{% for error in form.

email.

errors %}<li>{{ error }}</li>{% endfor %}</ul> {% endif %} </div> <div class="password"> {{ form.

password }} {{ form.

password.

label }} {% if form.

password.

errors %} <ul class="errors">{% for error in form.

password.

errors %}<li>{{ error }}</li>{% endfor %}</ul> {% endif %} </div> <div class="confirm"> {{ form.

confirm }} {{ form.

confirm.

label }} {% if form.

confirm.

errors %} <ul class="errors">{% for error in form.

password.

errors %}<li>{{ error }}</li>{% endfor %}</ul> {% endif %} </div> <div class="website"> {{ form.

website(placeholder='http://example.

com') }} {{ form.

website.

label }} </div> <div class="submitbutton"> <input id="submit" type="submit" value="Submit"> </div> </form></div>{% for message in get_flashed_messages() %}<div class="alert alert-warning"> <button type="button" class="close" data-dismiss="alert">&times;</button> {{ message }}</div>{% endfor %}Notice that our form contains a method, but neither a destination nor an action.

More on that late.

Each form field is pulling in three dynamic assets: the form itself, the display name, and a space reserved for error handling.

This general layout is robust enough to handle returning multiple errors per field, which we obviously would prefer to keep in-line with the offending fields for UI purposes.

Another way of handling errors is by utilizing Flask’s flash module.

A ‘flash’ is a temporary modal telling the user what they did wrong, which can be closed or simply expire after a number of seconds.

We’re using both forms of error handling here for educational purposes, but you probably won’t need to most of the time.

Drop Some Logic on These Foolsapp.

py contains the route to the form, which allows for both GET and POST methods.

Submitting a form in Flask cleverly routes the user to the page they’re already on.

Depending on what the logic decides, the user will experience either:Instant inline errors, with no visible change of page.

A successful redirect to wherever they hoped the form would take them.

from flask import Flask, url_for, render_template, Markup, redirect, request, flashfrom flask import session as login_sessionfrom forms import SignupFormfrom models import Userimport jsonapp = Flask(__name__, static_url_path='', static_folder="static", template_folder="templates")app.

config.

from_object('config.

Config')compress = FlaskStaticCompress(app)@app.

route('/signup', methods=['GET', 'POST'])def signup(): """Signup Form.

""" signup_form = SignupForm() if request.

method == 'POST': if signup_form.

validate(): flash('Logged in successfully.

') return render_template('/dashboard.

html', template="dashbord-template") return render_template('/signup.

html', form=signup_form, template="form-page")You’ll recognize SignupForm() as our good old buddy from forms.

py which has been imported here.

Because the user submitted their form, they will experience the page with everything that lives within if request.

method == 'POST': this time around.

Determining validation is as simple as the next line: if signup_form.

validate():.

This one-liner will validate each field individually, and deliver the proper errors to the offending fields.

This level of black magic saves us a huge headache and actually means that creating additional forms in the future won't be all that different from simply adjusting the class and template we already made.

All that negative form talk I dropped earlier was a test.

You passed.

What Happens Next?As you might infer from the conditional statements, the user will either successfully complete the form and move on to the next page, or they might find themselves in an Ancient Greek version of hell where they find themselves incorrectly filling out the same form forever.

Sucks to suck.

If this were a real signup form, we’d handle user creation and database interaction here as well.

As great as that sounds, I’ll save your time as I know you still have an Instagram to check.

Hats off to anybody who has made it through this rambling nonsense — you deserve it.

Originally published at hackersandslackers.

com on August 15, 2018.

.

. More details

Leave a Reply