2018-04-17T19:20:08Z

The Flask Mega-Tutorial Part XX: Some JavaScript Magic

This is the twentieth installment of the Flask Mega-Tutorial series, in which I'm going to add a nice popup when you hover your mouse over a user's nickname.

For your reference, below is a list of the articles in this series.

Note 1: If you are looking for the legacy version of this tutorial, it's here.

Note 2: If you would like to support my work on this blog, or just don't have patience to wait for weekly articles, I am offering the complete version of this tutorial packaged as an ebook or a set of videos. For more information, visit courses.miguelgrinberg.com.

Nowadays it is impossible to build a web application that doesn't use at least a little bit of JavaScript. As I'm sure you know, the reason is that JavaScript is the only language that runs natively in web browsers. In Chapter 14 you saw me add a simple JavaScript enabled link in a Flask template to provide real-time language translations of blog posts. In this chapter I'm going to dig deeper into the topic and show you another useful JavaScript trick to make the application more interesting and engaging to users.

A common user interface pattern for social sites in which users can interact with each other is to show a quick summary of a user in a popup panel when you hover over the user's name, anywhere it appears on the page. If you have never paid attention to this, go to Twitter, Facebook, LinkedIn, or any other major social network, and when you see a username, just leave your mouse pointer on top of it for a couple of seconds to see the popup appear. This chapter is going to be dedicated to building that feature for Microblog, of which you can see a preview below:

User Popup

The GitHub links for this chapter are: Browse, Zip, Diff.

Server-side Support

Before we delve into the client-side, let's get the little bit of server work that is necessary to support these user popups out of the way. The contents of the user popup are going to be returned by a new route, which is going to be a simplified version of the existing user profile route. Here is the view function:

app/main/routes.py: User popup view function.

@bp.route('/user/<username>/popup')
@login_required
def user_popup(username):
    user = User.query.filter_by(username=username).first_or_404()
    return render_template('user_popup.html', user=user)

This route is going to be attached to the /user/<username>/popup URL, and will simply load the requested user and then render a template with it. The template is a shorter version of the one used for the user profile page:

app/templates/user_popup.html: User popup template.

