This is the twentieth edition of the Flask Mega-tutorial series, in which I'm going to add a pop-up window that appears when you hover the mouse over a username.
Under the spoiler is a list of all articles in the 2018 series.
Note 1: If you are looking for old versions of this course, this is here .
Note 2: If suddenly you would like to speak in support of my (Miguel) work, or simply do not have the patience to wait for the article for a week, I (Miguel Greenberg) offer the full version of this manual (in English) in the form of an electronic book or video. For more information, visit learn.miguelgrinberg.com .
Currently, it is not possible to create a web application that would not use at least a little JavaScript. I'm sure you know that JavaScript is the only language that was originally designed to work in web browsers. In Chapter 14, you saw the use of unpretentious JavaScript linking in a flask template to provide real-time language translations of blog posts. In this chapter, I'm going to delve into the topic and show you another useful JavaScript trick to make the application more interesting and attractive to users.
A common feature of the user interface for social networking sites, in which users can interact with each other, is the display of a brief user summary in a pop-up panel when you hover the mouse over a user name anywhere on the page. If you have never paid attention to it, go to Twitter, Facebook, LinkedIn or any other major social network, and when you see the username, just leave the mouse pointer on top of it for a couple of seconds to see a pop-up window. This chapter will be devoted to the creation of this function for Microblog, an example of which you can see in the screenshot below:
GitHub links for this chapter: Browse , Zip , Diff .
Before we dive into the client side, let's work a little with the server to understand what is needed to support these custom pop-ups. The contents of the user pop-up window will be returned by the new route, which will be a simplified version of the existing profile profile route. Here is the function, view:
app / main / routes.py : User popup 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 will be attached to the URL / user / <username>
/ popup and simply load the requested user, and then display a template with its data, which is a shortened version of 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 write in the following sections will refer to this route when the user hovers over the username. In response, the server returns the HTML content for the pop-up window, which then displays the client part. When the user moves the mouse, the popup will be removed. Sounds easy, right?
If you want to see what the pop-up window will look like, you can launch the application, go to any user's profile page, and then add / popup to the URL in the address bar to view the full-screen version of the pop-up content.
In chapter 11, I introduced you to the Bootstrap framework as a convenient way to create beautifully formatted web pages. So far, I have used only the minimum part of this framework. Bootstrap comes bundled with many common user interface elements, all of which have demos and examples in the Bootstrap documentation at https://getbootstrap.com . One of these components is Popover , which is described in the documentation as "small invoice content, for posting additional information." Exactly what I need!
Most bootstrap components are defined using HTML markup, which refers to CSS Bootstrap definitions that add a formatting style. Some of the most advanced also require javascript. The standard way that an application includes these components on a web page is to add HTML in the right place, and then for components that need scripting support, invoke a JavaScript function that initializes or activates it. The popover component requires javascript support.
The HTML part for creating a popover is very simple, you just need to define the element that will cause the popover to appear. In my case, it will be a clickable username that appears in each blog post. In the app / templates / _post.html sub-template, the username is already defined:
<a href="{{ url_for('main.user', username=post.author.username) }}"> {{ post.author.username }} </a>
Now, in accordance with the popover documentation, I need to call the popover () JavaScript function for each link like the one above on the page, and this will cause the popup to be initialized. The initialization call accepts several parameters that set up a pop-up window, including parameters that send content to be displayed in a pop-up window, which method to use to cause a pop-up window to appear or disappear (click, hang over an item, etc.) if the content is is a simple text or HTML, and a few more parameters that you can see on the documentation page. Unfortunately, after reading this information, I had more questions than answers, because it seems that this component is not designed to work as I need it. Below is a list of problems that I need to solve in order to implement this function:
data-content
attribute added to the target HTML element, so when the hover event fires, all Bootstrap needs is a pop-up window. This is terribly inconvenient for me, because I want to create an Ajax call to the server in order to receive content, and only when the server response is received should a pop-up window appear.In fact, this is not so rare when working with browser-based applications that quickly become more complex. You need to think deeply enough about the dependencies of the interaction of DOM elements with each other and make them behave in such a way that the user is left with only pleasant impressions.
It is clear that I will need to run the JavaScript code immediately after loading each page. The function I'm about to launch will search for all references to usernames on the page and configure those that have the popover component from Bootstrap.
The jQuery javascript library is loaded as a dependency on Bootstrap, so I'm going to use it. When using jQuery, you can register a function that will be launched when the page is loaded by wrapping it in $(...)
. Perhaps this should be added to the app / templates / base.html template so that it runs on each page of the application:
app / templates / base.html : Run the function after the page loads.
... <script> // ... $(function() { // write start up code here }); </script>
As you can see, I added the start up function to the <script>
element, in which I defined the translate()
function in Chapter 14 .
My first task is to create a JavaScript function that will find all the links on the page. This function will be triggered when the page has finished loading, and when it is completed, it will configure the hang and popup behavior for all of them. Now I will focus on finding links.
If we recall from Chapter 14 , the HTML elements that were involved in live translations had unique identifiers. For example, the attribute id="post123"
was added to the entry with ID = 123. Then, using jQuery, the expression $('#post123')
was used in JavaScript to find this element in the DOM. The $()
function is extremely powerful and has a fairly complex query language for finding DOM elements based on CSS Selectors .
The selector that I used for the translation function was designed to find one particular element that had a unique identifier set as the id
attribute. Another way to identify elements is through the class
attribute, which can be assigned to several elements on the page. For example, I could mark all user references with class="user_popup"
, and then get a list of links from JavaScript using $('.user_popup')
(in CSS selectors, the prefix #
searches by ID, while .
Search for prefixes by class ). The return value in this case will be a collection of all elements that have a class.
After playing with examples of popover in the Bootstrap documentation and checking the DOM in the browser debugger, I determined that Bootstrap creates the popover component as a child element of the target element in the DOM. As I mentioned above, this affects the behavior of the hover event, which will display a “mouse” as soon as the user moves the mouse pointer from the <a>
link and into the popup window itself.
The trick that I can use to extend the hover event to enable popover is to make the popover a child of the target element and then hover guidance is inherited. Looking through the popover options in the documentation, I found that this can be done by passing the parent element to the container
option.
Creating a popover child element from hover will work well for buttons or common <div>
or <span>
elements, but in my case, the target for popover is an <a>
element that displays a link to the username. The problem with creating a popover <a>
child element is that the popover will then get the behavior of the parent <a>
reference. The end result will be something like this:
<a href="..." class="user_popup"> username <div> ... popover elements here ... </div> </a>
To avoid popover inside the <a>
element, we’ll use another trick. I'm going to wrap the <a>
element inside the <span>
element, and then bind the hover and popover event to <span>
. The resulting structure will be as follows:
<span class="user_popup"> <a href="..."> username </a> <div> ... popover elements here ... </div> </span>
The <div>
and <span>
elements are invisible, so they are excellent elements to help you organize and structure the DOM. The <div>
element is a block element , similar to a paragraph in an HTML document, while the <span>
element is a string element that is comparable to a word. For this case, I decided to use the <span>
element, since the <a>
element that I wrap is also a string element.
Now we need to reorganize my app / templates / _post.html subpattern 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're wondering where the HTML popover elements are, then the good news is that I don’t need to worry about it. When I get a call to the popover()
initialization function in the <span>
elements I just created, the Bootstrap environment will dynamically insert a popup component.
As I mentioned above, the hover behavior used by the popover component of Bootstrap is not flexible enough to meet my needs, but if you look at the documentation for the trigger
option, “hover” is just one of the possible values. My view was attracted to the “manual” mode, in which the popover can be displayed or removed manually by invoking JavaScript. This mode will give me the freedom to implement the hang logic, so I'm going to use this parameter and implement my own hover event handlers, which work the way I need them.
So my next step is to attach the “hover” event to all links on the page. Using jQuery, the hover event can be bound to any HTML element by calling element.hover (handlerIn, handlerOut)
. If this function is called in a set of elements, jQuery conveniently attaches an event to all of them. Two arguments are two functions that are called when the user moves the mouse to and from 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 an event
object that contains useful information. In this case, I retrieve the item that was the target of the event using event.currentTarget
.
The browser sends a hover event immediately after the mouse enters the area of ​​the influencing element. In the case of a pop-up window, I would like the event to be activated only after waiting a short period of time when the mouse is delayed on the element, so that when the mouse pointer briefly passes over the element, but there are no instant pop-up blinking windows on it. Since the event does not come with delay support, this is another thing that I am going to implement myself. Probably you need to add a 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 var elem = event.currentTarget; timer = setTimeout(function() { timer = null; // Popup }, 1000); }, function(event) { // mouse out 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 called after a specified delay. So I added a function that is still empty, which will be called a second after sending the hover event. Thanks to the closures in the JavaScript language, this function can access variables defined in the external scope, such as elem
.
I store the timer object in the timer variable, which I defined outside of the hover()
call to make the timer object also accessible to the mouse out handler. The reason why I need it is, once again, to leave a pleasant impression to the user. If the user moves the mouse pointer to one of these user links and remains on it, but say, half a second before it triggers, the mouse pointer moves, in this case I do not want the Timer to read my delay and call the function that will display the pop-up window. Therefore, my mouse out event handler checks if there is an active timer object, and if so, cancels it.
Ajax requests are not a new topic, as I already mentioned in Chapter 14 , as part of a living translation language. When using jQuery, the $.ajax()
function will send an asynchronous request to the server.
The request that I am going to send to the server will have the URL / user / <username>
/ popup , which I added to the application at the beginning of this chapter. The response from this request will contain the HTML that I need to insert into the popup.
My immediate problem regarding this request is to know what the username
value I need to include in the URL. The mouse in event handler function is universal, it will work for all user links that are on the page, so the function must determine the user name from its context.
The variable elem
contains the target element from the hover event, which is a <span>
element that wraps the <a>
element. To retrieve the username, I can navigate the DOM, starting at <span>
, going to the first child element, which is the <a>
element, and then extracting the text, which is the username, that I need to use in my URL. With jQuery DOM bypass functions, this is not difficult:
elem.first().text().trim()
The first()
function applied to the DOM node returns its first child element. The text()
function returns the text content of the node. This function does not trim the text, so, for example, if <a>
is on one line, text is on the next line and </a>
on another line, the text()
function will return all spaces that surround the text. To remove all spaces and leave only text, I use the JavaScript trim()
function.
And this is all the information that I need to be able to issue a request to the server:
app / templates / base.html : XHR request.
$(function() { var timer = null; var xhr = null; $('.user_popup').hover( function(event) { // mouse in var elem = $(event.currentTarget); timer = setTimeout(function() { timer = null; xhr = $.ajax( '/user/' + elem.first().text().trim() + '/popup').done( function(data) { xhr = null // } ); }, 1000); }, function(event) { // mouse out var elem = $(event.currentTarget); if (timer) { clearTimeout(timer); timer = null; } else if (xhr) { xhr.abort(); xhr = null; } else { // } } ) });
Here I have defined a new variable in the external scope, xhr
. This variable will contain an asynchronous request object, which I initialize from the $.ajax()
call. Unfortunately, when building a URL directly on the JavaScript side, I cannot use url_for()
from Flask, so in this case I must explicitly combine parts of the URL.
Calling $.ajax()
returns a promise (commitment), which is a special JavaScript object representing an asynchronous operation. I can attach a completion callback by adding .done (function)
, so my callback function will be called after the request is completed. The callback function will receive a response as an argument, which I called data
and you can see it in the code above. This will be the HTML content that I'm going to put in the popover.
But before we get to the popover, there is another detail related to providing the user with a convenient interface and good mood, which you need to take care of. Recall that I added logic to the "mouse out" event handler function to cancel a one-second timeout if the user moved the mouse pointer out of a <span>
. The same idea should be applied to an asynchronous request, so I added a second condition to interrupt my xhr
request object, if it exists.
It is a significant moment.
I can create a popover component,
Using only the data
argument
From the Ajax callback function returned to me:
app / templates / base.html : Displays a popup window.
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 a popup window is not so difficult, the popover()
function from Bootstrap will do all the work necessary to set it up. Parameters for popover are given as an argument. I set up this popover with a "manual" trigger mode, HTML content, without fading animation (so it appears and disappears faster), and I set the parent element as the <span>
element itself, so the hover behavior extends to the popover by inheritance . Finally, I pass the data
argument to the Ajax callback as the content
argument.
Returning a popover()
call is a newly added popover component that, for a strange reason, had another method, also called popover()
, which is used to display it. So I had to add a second popover('show')
call for a pop-up window to appear on the page.
"last seen" ( ), Flask-Moment, 12 . Flask-Moment Ajax, flask_moment_render_all()
.
mouse out. popover, , . , , , , popover('destroy')
.
app/templates/base.html : 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'); } }
Source: https://habr.com/ru/post/353804/