DEV Community

Bruno Oliveira
Bruno Oliveira

Posted on • Updated on

Flask series part 3 - Adding detail views to your app

Introduction

On the latest installment of this series, we created a basic single page web application with Flask that leveraged a public API to display some data. Let's work on adding a detail page to the app to display the details of a particular record (recipes on the case of our app).

What we want to have

By the end of the previous post, we ended up with a functional search where we could input some comma separated ingredients and receive a list of top 10 recipes matching the entered ingredients. This is already quite nice, but what we want to achieve is to be able to, when clicking on a certain recipe, to see details about it, like cooking instructions and maybe a list of ingredients as well!

In order to do this, we'll need to refer to the API documentation to see which call we can use, and we'll need to design the corresponding Jinja view.

There's also an added complexity step, which relates to the fact that this endpoint will need to receive information with the context of the clicked recipe, typically, this is an ID or similar unique identifier for the current record, and we'll need to bind it to our get request.

Perform GET request with JS from a Jinja template

We can use AJAX to perform a GET request to the backend.

AJAX which stands for Asynchronous JavaScript and XML is a technique where web applications can send and retrieve data from a server asynchronously (in the background) without interfering with the display and behavior of the existing page. By decoupling the data interchange layer from the presentation layer, Ajax allows web pages and, by extension, web applications, to change content dynamically without the need to reload the entire page.

To do it, we first bind a function to be called when the button is clicked:

 <button id="{{entry['id']}}" type=button class="btn btn-link" onclick="recipeDetails({{entry['id']}})">{{
                    entry['title'] }}
                </button>

The onclick property of our button is bind to call the function recipeDetails with the ID of the clicked entry. The ID is used here because it is what will be needed on the backend to be used on the Flask side to make the correct GET call to the API.

The onclick references a function call, which we need to define within a JavaScript file. The way to define this function can vary whether we use (or not) jQuery and also depending on which type of UI framework we are using. For us, in Jinja, we refer to the JS file from the HTML document like this:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{{url_for('static', filename = 'jquery.js')}}">\x3C/script>')</script>
<script type="text/javascript" language="javascript" src="{{url_for('static', filename='recipe_detail.js')}}"></script>
</html>

Notice the way to refer to the place where our script sits. It follows the standard Flask syntax and the first argument of url_for is the directory where the file sits.

Inside our script we can add the following code:

function recipeDetails(recipeId) {
        $.ajax(
            type: 'GET',
            url: 'recipe/'+recipeId,
            success: function (data) {
                window.location = "/recipe/"+recipeId;
            },
            fail: function (data) {
               window.location = "/error";
            }
        });
}

Essentially this is a function that uses jQuery to perform an AJAX GET request that will be made with the url for the new view, parametrized with the recipe ID of which we want to see the details of.

Since it's asynchronous by nature, the main program can keep executing and the result will be in a callback function, i.e. we perform the get request, and, when the computation will be finished, we will receive our result, either on the success branch or the fail branch depending whether or not the computation succeeded or failed, respectively.

On both cases, we simply redirect to a new page after the return value is received.

Handling the request from Flask side

To handle the request, we define a route:

@app.route('/recipe/<recipe_id>', methods=['GET'])
def recipe(recipe_id):
    response = requests.get("https://api.spoonacular.com/recipes/informationBulk?ids="+recipe_id+"&includeNutrition=true&apiKey="+API_KEY)
    return make_response(render_template("recipe_details.html", recipe_id=json.loads(response.text)), 200)

And we simply perform a get request to the API with the parameter recipe_id as explained above, receive the response, and return a Response object consisting of the rendered template with the data and the success code 200. Currently, only the "happy path" is handled and this will change in future iterations to handle the failing case as well.

Noteworthy here is the decorator for the route:

'/recipe/<recipe_id>'

Note how the recipe_id parameter is in angle brackets. This tells Flask that this value should be interpreted as a URL parameter and it will allow our users to be displayed dynamic pages according to the recipe they select.

It's also how the templates in Jinja can be reused thanks to the parametrization of the page they will render

This is all we need on the flask side.

The complete Jinja template

Here is the complete Jinja template for our new view:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Recipe details</title>
    <!-- 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>Recipe details</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>
<body>
<p id="recipeTitle">{{ recipe_id[0]['title'] }}</p>
{% for entry in recipe_id[0]['extendedIngredients'] %}
<li>{{entry['originalString']}}</li>
{% endfor %}
<img id="recipeImage" src=" {{ recipe_id[0]['image'] }}">
<p id="recipeInstructions">{{ recipe_id[0]['instructions'] }}</p>
</body>
</html>

We can notice that the base of our page has not changed, the head and footer are still the same. There's a feature of Flask called template inheritance that allows one to define a base template and simply have other files extending the base one. I will explore this feature on a further stage of my application development.

Notice how on the body of the template we use powerful looping constructs to iterate over all that we need from our JSON response. Simple, clean, dynamic, reusable.

Conclusion

We saw how Flask allows us to create parametrized views, how to link AJAX requests from Jinja to Flask, how routes are configured to handle dynamic pages and how to build responses as a return value from our endpoints!

Hope you liked reading, keep following for more! I plan to keep adding functionalities to allow to explore the Flask framework further and further and will write posts and start new projects as I see fit and my schedule allows.

PS: Our current view, with minimal CSS tweaks now looks like:

detail view

Discussion (0)