<table class="table">
    <tr>
        <td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td>
        <td style="border: 0px;">
            <p>
                <a href="{{ url_for('main.user', username=user.username) }}">
                    {{ user.username }}
                </a>
            </p>
            <small>
                {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
                {% if user.last_seen %}
                <p>{{ _('Last seen on') }}: 
                   {{ moment(user.last_seen).format('lll') }}</p>
                {% endif %}
                <p>{{ _('%(count)d followers', count=user.followers.count()) }},
                   {{ _('%(count)d following', count=user.followed.count()) }}</p>
                {% if user != current_user %}
                    {% if not current_user.is_following(user) %}
                    <a href="{{ url_for('main.follow', username=user.username) }}">
                        {{ _('Follow') }}
                    </a>
                    {% else %}
                    <a href="{{ url_for('main.unfollow', username=user.username) }}">
                        {{ _('Unfollow') }}
                    </a>
                    {% endif %}
                {% endif %}
            </small>
        </td>
    </tr>
</table>

The JavaScript code that I will write in the following sections will invoke this route when the user hovers the mouse pointer over a username. In response the server will return the HTML content for the popup, which the client then display. When the user moves the mouse away the popup will be removed. Sounds simple, right?

If you want to have an idea of how the popup will look, you can now run the application, go to any user's profile page and then append /popup to the URL in the address bar to see a full-screen version of the popup content.

Introduction to the Bootstrap Popover Component

In Chapter 11 I introduced you to the Bootstrap framework as a convenient way to create great looking web pages. So far, I have used only a minimal portion of this framework. Bootstrap comes bundled with many common UI elements, all of which have demos and examples in the Bootstrap documentation at https://getbootstrap.com. One of these components is the Popover, which is described in the documentation as a "small overlay of content, for housing secondary information". Exactly what I need!

Most bootstrap components are defined through HTML markup that references the Bootstrap CSS definitions that add the nice styling. Some of the most advanced ones also require JavaScript. The standard way in which an application includes these components in a web page is by adding the HTML in the proper place, and then for the components that need scripting support, calling a JavaScript function that initializes it or activates it. The popover component does require JavaScript support.

The HTML portion to do a popover is really simple, you just need to define the element that is going to trigger the popover to appear. In my case, this is going to the clickable username that appears in each blog post. The app/templates/_post.html sub-template has the username already defined:

            <a href="{{ url_for('main.user', username=post.author.username) }}">
                {{ post.author.username }}
            </a>

Now according to the popover documentation, I need to invoke the popover() JavaScript function on each of the links like the one above that appear on the page, and this will initialize the popup. The initialization call accepts a number of options that configure the popup, including options that pass the content that you want displayed in the popup, what method to use to trigger the popup to appear or disappear (a click, hovering over the element, etc.), if the content is plain text or HTML, and a few more options that you can see in the documentation page. Unfortunately, after reading this information I ended up with more questions than answers, because this component does not appear to be designed to work in the way I need it to. The following is a list of problems I need to solve to implement this feature:

  • There will be many username links in the page, one for each blog post displayed. I need to have a way to find all these links from JavaScript after the page is rendered, so that I can then initialize them as popovers.
  • The popover examples in the Bootstrap documentation all provide the content of the popover as a data-content attribute added to the target HTML element, so when the hover event is triggered, all Bootstrap needs to do is display the popup. That is really inconvenient for me, because I want to make an Ajax call to the server to get the content, and only when the server's response is received I want the popup to appear.
  • When using the "hover" mode, the popup will stay visible for as long as you keep the mouse pointer within the target element. When you move the mouse away, the popup will go away. This has the ugly side effect that if the user wants to move the mouse pointer into the popup itself, the popup will disappear. I will need to figure out a way to extend the hover behavior to also include the popup, so that the user can move into the popup and, for example, click on a link there.

It is actually not that uncommon when working with browser based applications that things get complicated really fast. You have to think very specifically in terms of how the DOM elements interact with each other and make them behave in a way that gives the user a good experience.

Executing a Function On Page Load

It is clear that I'm going to need to run some JavaScript code as soon as each page loads. The function that I'm going to run will search for all the links to usernames in the page, and configure those with a popover component from Bootstrap.

The jQuery JavaScript library is loaded as a dependency of Bootstrap, so I'm going to take advantage of it. When using jQuery, you can register a function to run when the page is loaded by wrapping it inside a $( ... ). I can add this in the app/templates/base.html template, so that this runs on every page of the application:

app/templates/base.html: Run function after page load.

...
<script>
    // ...

    $(function() {
        // write start up code here
    });
</script>

As you see, I have added my start up function inside the <script> element in which I defined the translate() function in Chapter 14.

Finding DOM Elements with Selectors

My first problem is to create a JavaScript function that finds all the user links in the page. This function is going to run when the page finishes loading, and when complete, will configure the hovering and popup behavior for all of them. For now I'm going to concentrate in finding the links.

If you recall from Chapter 14, the HTML elements that were involved in the live translations had unique IDs. For example, a post with ID=123 had a id="post123" attribute added. Then using the jQuery, the expression $('#post123') was used in JavaScript to locate this element in the DOM. The $() function is extremely powerful and has a fairly sophisticated query language to search for DOM elements that is based on CSS Selectors.

The selector that I used for the translation feature was designed to find one specific element that had a unique identifier set as an id attribute. Another option to identify elements is by using the class attribute, which can be assigned to multiple elements in the page. For example, I could mark all the user links with a class="user_popup", and then I could get the list of links from JavaScript with $('.user_popup') (in CSS selectors, the # prefix searches by ID, while the . prefix searches by class). The return value in this case would be a collection of all the elements that have the class.

Popovers and the DOM

By playing with the popover examples on the Bootstrap documentation and inspecting the DOM in the browser's debugger, I determined that Bootstrap creates the popover component as a sibling of the target element in the DOM. As I mentioned above, that affects the behavior of the hover event, which will trigger a "mouse out" as soon as the user moves the mouse away from the <a> link and into the popup itself.

A trick that I can use to extend the hover event to include the popover, is to make the popover a child of the target element, that way the hover event is inherited. Looking through the popover options in the documentation, this can be done by passing a parent element in the container option.

Making the popover a child of the hovered element would work well for buttons, or general <div> or <span> based elements, but in my case, the target for the popover is going to be an <a> element that displays the clickable link of a username. The problem with making the popover a child of a <a> element is that the popover will then acquire the link behavior of the <a> parent. The end result would be something like this:

        <a href="..." class="user_popup">
            username
            <div> ... popover elements here ... </div>
        </a>

To avoid the popover being inside the <a> element, I'm going to use is another trick. I'm going to wrap the <a> element inside a <span> element, and then associate the hover event and the popover with the <span>. The resulting structure would be:

        <span class="user_popup">
            <a href="...">
                username
            </a>
            <div> ... popover elements here ... </div>
        </span>

The <div> and <span> elements are invisible, so they are great elements to use to help organize and structure your DOM. The <div> element is a block element, sort of like a paragraph in the HTML document, while the <span> element is an inline element, which would compare to a word. For this case I decided to go with the <span> element, since the <a> element that I'm wrapping is also an inline element.

So I'm going to go ahead and refactor my app/templates/_post.html sub-template to include the <span> element:

...
                {% set user_link %}
                    <span class="user_popup">
                        <a href="{{ url_for('main.user', username=post.author.username) }}">
                            {{ post.author.username }}
                        </a>
                    </span>
                {% endset %}
...

If you are wondering where the popover HTML elements are, the good news is that I don't have to worry about that. When I get to call the popover() initialization function on the <span> elements I just created, the Bootstrap framework will dynamically insert the popup component for me.

Hover Events

As I mentioned above, the hover behavior used by the popover component from Bootstrap is not flexible enough to accommodate my needs, but if you look at the documentation for the trigger option, "hover' is just one of the possible values. The one that caught my eye was the "manual" mode, in which the popover can be displayed or removed manually by making JavaScript calls. This mode will give me the freedom to implement the hover logic myself, so I'm going to use that option and implement my own hover event handlers that work the way I need them to.

So my next step is to attach a "hover" event to all the links in the page. Using jQuery, a hover event can be attached to any HTML element by calling element.hover(handlerIn, handlerOut). If this function is called on a collection of elements, jQuery conveniently attaches the event to all of them. The two arguments are two functions, which are invoked when the user moves the mouse pointer into and out of the target element respectively.

app/templates/base.html: Hover event.

    $(function() {
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = event.currentTarget;
            },
            function(event) {
                // mouse out event handler
                var elem = event.currentTarget;
            }
        )
    });

