Introduction
As a follow-up of part I of my learnings of Flask, I will show how to build a simple entry page that gets populated with some API data after the user types in some input on a search box.
This will be totally responsive, it will work well on mobile devices and it will be the base for further developing more functionality.
The final product
Before, we start, this is what we will be building:
And if we look at it on our mobile phone:
Note how the recipes stack on top of each other and also more importantly, note the focus that the search bar contains now, by taking the whole screen and still keeping it's dimensions proportional to the whole screen size. It's responsive, and easy to use on a mobile phone, even on such an early stage in the app. It won't win any awards, but, it's a great start!
Let's learn how to build this ourselves!
Some basics on architecture and Flask routing
The idea for this landing page is quite simple, we want to:
Input some data (in the form of comma separated names of ingredients)
Send the data to the backend, where it will be handled by the flask routing mechanism, to be further processed
When we reach the correct endpoint, we will perform a GET request to the API, using the "cleaned" values as input, in a format where they can be consumed by the API, and receive a JSON as a response.
Once we have the response in our hands, we can link it to the Jinja template (Jinja is a template engine that is used with Flask to generate pages) and then ensure that it will be displayed as we want in the UI.
All of this will be quite a lot of work, so let's break it down step by step.
Data input: version 0.1
Obviously, this way of inputting data is flawed, and will need to be improved later.
Letting users entering comma separated values as input manually, is a recipe for disaster, but, it will do for now, for this prototype and as a motivation to keep building on top of what we have.
It's important to know how to stay motivated, and, building something, iterating and improving it, can be a great way to do so.
To start, we can see the HTML for the template that will compose our view:
<form method="POST">
<div class='col-xs-12 col-sm-12 col-md-10 col-lg-10'>
<div class='input-group'>
<input class='form-control' type='text' name='restaurant_name'
placeholder='Enter ingredients separated by commas...'/>
<span class="input-group-btn">
<button type='submit' class='btn btn-default'>
<span class='glyphicon glyphicon-search'></span>
</button>
</span>
</div>
</div>
</form>
This is a small piece of the full Jinja template that powers the page seen above in the beginning of the article.
It contains a form configured with the POST method, which will allow us to POST data to the server.
When we enter some input, for instance, cheese,steak
, this will be POSTed to the backend as form contents.
On the Flask side, the "binding" between the endpoint and the actual data we want to process happens through:
the name property of the input bar, "restaurant_name", from the front-end side;
the Flask,
request
object, which acts as a proxy to the current context and can be used to check for method requests (is it a POST, GET, etc.?), used to access form content (request.form[]) and other things. This object object is created with the WSGI environment as first argument and will add itself to the WSGI environment as'werkzeug.request'
, becoming available for use on any endpoint;
Once we have the data available to us on our endpoint, we can simply process it (in this case, we will need to do some processing since the API requires a specific format for the request we are going to make), use it to make a request to the API and receive a JSON response, and then we can render our template with the data we need, and our initial page will be completed!
The Flask endpoint which will do the work
Now that we can send data, we need to configure an endpoint to receive that data and operate on it, so we can do something useful with it!
The way we can configure these endpoints using Flask is by defining routes, which are bind to the Flask app instance, and that are used to control and redirect incoming request to the correct handling method.
A route receives the path (think, URL mapping) to which to match the request and also the methods it will support.
The route is added as a decorator on top of the actual method name which is what we use from the client-side to refer to a specific endpoint:
@app.route('/', methods=['GET', 'POST'])
def recipes():
if request.method == 'POST':
content = requests.get(
"https://api.spoonacular.com/recipes/findByIngredients?ingredients=" +
convert_input(request.form['restaurant_name']) +
"&apiKey=" + API_KEY)
json_response = json.loads(content.text)
print json_response
return render_template("restaurant_list.html", response=json_response) if json_response != [] else render_template(
"restaurant_list.html", response="")
else:
return render_template("restaurant_list.html")
On this case, the route decorator is '/' since we are matching the homepage and not any other specific path (so the URL matched is: 0.0.0.0:5000
) and the endpoint needs to support both POST and GET methods since:
when the page first loads, a GET request is performed to the homepage so that the initial page can be rendered on the screen (that's the else part).
when we type something on our search bar, a POST request happens as well on the homepage, in order to POST the data from our form, so both methods need to be supported.
The logic of the endpoint itself is relatively straightforward, on cases where we receive a POST request, we will perform a GET request to our API to retrieve the data we want, receive a JSON response and render the template enriched with that data in order to populate our UI.
We use the python requests
module to perform the GET request, and receive the response. The method convert_input(request.form['restaurant_name'])
is a simple helper method that we use to ensure that the input parameters are both well-formatted and in a format which is required by the API. To know this, it's good practice to read the available documentation for the API (this one for the current code) to ensure all is correct.
When the page is loaded the first time, the response will be empty, and on the Jinja side we'll render only what we need according to the response we receive, which leads us to...
Rendering the data on the front-end
Jinja is a template engine language, in the sense it combines static user interface elements with dynamic data, which is perfect as a mechanism to render web pages with a base template and mutating data.
Recall that we had a variable called response inside our render_template
method. This will be used to parametrize the front-end with custom view elements based on the value of the response:
<div class='col-xs-12 col-sm-12 col-md-10 col-lg-10'>
{% if response!=undefined and response|length > 0%}
<h2>Recipes using these ingredients</h2>
{% else %}
{% if response!="No recipes found"%}
<p>No recipes found</p>
{% else %}
<p></p>
{% endif %}
{% endif %}
<table>
<tr>
{% for entry in response %}
<div class='col-xs-3 col-sm-3 col-md-3 col-lg-3' id="recipeSelectionComponent">
<img id="recipeImage" src="{{entry['image']}}">
<button id="{{entry['id']}}" type=button class="btn btn-link" onclick="recipeSteps({{entry['id']}})">{{
entry['title'] }}
</button>
</div>
{% endfor %}
</tr>
</table>
</div>
As we see, Jinja uses curly brackets as its notation for parametrizing values received from the outside and that are meant to be dynamic in nature and uses a pipe ( | ) to link to some properties like length and others. It also allows looping and template inheritance amongst other things.
If a response is empty or the input is invalid (generating an undefined response) we choose to render a simple paragraph to indicate there are no recipes.
Otherwise, we iterate over the data (which is a JSON list) and we build a simple custom component which consists of an image and a button which will be implemented later.
Notice how using loops can make it so easy to create custom components that leverage complex data.
To finish this article the only thing needed to mention, is that we need headers for Bootstrap which is a library that allows flexible designs across devices, which can be added to the header along with a Jinja feature which allows us to specify where to add our custom CSS files:
<head>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
</head>
The last line is the only one worth mentioning here and what it does is to ensure that the viewport scales according to the device width and it's not user scalable which gives us a "normal search bar" on mobile devices as opposed to a bar with the same width as in a desktop, but zoomed in to fit on a tiny screen, which would make it not mobile friendly.
The CSS file contains some basic and standard style for widths and heights of individual components and that's all there is to it!
Conclusion
I hope you enjoyed reading this article, and that it can be interesting for you if you're keen on learning Python and Flask! (and if you love food).
PS: Suggestions regarding architecture of endpoints and best practices are welcome, you never stop learning in programming!
Top comments (0)