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()
    form = EmptyForm()
    return render_template('user_popup.html', user=user, form=form)

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) %}
                    <p>
                        <form action="{{ url_for('main.follow', username=user.username) }}" method="post">
                            {{ form.hidden_tag() }}
                            {{ form.submit(value=_('Follow'), class_='btn btn-default btn-sm') }}
                        </form>
                    </p>
                    {% else %}
                    <p>
                        <form action="{{ url_for('main.unfollow', username=user.username) }}" method="post">
                            {{ form.hidden_tag() }}
                            {{ form.submit(value=_('Unfollow'), class_='btn btn-default btm-sm') }}
                        </form>
                    </p>
                    {% 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, but I'm using the $() jQuery function which returns a list of matching results, so this is going to be a one item list. To extract the username, I can navigate the DOM starting from the first and only item in elem, 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 list of DOM nodes returns the first one. The text() function returns the text contents of a node and all of its children combined, without including HTML tags. 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 filter the <a> and </a> but 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');
                    }
                }

49 comments

  • #26 kevin said 2019-04-14T02:00:42Z

    Hey Miguel, thanks for taking the time to write this and answer so many questions. What is the best approach to using flask with javascript forms. I would like to use flexx to create the required javascript (ex: https://flexx.readthedocs.io/en/stable/examples/themed_form_src.html#themed-form-py) but have flask handle the form submission. Is this possible or practical? Thanks

  • #27 Miguel Grinberg said 2019-04-14T10:01:53Z

    @kevin: there shouldn't be really any problems. Flask receives form information in the standard format that browsers use. Most client side form libraries are going to preserve this format, so as far as Flask is concerned there will not be any difference. You will obviously need to handle the request as an Ajax request, so you would not be returning a full templated page as a response, but other than that there shouldn't be any difference.

  • #28 Célia said 2019-05-13T09:06:08Z

    Hello Miguel,

    First of all, thank you for this tutorial. It's really amazing and helpfull. I would also like to know how to create a post automatically with a predefined text? I am trying to create a chatbot from your flask interface and I would like to nest his answers between the user's posts as if it were a real person.

    Thank you in advance for your answer.

  • #29 Miguel Grinberg said 2019-05-14T10:48:26Z

    @Célia: your question is too vague for me to answer it. An automatic post is no different than a manual post in terms of how the server creates it. If you are asking how to generate the text of the post, the topic of chat bots is vast, there are many ways to do that and which way you use will depend on your particular situation.

  • #30 Elvin said 2019-06-09T11:13:09Z

    Miguel, Thanks a lot for your great tutorial. I love it.

    When I amend the width of each td in user_popup html, the popup window size remained unchanged?

    May you shed a light on how to enlarge the popup window size? Thanks a lot!

  • #31 Miguel Grinberg said 2019-06-09T14:31:05Z

    @Elvin: Impossible for me to advice you if you don't show me what you've done.

  • #32 Zarkaiss said 2019-07-12T13:40:05Z

    Getting this error when trying to do the user_popup template any ideas on what's going on? It's setup exactly like the user template.

    File "/home/zarkaiss1/microblog/app/templates/user_popup.html", line 18, in template

    {{ ('%(count)d followers', count=user.followers.count()) }}, File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/environment.py", line 497, in _parse return Parser(self, source, name, encode_filename(filename)).parse() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 901, in parse result = nodes.Template(self.subparse(), lineno=1) File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 875, in subparse add_data(self.parse_tuple(with_condexpr=True)) File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 620, in parse_tuple args.append(parse()) File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 432, in parse_expression return self.parse_condexpr() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 437, in parse_condexpr expr1 = self.parse_or() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 450, in parse_or left = self.parse_and() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 459, in parse_and left = self.parse_not() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 470, in parse_not return self.parse_compare() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 474, in parse_compare expr = self.parse_math1() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 496, in parse_math1 left = self.parse_concat() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 507, in parse_concat args = [self.parse_math2()] File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 517, in parse_math2 left = self.parse_pow() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 528, in parse_pow left = self.parse_unary() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 546, in parse_unary node = self.parse_primary() File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/parser.py", line 577, in parse_primary self.stream.expect('rparen') File "/home/zarkaiss1/.virtualenvs/flask/lib/python3.6/site-packages/jinja2/lexer.py", line 384, in expect self.name, self.filename) jinja2.exceptions.TemplateSyntaxError: expected token ')', got '='

  • #33 Miguel Grinberg said 2019-07-13T15:32:21Z

    @Zarkaiss: jinja templates do not implement the full python syntax. For this line that gives you the error, try the following:

    {{ user.followers.count() }} followers

  • #34 Jonas Andersen said 2019-07-26T00:55:54Z

    A guy wrote a plugin that gives you the "url_for" syntax inside of a javascript, if you want to keep all of the clientside url stuff dynamic with a familiar syntax.

    http://stewartpark.github.io/Flask-JSGlue/

  • #35 Len said 2019-08-10T05:42:11Z

    Great tutorial but code is hard to read with the font size and font color

  • #36 Miguel Grinberg said 2019-08-10T20:47:57Z

    @Len: you can make the font larger. All browsers have the option.

  • #37 nathan said 2019-08-19T01:12:29Z

    Hi Miguel, I console logged the result of the elem of the hover jQuery function, and it seems that elem and elem.first() both return the same object, the element. But when I compare them and console log the result, it is false. I looked up the definition of .first() of jQuery, it seems that it is not returning the first child element, but rather the first element of a group that is returned.

    I wonder why it was like this, I am really confused..

  • #38 Sebastian said 2019-09-18T01:12:56Z

    Hi Miguel, I have a value (from a timer, for the duration of playing a game) I want to pass it back to the server-side and redirect to a new side. That does not work. Hence, I was wondering if I can pass this JS data to a Flask Form to hande the request and redirect. Is that possible? I tried out a couple of things, but no luck. Thanks!

  • #39 Miguel Grinberg said 2019-09-18T10:39:50Z

    @Sebastian: You can pass this data with an Ajax request, but the redirect will have to be done from JS once you get the success callback for the request. Alternatively, you can have a hidden form in your page and trigger the form submission by issuing a submit button click. In that case the request will be issued in the foreground by the web browser, and the response from it will be interpreted, so a redirect response should work.

  • #40 Miguel Grinberg said 2019-09-22T03:09:53Z

    @nathan: You are correct, my description of the first() function was incorrect. I have updated this article with a correction, please see the update above.

  • #41 Raj said 2020-03-19T20:22:21Z

    I tried it for my web page, where on hover i want to display the link details. It worked but mouse out event is not destroying the content. Upon debugging i find that this code part elem.popover('destroy'); is not working. I am not sure if this is the case or something else. Any clue why it is not working.

  • #42 Miguel Grinberg said 2020-03-21T23:39:56Z

    @Raj: check the documentation for the Bootstrap version that you are using. This tutorial uses version 3.3.7, which does have the destroy. I believe Bootstrap 4 uses "dispose" instead of "destroy", if I remember correctly.

  • #43 Alessandro said 2020-06-15T10:29:46Z

    Hi Miguel, Thanks for putting together such a great tutorial. I had a quick question about the "Follow" hyperlink that appears within the user popup. When clicked, this returns a 405 error code (Method not allowed). If I look at the Flask logs I can see that this is because '/follow/<user>' is called using GET instead of POST, since this link is generated by <a href>. How would I force the request to be made as a POST? I tried using the approach taken for the Follow button in 'user.html' but this requires a form to be passed in when generating the page, which doesn't seem appropriate here. Thanks!

  • #44 Miguel Grinberg said 2020-06-15T15:42:39Z

    @Alessandro: sorry, this is my mistake. I have made some updates to the early parts of this tutorial a few days ago, specifically in the way follow and unfollow links are shown. I forgot to update the popup as well. I'll update it shortly. Sorry for the inconvenience!

  • #45 Shyam said 2020-06-27T20:07:05Z

    Hi Miguel,

    Thanks for a brilliant tutorial. I have quick question regarding {% set user_link %} {% endset %} assignment.

    In _post.html when I use {% set user_link %} <span class="user_popup"> <a href="{{ url_for('user', username=post.author.username) }}">{{ post.author.username }}</a> </span> {% endset %} ,

    {{ post.author.username }} is not getting displayed. When I take out {% set user_link %}, it is getting displayed again, do you know any reason why that might be?

    Thanks in advance for your help

    Shyam

  • #46 Miguel Grinberg said 2020-06-27T22:50:18Z

    @Shyam: the {% set user_link %} just sets a variable, it is not intended to show this variable. You have to use this user_link variable later in the template to show its contents. The line that follows the {% endset %} should use this variable to build the username as a link.

  • #47 Tom said 2020-07-23T16:34:34Z

    Hi Miguel,

    My popup is appearing and disappearing correctly, but it is not displaying the content retrieved from the ajax call. I have tried replacing the content option with just a a string to see if that displays correctly and it does.

    The debugger indicates the ajax call is getting the info correctly and it is stored as 'data' but I am just getting an empty popup displayed.

    Do you know why this may be? I am using Bootstrap 4. so could this potentially be interfering?

  • #48 Miguel Grinberg said 2020-07-23T22:05:41Z

    @Tom: difficult for me to say. You need to debug this on your side. If you are getting the data, but the data does not appear inside the popup, then this data must be incorrectly applied. A combination of logging to the console plus checking the Bootstrap 4 docs should help figure this out.

  • #49 Tom said 2020-07-24T21:49:25Z

    For anyone using Bootstrap 4 and encountering this issue, it is because BS4 has a built-in sanitizer for popovers that doesn't like tables for some reason. (https://getbootstrap.com/docs/4.3/getting-started/javascript/#sanitizer)

    It can be deactivated by adding "sanitize: false" as an attribute of the popover code. (https://stackoverflow.com/questions/55362609/bootstrap-4-3-1-popover-does-not-display-table-inside-popover-content)

    Thanks Miguel for this great tutorial.

Leave a Comment