The event argument is the event object, which contains useful information. In this case, I'm extracting the element that was the target of the event using the event.currentTarget.

The browser dispatches the hover event immediately after the mouse enters the affected element. In the case of a popup, you want to activate only after waiting a short period of time where the mouse stays on the element, so that when the mouse pointer briefly passes over the element but doesn't stay on it there is no popups flashing. Since the event does not come with support for a delay, this is another thing that I'm going to need to implement myself. So I'm going to add a one second timer to the "mouse in" event handler:

app/templates/base.html: Hover delay.

    $(function() {
        var timer = null;
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = event.currentTarget;
                timer = setTimeout(function() {
                    timer = null;
                    // popup logic goes here
                }, 1000);
            },
            function(event) {
                // mouse out event handler
                var elem = event.currentTarget;
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
            }
        )
    });

The setTimeout() function is available in the browser environment. It takes two arguments, a function and a time in milliseconds. The effect of setTimeout() is that the function is invoked after the given delay. So I added a function that for now is empty, which will be invoked one second after the hover event is dispatched. Thanks to closures in the JavaScript language, this function can access variables that were defined in an outer scope, such as elem.

I'm storing the timer object in a timer variable that I have defined outside of the hover() call, to make the timer object accessible also to the "mouse out" handler. The reason I need this is, once again, to make for a good user experience. If the user moves the mouse pointer into one of these user links and stays on it for, say, half a second before moving it away, I do not want that timer to still go off and invoke the function that will display the popup. So my mouse out event handler checks if there is an active timer object, and if there is one, it cancels it.

Ajax Requests

Ajax requests are not a new topic, as I have introduced this topic back in Chapter 14 as part of the live language translation feature. When using jQuery, the $.ajax() function sends an asynchronous request to the server.

The request that I'm going to send to the server will have the /user/<username>/popup URL, which I added to the application at the start of this chapter. The response from this request is going to contain the HTML that I need to insert in the popup.

