In a previous tutorial , we’ve seen how to build a backend of a CRUD app . In this tutorial, we will complete our full stack application by building the frontend of that app.
At the end of this tutorial, you will learn how to use Jinja2 template to render HTML templates and Bootstrap through Flask-Bootstrap extension by building a CRUD app which is a common task in web dev that allows you to create, read, update, and delete things. I’ve made it a useful RESTful API which is a bookshop app built by Flask .
I’ll also offer something for free right at the bottom. Stay tuned!
This tutorial is based on Flask and Flask-Bootstrap extension .
Flask is a lightweight web framework that allows you to build web apps in Python. You can use extensions to extend the functionality of your app like Bootstrap that we will going to use in this tutorial.
Bootstrap is a powerful frontend toolkit that you can use to create stunning apps.
In this tutorial, we will build this app:
Revisiting the backend
Let’s revisit the backend app introduced in the previous tutorial.
As we can see, our Flask API has 5 functions that depend on the Book
table. You’ll see how these functions will invoke appropriate methods
from SQLAlchemy. Let’s first see how we structure the API functions.
Those functions will be invoked by the Flask API and will be decorated
with the @app.route
decorator. The mappings for each are shown
below:
get_books()
to list all the books which maps to/book/list
URL using the GET request.get_book(isbn)
to get the specified book defined by the URL parameterisbn
that we pass to the function. This function maps to/book/<isbn>
URL using the GET request.create_book()
to add a new book to the database. This function maps to/book
URL using the POST request.update_book(isbn)
to update the specified book which maps to/book/<isbn>
URL using the PUT request.delete_book(isbn)
to delete the specified book which maps to/book/<isbn>
URL using the DELETE request.
Creating the navigation bar
Let’s start with the navigation bar which we import from Bootstrap. To
be able to use Bootstrap in our HTML template, you need to import it in
the Jinja2 template. Let’s put the following in the index.html
file:
{% extends "bootstrap/base.html" %}
You can set the title of our app with the following command:
{% block title %}Bookshop{% endblock %}
Add the following for the navbar block:
{% block navbar %}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Bookshop</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home</a></li>
</ul>
</div>
</div>
</nav>
{% endblock %}
Let’s now set up the parent root to render the index
template:
@app.route("/")
def index():
return render_template("index.html")
Now, your navbar would look like the following:
Creating the table
We want to create a table like this:
To create such a table, use the bootstrap version of table and wrap it inside the content Jinja2 block like the following:
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<table class="table" border="1">
<thead>
<tr>
<th>ISBN</th>
<th>Author</th>
<th>Title</th>
<th>Price</th>
<th colspan="2">Action</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{ book.isbn }}</td>
<td>{{ book.author }}</td>
<td>{{ book.title }}</td>
<td>{{ book.price }}</td>
<td><button type="button" class="btn btn-success" data-toggle="modal"
data-target="#update_book_{{book['isbn']}}">Update</button></td>
<td><button type="button" class="btn btn-danger" data-toggle="modal"
data-target="#delete_book_{{book['isbn']}}">Delete</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
Note that the div HTML tag with the container class provides a responsive width to the application to be convenient with different screen sizes.
Let’s make sure we understand the details of the previous HTML snippet.
The table HTML tag wraps two HTML tags; thead
and tbody
. The thead
includes the header cells; each one ( th
) is inside one row
( tr
). As you can see from the picture above, there are 5 columns
with the headers: ISBN, Author, Title, Price, and Action. Each header
spans one column except the Action column that spans two columns.
The tbody
wraps multiple rows depending on how many books you have
in your database. As you can see, you can run Python code inside a
Jinja2 template. The expression of a for loop is wrapped in {% %}
while the variable definition is wrapped inside {{ }}
(the ones
inside td
tags).
Finally, the last two td
tags have two buttons; one to update a book
and another to delete a book. Each button is customized by bootstrap to
indicate success or danger CSS. Each has a data toggle of modal to
indicate that they will open a pop up modal and we will have a decision
accordingly. The data-target attribute is a unique string that links to
the id in the modal tag. We then end up with the for loop block in the
Jinja2 template with endfor
in a Jinja2 expression.
If you run this Flask app, you’ll see that the headers of the table are hidden by the nav bar and not aligned well on the screen. You’ll find also that the text inside the cells of the table are not centered. To edit that, we need to add some CSS styles here. But how do we import a CSS file in a Flask app?
Let’s first make the CSS file and then learn how a Flask app would pick
this CSS file. Let’s name main.css
and put it in app/static
directory.
Open the file and add the following:
body {
padding-top: 50px;
}
td, th {
text-align: center
}
These CSS styles do three things.
First, remember that when the Jinja2 template is rendered, the page is
structured by HTML tags. So here we style the body
tag with a
padding on the top. This will offset it from appearing under the nav
bar. Adjust it to the amount you want. I’ve made it 50px.
The second style is customizing two HTML tags inside the table to have the text centered.
Now, it’s time to link this CSS file to be rendered in the index
page.
Add the following to the index.html
file:
{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
{% endblock %}
The head
block, in this Jinja2 template, is appending to the head
HTML tag with the super()
method. This method represents the
parent class and what’s following it is what we need to list after the
sibling of the head
tag.
The sibling is a link
tag pointing to the main.css
file inside
the static
directory. This is done by wrapping the directory and the
filename as arguments to the url_for()
method as an expression value
to the href
attribute.
Now, if you run the app again, you’ll find that all td
and th
elements (texts and buttons) inside the table are centered and the whole
table is padded appropriately.
Let’s add the title and structure our app’s templates accordingly.
Structuring the Jinja2 templates and setting the title
What we need to do is the following:
To set a title, go ahead to the index.html
file and add the
following to the content Jinja block the first thing.
<div class="starter-template">
<h1>
Welcome to our bookshop!
</h1>
</div>
Note, that the div
tag has a .starter-template class because we want
to style that element. If you run the app now, you’ll find that the h1
tag is inclined to the left. To make it centered and to add some
padding from all four sides, add the following to the main.css
file:
.starter-template {
padding: 40px 15px;
text-align: center;
}
Now, The title has a top and bottom padding of 40px, and right and left padding of 15px. It’s also centered.
In web apps, it’s better to separate our presentation layers to do just one thing . You might observe that the nav bar is a necessary component in almost all pages of the app we’re building. Also, the title might be so. In this app, we demonstrate this separation to be able to extend HTML templates from one template to another and to avoid duplicate code especially if you want to make this app a bit more complex, not a single page.
So let’s break some components inside the index.html
file and put
them in a new file: base.html
.
The base.html
can now contain the head
and navbar
blocks.
Let’s also move the following from index.html
:
{% extends "bootstrap/base.html" %}
{% block title %}Bookshop{% endblock %}
Now, we need to extend the index
template extend from base
template with the following:
{% extends "base.html" %}
To continue getting rid of DRY (Don’t Repeat Yourself), we can customize
the content
block. So instead of wrapping our HTML elements with a div
tag with a container
class, we can make a specific block
inside the base
template and use that block in any template we
extend base
from.
So append the following to what exists in the base.html
:
{% block content %}
<div class="container">
{% block page_content %} {% endblock %}
</div>
{% endblock %}
Now, we define a block called page_content
. Use this block instead
of the content
block in the index
template. Simply, open the index.html
file, replace content
by page_content
and remove
the div tag with the container
class.
Let’s now create the popup window that will show when we add a new book.
Creating a popup modal for adding a book
The modal that we will build looks like the following:
Before we create it, let’s add the “Add a book” button. Add the
following to the index.html
right after the
title:
<button type="button" data-toggle="modal" class="btn btn-lg btn-primary" data-target="#insert_book">Add a book</button>
This modal references an id called “insert_book” which is the id of the modal that we’re going to create. Add the following HTML snippet after that button:
<!-- Modal 1 for adding a book -->
<div class="modal fade" id="insert_book" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">Add a book</h4>
</div>
<form action="{{url_for('add_book')}}" method="post">
<div class="modal-body">
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Author</label>
<div class="col-xs-10">
<input type="text" id="author" class="form-control" name="author" placeholder="Author" />
</div>
</div>
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Title</label>
<div class="col-xs-10">
<input type="text" class="form-control" name="title" placeholder="Title" />
</div>
</div>
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Price</label>
<div class="col-xs-10">
<input type="number" class="form-control" name="price" placeholder="Price" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-success">Submit</button>
</div>
</form>
</div>
</div>
</div>
<!-- End Modal 1 -->
This is imported from Bootstrap. The div
tag here has an id of
“insert book” as we mentioned. This modal essentially consists of div
tags with the modal-header
, modal-body
, and modal-footer
classes. So how can we interact with that through the database? In other
words, how can we input data and store it inside our MySQL engine (or
whatever)? Simple.
In our case, we use a form. This form wraps the body and the footer of
the modal. It has an action
attribute pointing to the endpoint that
we want to invoke, with a method
of a post request. In this modal,
we point to the add_book
endpoint in the routes.py
file.
Inside the body of the modal, we have three rows. Each has a label and
an input
tag inside a div. The input
has a placeholder; the text
that appears with gray on the text box. The input
has a name
attribute which is the name of the column in our database (It’s the
variable name in the ORM, in our case).
To configure the endpoint of add_book
, let’s add the following to
the routes:
@app.route('/add_book/', methods=['POST'])
def add_book():
if not request.form:
abort(400)
book = Book(
title=request.form.get('title'),
author=request.form.get('author'),
price=request.form.get('price')
)
db.session.add(book)
db.session.commit()
return redirect(url_for("index"))
Note that we get the request from the form and add it to the SQLAlchemy
session and then commit it to the database. We finally render the index
template to trigger the change.
Try to add a new author name and a title priced at any number and see the new change when you hit Submit.
Let’s add the second modal to update a book.
Creating popup modal for updating a book
We need to create a popup modal like this:
This modal is similar to the previous one. Let’s add the following to
the index.html
file, after the Update button:
<!-- Modal 2 for updating a book -->
<div class="modal fade" id="update_book_{{book['isbn']}}" tabindex="-1" role="dialog"
aria-labelledby="basicModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">Update a book</h4>
</div>
<form action="{{url_for('update_book', isbn=book['isbn'])}}" method="post">
<div class="modal-body">
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Author</label>
<div class="col-xs-10">
<input type="text" id="author" class="form-control" name="author"
value="{{book['author']}}" />
</div>
</div>
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Title</label>
<div class="col-xs-10">
<input type="text" class="form-control" name="title"
value="{{book['title']}}" />
</div>
</div>
<div class="form-group row">
<label for="author" class="col-xs-2 control-label">Price</label>
<div class="col-xs-10">
<input type="number" class="form-control" name="price"
value="{{book['price']}}" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-success">Submit</button>
</div>
</form>
</div>
</div>
</div>
<!-- End Modal 2 -->
Things to note here:
- The endpoint here is
update_book
, so we need to configure that in the route - In the form, there is an action of
url_for('update_book', isbn=book['isbn'])
which indicates the endpoint as the first argument while the second argument indicates that argument that would be passed to that endpoint. In this case, we need to pass the ISBN value to theupdate_book
method to update that specific book - The placeholder is the current value that you might need to change.
That’s why we configure it with the
book['price']
in case of updating the price value
To make it work, add the following route:
@app.route('/update_book/<int:isbn>', methods=['POST'])
def update_book(isbn):
if not request.form:
abort(400)
book = Book.query.get(isbn)
if book is None:
abort(404)
book.title = request.form.get('title', book.title)
book.author = request.form.get('author', book.author)
book.price = request.form.get('price', book.price)
db.session.commit()
return redirect(url_for("index"))
Let’s finalize this tutorial with the delete modal.
Creating a popup modal for deleting a book
We want to end up with the following:
Let’s add the following to the index
template, after the Delete
button:
<!-- Modal 3 for deleting a book -->
<div class="modal fade" id="delete_book_{{book['isbn']}}" tabindex="-1" role="dialog"
aria-labelledby="basicModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">Delete a book</h4>
</div>
<form action="{{url_for('delete', isbn=book['isbn'])}}" method="post">
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-12 col-form-label">Do you want to delete the book <span
style='font-weight:bold;color:red'>{{book['title']}}</span>
?</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
<!-- End Modal 3-->
This modal has the same main components; a header, a body, and a footer.
The body in this case is wrapped in a form
with an action of delete
and ISBN value passed to the delete
endpoint.
Configure it with the following route:
@app.route("/delete/<int:isbn>", methods=["POST"])
def delete(isbn):
book = Book.query.get(isbn)
if book is None:
abort(404)
db.session.delete(book)
db.session.commit()
return redirect(url_for("index"))
Now once you hit Delete, that record will be deleted from the database.
Conclusion
This tutorial covered the basics of how to create a frontend for your Flask app. We used Jinja2 templates to render HTML pages, and Bootstrap for CSS to style our elements. We did so through creating a real project, a bookshop.
We’ve seen how to use modals and reference them in our app, how to use forms, how to map the forms to the database, how to create a route that gets the request from that form, and how to iterate in our app and build each component in a step-by-step approach.
To write clean code in Python like the above and beyond, download my free ebook; Cleaner Python
You can get the complete code for this tutorial from this link .
Originally published on Python Code