A recent change (https://github.com/twbs/bootstrap/commit/76762169c359b381f70fa518daef09e76b0aefe3) creates a situation where it is impossible to ensure that the destroying of a tooltip has been properly finished before re-initializing it. This creates a race condition where the newly initialized tooltip may (in my testing, in all cases) get destroyed by the previously called destroy call.
Observe:
$('.my-element').tooltip();
$('.my-element').tooltip('destroy');
$('.my-element').tooltip();
We will never see the tooltip because the destroy method actually removes data, etc only after the tooltip has been hidden, which happens asynchronously. This means that the second tooltip()
call finishes before destroy
, and this means that destroy
will remove the newly initialized tooltip data form the element.
I'm not sure what the best solution is, but an option would be to allow passing in a callback function to the destroy method, but I don't really like the idea of having to dive into async world when destroying a tooltip.
X-Ref: https://github.com/twbs/bootstrap/issues/15607#issuecomment-96273881
CC: @fat
Yep I agree this is a problem, but don't really think it's worth addressing in 3.+.
One work around would be to call hide yourself first and then remove the fade class from the tooltip - this would force destroy to be synchronous.
Not 100% the circumstances around why you are trying to destroy the tip and reintialize it. My hunch because they're coming sequentially like that is that you should probably just be using show/hide alone (maybe with manual triggers), but again it's hard to say without knowing more
@fat can you explain me further the work around? I'm having the same problem and I'm not being able to open a new popover without it being closed
@escobar5 here's how the mentioned work around should look like:
destroyTooltip: function ($targetElement) {
var bsData = $targetElement.data()["bs.tooltip"];
if (bsData && bsData.$tip) {
bsData.$tip.removeClass("fade");
}
$targetElement.tooltip("destroy");
}
When fade
class is removed, this condition will not pass and as a result destroy
will run synchronously. Tested in 3.3.7
.
Unfortunately there's a corner-case with this approach I recently encountered in my project - I started getting "Uncaught TypeError: Cannot read property 'trigger' of null".
This is due to the fact that complete-callback in enter
method can fire after tooltip is destroyed:
var complete = function () {
var prevHoverState = that.hoverState
that.$element.trigger('shown.bs.' + that.type)
that.hoverState = null
if (prevHoverState == 'out') that.leave(that)
}
that.$element
is null.
Here's full stack:
bootstrap.js:1491 Uncaught TypeError: Cannot read property 'trigger' of null
at HTMLDivElement.complete (bootstrap.js:1491)
at HTMLDivElement.fn (jquery.js:4825)
at HTMLDivElement.handle (bootstrap.js:72)
at HTMLDivElement.dispatch (jquery.js:5226)
at HTMLDivElement.$event.dispatch (jquery.event.drag.js:374)
at HTMLDivElement.elemData.handle (jquery.js:4878)
at Object.trigger (jquery.js:5130)
at HTMLDivElement.<anonymous> (jquery.js:5860)
at Function.each (jquery.js:370)
at jQuery.fn.init.each (jquery.js:137)
So it's a check needed in complete callback:
if (!that.$element) return
Most helpful comment
@escobar5 here's how the mentioned work around should look like:
When
fade
class is removed, this condition will not pass and as a resultdestroy
will run synchronously. Tested in3.3.7
.