My immediate problem regarding this request is to know what is the value of username that I need to include in the URL. The mouse in event handler function is generic, it's going to run for all the user links that are found in the page, so the function needs to determine the username from its context.

The elem variable contains the target element from the hover event, which is the <span> element that wraps the <a> element. To extract the username, I can navigate the DOM starting from the <span>, moving to the first child, which is the <a> element, and then extracting the text from it, which is the username that I need to use in my URL. With jQuery's DOM traversal functions, this is actually easy:

elem.first().text().trim()

The first() function applied to a DOM node returns its first child. The text() function returns the text contents of a node. This function doesn't do any trimming of the text, so for example, if you have the <a> in one line, the text in the following line, and the </a> in another line, text() will return all the whitespace that surrounds the text. To eliminate all that whitespace and leave just the text, I use the trim() JavaScript function.

And that is all the information I need to be able to issue the request to the server:

app/templates/base.html: XHR request.

    $(function() {
        var timer = null;
        var xhr = null;
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = $(event.currentTarget);
                timer = setTimeout(function() {
                    timer = null;
                    xhr = $.ajax(
                        '/user/' + elem.first().text().trim() + '/popup').done(
                            function(data) {
                                xhr = null
                                // create and display popup here
                            }
                        );
                }, 1000);
            },
            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                else if (xhr) {
                    xhr.abort();
                    xhr = null;
                }
                else {
                    // destroy popup here
                }
            }
        )
    });

Here I defined a new variable in the outer scope, xhr. This variable is going to hold the asynchronous request object, which I initialize from a call to $.ajax(). Unfortunately when building URLs directly in the JavaScript side I cannot use the url_for() from Flask, so in this case I have to concatenate the URL parts explicitly.

The $.ajax() call returns a promise, which is this special JavaScript object that represents the asynchronous operation. I can attach a completion callback by adding .done(function), so then my callback function will be invoked once the request is complete. The callback function will receive the response as an argument, which you can see I named data in the code above. This is going to be the HTML content that I'm going to put in the popover.

But before we get to the popover, there is one more detail related to giving the user a good experience that needs to be taken care of. Recall that I added logic in the "mouse out" event handler function to cancel the one second timeout if the user moved the mouse pointer out of the <span>. The same idea needs to be applied to the asynchronous request, so I have added a second clause to abort my xhr request object if it exists.

Popover Creation and Destruction

So finally I can create my popover component, using the data argument that was passed to me in the Ajax callback function:

app/templates/base.html: Display popover.

                                function(data) {
                                    xhr = null;
                                    elem.popover({
                                        trigger: 'manual',
                                        html: true,
                                        animation: false,
                                        container: elem,
                                        content: data
                                    }).popover('show');
                                    flask_moment_render_all();
                                }

The actual creation of the popup is very simple, the popover() function from Bootstrap does all the work required to set it up. Options for the popover are given as an argument. I have configured this popover with the "manual" trigger mode, HTML content, no fade animation (so that it appears and disappears more quickly), and I have set the parent to be the <span> element itself, so that the hover behavior extends to the popover by inheritance. Finally, I'm passing the data argument to the Ajax callback as the content argument.

The return of the popover() call is the newly created popover component, which for a strange reason, had another method also called popover() that is used to display it. So I had to add a second popover('show') call to make the popup appear on the page.

The content of the popup includes the "last seen" date, which is generated through the Flask-Moment plugin as covered in Chapter 12. As documented by the extension, when new Flask-Moment elements are added via Ajax, the flask_moment_render_all() function needs to be called to appropriately render those elements.

What remains now is to deal with the removal of the popup on the mouse out event handler. This handler already has the logic to abort the popover operation if it is interrupted by the user moving the mouse out of the target element. If none of those conditions apply, then that means that the popover is currently displayed and the user is leaving the target area, so in that case, a popover('destroy') call to the target element does the proper removal and cleanup.

app/templates/base.html: Destroy popover.

                function(event) {
                    // mouse out event handler
                    var elem = $(event.currentTarget);
                    if (timer) {
                        clearTimeout(timer);
                        timer = null;
                    }
                    else if (xhr) {
                        xhr.abort();
                        xhr = null;
                    }
                    else {
                        elem.popover('destroy');
                    }
                }

