Created | ![]() |
Favourites | Opened | Upvotes | Comments |
1. Apr 2020 | 2 | 0 | 347 | 0 | 0 |
May 2020. Bootstrap popover is among the most useful & used widget in web development - here is a list of common popover issues, their reasons and how to solve bootstrap 4 popover problems.
Index :
Let's say we have a list of recipe names and we want to dynamically load a preview of a recipe then the mouse is hovering over the recipe name.
To keep all the javascript code together, I'll build a Recipe object having the following 4 members :
Each recipe name should trigger a popover and the content should be dynamically loaded, therefore we set the content property to a function that will make make an ajax request to the server appended a recipeId. The server (not shown here) will fetch some data a recipe preview can be build from and send these data back to the xmlHttpRequest object in the browser and these data will then be used to create a recipe preview from and the preview UX will then be added to the popover content div.
var Recipes = {
Cache: {},
initializeRecipesPopovers: function(options) { // let's set a few options
var cssTrigger = options.cssTrigger; // all elements with this class will have a popover
var mouseTrigger = options.mouseTrigger; // hover or click
var popoverPosition = options.popoverPosition; // top, right, bottom or left
$('.' + cssTrigger).popover({
trigger: mouseTrigger,
placement: popoverPosition,
html: true,
content: function () {
var elmTrigger = $(this);
var recipeId = $(this).attr('data-recipeId');
var divContent = document.createElement("div"); // html fragment to return
if (Recipes.Cache[recipeId]) { // if recipePreview is already cached no need to request it from the server
divContent.appendChild(Recipes.Cache[recipeId]);
}
else { // if not cached (typically first time it is requested), we present an ajax loader image and start fetch the recipe preview from the server
var imgLoader = document.createElement("img"); // first add an ajax loader image
imgLoader.src = "/path/to/loader.png";
divContent.appendChild(imgLoader)
Recipes.getRecipePreview(divContent, elmTrigger, recipeId, (divContent, elmTrigger, recipeId, apiResult) => { // recipeId is passed to getRecipePreview to get a preview for the current recipe, however recipeId is then passed back to the callback function
/* this callback function will be called by getRecipePreview then data have arrived */
if (apiResult.error) { // the .error property is either created on the server (if the server catched an error) or created below in $.ajax (in case of a non-catched error)
divContent.innerHTML = apiResult.error;
}
else { // no error
divContent.innerHTML = ""; // remove the ajax loader image
var previewUX = Recipes.drawRecipePreview(apiResult.data) // the .data property is created on the server and contains all the recipe data necessary to create a recipe preview
divContent.appendChild(previewUX);
// re-position the popover after it have been finally created with the asyncronously loaded data
setTimeout(function () { // be sure all other popover things have happened by delaying the repositioning a few milliseconds
$(elmTrigger).popover('show');
}, 50);
}
});
}
return divContent;
}
});
},
getRecipePreview: function (divContent, recipeId, callback) {
$.ajax({
url: '/http/endpoint/url?recipeId=' + recipeId,
method: 'GET',
dataType: 'json',
success: function (apiResult) { // ok-200 response and the response body here called apiResult (apiResult is a string that by design as created on the serverside can be evaluated as a JSON object, which happens automatically by $.ajax)
callback(divContent, recipeId, apiResult);
},
error: function (xmlHttpRequest, textStatus, errorThrown) { // non-catched error
callback(divContent, recipeId, { error: errorThrown }); // create a literal object with a property called error to represent apiResult
}
});
},
drawRecipePreview: function (data) { // data is transferred from the server and it should have some relevant data for creating the recipe preview
let recipeId = data.recipeId;
let name = data.name;
let description = data.description;
let rating = data.rating;
let commentCount = data.comments.Count;
var divRecipePreview = document.createElement("div");
//create some preview UX using the data
return divRecipePreview; // return the preview UX
}
}
Recipes.initializeRecipesPopovers({ // set preview popover on relevant elements
cssTrigger: 'recipe', // all elements with the class 'recipe' are relevant
mouseTrigger: 'hover',
popoverPosition: 'right'
});
If the popover trigger is set to 'hover', the popover will hide then the trigger element gets a mouseout, however often we want to keep the popover open if the mouse enters the popover.
I know of 2 different methods to achieve keeping the popover open on hovering :
We can achieve that by making the trigger element itself the container of the popover, that is: making the popover a child of the trigger element - the trigger element will then NOT fire the mouseout then visually leaving the trigger element as long as the mouse is hovering the popover.
$('selector').each(function() {
$this = this;
($(this)).popover({
trigger: 'hover',
placement: 'left',
container: $this
});
});
Elegant as this solution is, there are a few important caveats :
<div class="popover">
<a href="..." style="display:block;">...</a>
</div>
I know of 2 situations that leads to the wrong position of popover problem :
First time a popover loads an image (that does not have width & height explicitly set), the popover can be created and positioned before the image is loaded. Then the image is eventually loaded, the popover will dynamically change it's size but not it's position - so if the the popover is placed 'left' or 'top', the popover will expand over the triggering element.
However, second time the popover is created, the image will typically have been cached by the browser and therefore the popover size will be correctly calculated and in turn the position of the popover will be correctly calculated - the image problem typically happens only the first time the popover is shown.
Popover image problem can be solved 3 ways :
.popver img {
width: 160px;
height: 160px;
}
Then the content of a popover is dynamically loaded, the popover will be created to fit either nothing or typically an ajax loader and then positioned according to that size. Then the content is eventually loaded, the popover may expand but the position is not recalculated - thereby if placed 'left' or 'top' expanding over the triggering element.
If you cache the dynamically loaded content in the users browser, any subsequent hovering of the trigger element will result in a correctly calculated size of the popover and therefore of the position, just like for the image problem and just like for the image problem, the dynamic problem is relevant only for placements of 'left' & 'top' (not for 'right' or 'bottom').
Popover dynamic content problem can be solved 2 ways :
If the popover do not have enough space, it may be pushed over the trigger element and this situation is different from the issue above with initial wrong position because if there are not enough space, the popover is not only initially wrongly positioned but indefinitely wrongly positioned.
So what happens is that then the popover is pushed over the trigger element, mouseout is raised on the trigger element removing the popover and as the popover is removed mouseover is raised on the trigger element - this loop results in flickering and will continue until the mouse is removed.
,
Popover flickering can be stopped by setting the CSS property pointer-events to none for the popover like this :
.popover {
pointer-events: none!important;
}
Note that the above CSS will catch all popovers because the bootstrap popover widget will add the .popover classname to all popovers.
Another way to avoid the flicker is to set the popover container to the trigger element itself, see next section, Keep popovers open on hovering, to explore that option.
I know of 2 reasons for the popover stacking issue :
Bootstrap popover default has a z-index of
If you have an element A with z-index 10 and another element B with z-index 5, then all descendants of A will belong to a different stacking context than B and in case of overlap descendants of A will ALWAYS be stacked higher than descendants of B no matter of the z-index of individual descendants.
So if your popover trigger element is a descendent of element B, the popover itself may be attached to the DOM in different ways :
Typically you can fix the problem by setting the popover container to 'body':
$('selector').popover({
trigger: 'hover',
container: 'body',
...
});