13 comments

  • #1 Daher Luis said 2018-04-23T15:28:52Z

    Thanks MIguel for this wonderfull tutorial!!

  • #2 Fabrizio said 2018-04-24T12:56:02Z

    Hi Miguel, thank you for your lessons. I think this is the best tutorial on Flask all over the web! I was wondering whether there will be room for a lesson about the interaction between ReactJS and Flask to add reactivity to the application. Thank you in advance :)

  • #3 Miguel Grinberg said 2018-04-25T01:18:14Z

    @Fabrizio: not as part of this tutorial. I wanted to stay out of the JS framework wars, can't do React and leave the Angular, Vue, etc. crowds behind. For this tutorial I'm using plain JS and jQuery. You should be able to adapt the techniques that I use to any framework.

  • #4 Boudhayan said 2018-04-30T08:49:57Z

    Hello! In your previous sections, whenever you have used url_for() in the HTML sections, you used the route name directly like for ex - url_for('index') or url_for('dashboard'). But in this section you have used url_for('main.user',username=user.username) in the HTML file. Why ? Is it possible to just use - url_for('user',username=user.username) for the above link ??

  • #5 Miguel Grinberg said 2018-05-02T05:14:49Z

    @Boudhayan: the format of the endpoints in the url_for() call changed in Chapter 15, when I introduced blueprints. Since the current version of the application is based on blueprints, you have to prefix each endpoint with the blueprint name.

  • #6 Vlad said 2018-05-11T19:58:23Z

    Hello, Miguel! a. What's the point of 'set user_link' and 'endset' in the _post.html ? These tags break the code for me, everything works fine if I get rid of them. b. I've managed to build urls with url_for in the Javascript by using the Javascript template literals. rather than: xhr = $.ajax('/user/' + elem.first().text().trim() + '/popup'). ... one could write: u_name = elem.first().text().trim(); xhr = $.ajax( `{{ url_for('main.user_popup', username='${u_name}') | replace("%7B", "{") | replace("%7D", "}") | replace("%24", "$") }}`). ... It works, although it looks ugly and hackish and I'm not sure if it's a good idea in general :)

  • #7 Miguel Grinberg said 2018-05-13T03:12:28Z

    @Vlad: the content that is inside the set/endset block is assigned to the user_link variable, which is used to generate a clickable link for the user. If you remove that, your user links are going to be broken.

  • #8 Vlad said 2018-05-13T11:59:32Z

    Oh, right, sorry. You, actually, explain this in part 13 'Marking Texts to Translate In Templates'. I completely forgot that I skipped that part and, hence, my code to follow you tutorial has slightly deviated from yours.

  • #9 John said 2018-07-25T06:17:01Z

    Hi Miguel, thanks for the great tutorial! If one decides not to use Bootstrap, could you advise on any alternative way to produce the popover effect?

  • #10 Miguel Grinberg said 2018-07-26T16:10:23Z

    @John: if you use a different CSS framework, then look for ways to do it within the framework of your choice, or maybe also as a plugin to that framework. If you are using something more basic like jQuery to code your site, then look for a jQuery plugin that does it.

  • #11 Av said 2018-09-05T01:44:01Z

    Thank you for the great tutorial.

  • #12 Jose Luis said 2018-09-07T08:25:47Z

    Hi Miguel, is there a proper way to pass javascript parameters when redirecting to a new URL using url_for? For example let's think of an app in which the user clicks on a map from googlemaps and must be redirected to a new page which requires the x and y for some processing. I have seen this asked in a few places but never found an answer that seems correct (see for example https://stackoverflow.com/questions/36143283/pass-javascript-variable-to-flask-url-for) Some times I did it by defining a "dummy url" without paramters that will be accepted by the url_for function and then adding the parameter with a slash in javascript. Something like: ... @app.route('/process_xy') @app.route('/process_xy/x/y') def process_xy( x="0",y="00): ...process xy ... and then redirecting in javascript with a statement like ... window.location.href = "{{ url_for('process_xy') }}"+"/"+x.toString() +"/"+y.toString(); ... But I don't know if this is right.

  • #13 Miguel Grinberg said 2018-09-13T20:53:13Z

    @Jose: you can pass arguments that are part of the URL or query string as keyword arguments in the url_for() call.

Leave a Comment