Dojo - destroying tooltip widget when DOM node is destroyed - javascript

I use dijit/tooltips on a page that has a lot of domConstuct.destroy() and domConstuct.place() going on. So each time I remove some nodes from the DOM, I want to remove tooltips attached to those nodes. Currently the number of tooltip widgets is constantly growing on my page.
Is there a way to automatically remove a widget when corresponding DOM node is removed, or to check if existing tooltip widget's connect DOM node still exists?

You can attach a single Tooltip widget to multiple nodes at once, this may be the solution for you as you don't have to "manage" your tooltips anymore then. There's only one tooltip widget created for all tooltips, so you don't have to destroy it anymore.
The best way to achieve this is by using the selector property as described in the reference guide.
new Tooltip({
connectId: "myTable",
selector: "tr",
getContent: function(matchedNode){
return matchedNode.getAttribute("tooltipText");
}
});
If they don't have a common connectId and/or selector, then you can still use a single tooltip by adding the target to the same tooltip instance by using the addTarget() function.
To remove a target you can also use removeTarget() which accepts a DOM node (so you just pass the DOM node you want to remove).
If neither of these solutions is able to help you I'd like to know how you instantiate your tooltips, there are multiple ways to do that. For example by using connectId or by creating an ad hoc tooltip using the show() function.

I found a solution to my problem with a help of Dimitri's answer. I don't create separate Tooltip widget for each tooltip any more, now I put all the tooltips in one Tooltip using it's .addTarget() method. The second part of the solution is iterating through Tooltip's connectId property and checking if the DOM node still exists. I had to do it using Javascript native methods .contains() and .getElementById(), because Dojo's dom.byId() and query() gave me false positives. So, my code now looks like this:
// creating Tooltip
var tooltips = new Tooltip({
getContent: function(matchedNode){
return matchedNode.getAttribute("tooltiptext");
}
});
// adding tooltips
tooltips.addTarget(nameNode);
// deleting sufficient connects
for(var i = tooltips.connectId.length -1; i >= 0 ; i--){
if(!document.contains(tooltips.connectId[i]) && !document.getElementById(tooltips.connectId[i])){
tooltips.removeTarget(tooltips.connectId[i]);
}
}
The reason I had to use both .contains() and .getElementById() is that some of the nodes I attached tooltips to have ids and some don't, and Tooltip widget stores some of them as strings (id) and some as DOM nodes.

Related

Clone and restore "tooltiped" elements

I'm in trouble with restoring DOM structure that has elements passed to Bootstrap's .fn.tooltip() method.
To be specific: $('footer p') is passed to tooltip on document ready event, like this:
$(function(){
$('footer p').tooltip();
$('footer p').on('click', function(){
console.log('Just to test events')
});
})
I check it out, tooltip works, on click console message appears. Now I take backup of what am I about to delete and delete it, from console, by calling function:
function experiment_destroy() {
window.backup = $('footer').clone(true, true);
$('footer p').remove();
}
as expected, footer's p disappears.
Now I restore what is cloned and cached in window.backup variable with:
function experiment_restore(){
$('footer').empty();
$('footer').replaceWith(window.backup);
}
also called from console and here's what happens:
footer p element is back as it should be
footer p on click produces console message 'Just to test events'
message, so this event is restored along with element
no tooltip is restored.
Even if I re-call tooltip method in function experiment_restore I get nothing. Does anyone have some idea?
UPDATE:
I've made one more variation. Tried with different - totally minimal DOM environment with just p for tooltip and parent container element. Results are the same. Definitely there isn't just something in my complex DOM structure that was messing things up.
Here is very simple Fiddle.
You need to call the tooltip() method again. Optionally you should destroy the tooltip before cloning / removing the item for cleaning up the data.
Working Fiddle
$('footer p').tooltip();
$('#destroy').click(function(){
// optionally remove bindings
$('footer p').tooltip('destroy');
window.backup = $('footer').clone();
$('footer p').remove();
})
$('#restore').click(function(){
$('footer').replaceWith(window.backup);
// apply tooltip again
//window.backup.find("p").tooltip();
$('footer p').tooltip();
});
For the scenario you've shown in your question, I would use $().detach() to remove it from the DOM while at the same time keeping the event handlers and the data added to it with $().data() intact. In terms of the fiddle you've put in the question:
$('#destroy').click(function(){
var $footer_p = $('footer p');
window.backup = $footer_p;
$footer_p.detach();
})
$('#restore').click(function(){
var $footer = $('footer');
$footer.append(window.backup);
});
Here's an updated fiddle
What happens behind the scenes is that Bootstrap uses $().data() to add a JavaScript object of class Tooltip to your DOM element, and adds a bunch of event handlers. You need to preserve these.
If for some reason, you cannot use $().detach(), then you would have to recreate the tooltip by calling $().tooltip().
Why is $().clone(true, true) not working?
You call $().clone() with parameters to deep clone the DOM hierarchy and preserve the event handlers and the data set with $().data() so why is it not working? Is it not the case that the clone should have a reference to the Tooltip object created by Bootstrap?
Yes, the event handlers are preserved, and the clone does have a reference to the Tooltip object. However, this object it itself not cloned. More importantly, it is not adapted to refer to the new DOM node created by $().clone(). (So even if jQuery would clone it, it would still not work.) It does receive the event that would trigger the tooltip but Tooltip.prototype.show performs this check:
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
The inDom variable will be true if this.$element is in the DOM. However, this refers to the original element for which the tooltip was created, not the clone. Since that element is no longer in the DOM, then inDom is false and the next line returns, so the tooltip is never shown.
For giggles, take a clone of a DOM element on which you created a Bootstrap tooltip, do not remove the original element but add the clone somewhere else on the page. Then trigger the tooltip on the clone. The tooltip will appear on the original element. :)
What I described above is the general way Bootstrap's jQuery plugins work: they use $().data() to add JavaScript objects to the elements on which they operate. There's also a Dropdown class for dropdowns, a Modal class for modals, etc.
As an added answer, I used JQuery's clone method, but I copied all event listeners like .clone(true, true). My issue was that the tooltips were the exact same from the old and cloned elements, but they were in different positions (so hovering over the new one would show a tooltip in the top left corner of my browser).
The easiest fix I could think of that should work for all Javascript, Bootstrap, JQuery forever is:
const target = document.getElementById("random-div-you-want-to-clone-to")
$("selecting").clone(true, true).appendTo(target);
// if you only want .innerHTML of $("selecting")
// you can do $("selecting").children()
const _tooltips = target.querySelectorAll("[data-toggle='tooltip']");
for (const x of _tooltips) {
const build = x.cloneNode(true);
$(build).tooltip();
x.parentNode.replaceNode(build, x);
}
So the .clone(true, true) will grab all the event listeners, including "mousedown" which is the listener for the tooltips. When you use native ECMAScript's cloneNode method, you aren't getting the event listeners, so you need to reset the tooltips.
It's not the most efficient, but I had been working on this for an hour trying to think of something... be my guest in find a more efficient way because this method isn't, but it works. (e.g. use forEach, simply using JQuery directly, etc.).
Edit: you can also use .children() to get the innards of $("selecting") (i.e. its children) when cloning rather than getting it AND its children.
For those who use Bootstrap 4, the method $.tooltip("destroy") was replaced by $.tooltip("dispose")

How should I bind an event to DOM elements created with a JsRender custom tag?

Right now, I'm binding events to the parent element of my custom tag's rendered content, then using classes to target the event onto the element which my custom tag actually renders. I feel this is likely to cause strange bugs. For instance, if anyone on my team places two custom tags using the same targeting-classes under the same immediate parent element, it would cause multiple events to fire, associated with the wrong elements.
Here's a sample of the code I'm using now:
$.views.tags({
toggleProp: {
template: '<span class="toggle">{{include tmpl=#content/}}</span>',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
$(this.parentElem).on('click', '.toggle', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
$(this.parentElem).off('click', '.toggle');
}
}
// ... other custom tags simply follow the same pattern ...
});
By the time we hit onAfterLink, is there any reliable way to access the rendered DOM Element (or DOM Elements) corresponding to the custom tag itself? With no risk of hitting the wrong element by mistake? I understand that the custom tag may be text without an HTML Element, but it would still be a text node, right? (Could I even bind events to text nodes?)
In other places, and using (far) older versions of JsViews, I've bound events after the render using (sometimes a lot of) targeting logic built into the rendered elements as data- attributes. Not only is this a far more fragile method than I like for accessing the rendered data, it would be incredibly risky and convoluted to try to apply this approach to some of our deeply-nested-and-collection-ridden templates.
I also don't like needing to insert a span with my custom tag, just so I can apply classes to it, but if it's still necessary for the event, I'll cope.
I ask, then, what is a safe, modular way to bind events to the DOM so that I also have access to the data rendered directly against those elements?
Edit: As an additional concern, using onAfterLink won't let me bind events to non-data-linked rendered content. This may be part of the design intent of JsViews vs pure JsRender, but I don't yet understand why that would be the case.
Rather than using this.parentElem, you can use
this.contents()
which is a jQuery object containing all immediate content elements within the tag.
You can also provide a selector argument,
this.contents("someselector")
to "filter" , and include an optional boolean "deep" flag to both "filter" and "find" - i.e.
this.contents("someselector", true).
Using the above APIs ensures you are only taking elements that are actually within the tag content.
You may not need to remove the handlers in onDispose, if the tag is only deleted along with its content, you can rely on the fact that jQuery will dispose handlers when the elements are removed from the DOM.
You can only attach events to elements, not to text nodes. So if your content does not include elements, you would need to add your wrapper element, but not otherwise.
$.views.tags({
toggleProp: {
template: '{{include tmpl=#content/}}',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
this.contents().on('click', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
this.contents().off('click');
}
}
});
Also take a look at samples such as http://www.jsviews.com/#samples/tagcontrols/tabs which use the above approach.

Handle stuff after dom changes

I've got a page with some Javascript / jQuery stuff, for example:
(function()
{
$('.tip').tooltip();
$('.test').click(function()
{
alert('Clicked!')
});
}();
On the page I insert some HTML with jQuery so the DOM changes. For example, I insert a extra element with the class "tip" or "test". The just new inserted elements doesn't work because jQuery is working with the non-manipulated DOM and the just inserted elements aren't there. So I've searched around and came to this solution for the "click":
$('body').on('click','.click',function()
{
alert('Clicked!')
});
I don't understand why, but this way it's working with the manipulated DOM and the jQuery stuff works on the new inserted elements. So my first question is, why does this work and just the click() function not? And the second question, why do I have to point to the "body"?
Finally, my third question is, how get this done with the tooltip?
I know that there is so many information about this subject (previous the delegate() and live() function I've read) but I can't found a explanation about it. And I can't get my third question solved with the information I found.
I'm looking forward to your responses!
Extra question:
4) Is it recommended to point always to the "body" for this kind of situations? It's always there but for possible performance issues?
So my first question is, why does this work and just the click()
function not?
Because the event handler is now delegated to a parent element, so it remains even after replacing/manipulating child elements.
Ancient article on event delegation for your perusal - but the concepts remain the same:
http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
And the second question, why do I have to point to the "body"
You don't, any suitable parent element will do. For example, any direct parent (a div wrapper, for instance) which does not get replaced.
Finally, my third question is, how get this done with the tooltip?
You need to re-initialize your tooltip plugin on the newly inserted elements. For example:
$.get("foo.html", function (html) {
$("#someDiv").html(html);
$("#someDiv").find(".tip").tooltip();
});
The click() event doesn't work when you manipulate the DOM because JQuery is not watching for DOM changes. When you bind the click() event it is selecting the elements that are on the page at that time. New ones are not in the list unless you explicitly bind the event.
Because you have pointed the click() event on the body. JQuery then checks to see if the target of the click matches any of the event handlers (like what you have created) match the element clicked. This way any new elements will get the event 'associated' with them.
Because the tooltip isn't an event that you can place on the body, you will need to re-initialize it when the element is created.
EDIT:
For your fourth question, is it depends. The advantage of binding to the body is that you don't accidentally bind an event to an element more than once. The disadvantage is that you are adding event handlers that need to be checked on each event and this can lead to performance issues.
As for your concerns about DRY, put the initialization of the tooltips into a function and call that when you add them. Trying to avoid having the same function call is a little overkill in this regard, IMO.
Events are bound to the specific object you are binding it to.
So something like $('.tip').tooltip() will perform the tooltip() functionality on $('.tip') which is actually just a collection of objects that satisfies the css selector .tip. The thing you should take note of is, that collection is not dynamic, it is basically a "database" query of the current page, and returns a resultset of HTML DOM objects wrapped by jQuery.
Therefore calling tooptip() on that collection will only perform the tooltip functionality on the objects within that collection, anything that was not in that collection when tooltip is called will not have the tooltip functionality. So adding an element that satisfies the .tip selector, after the tooltip() call, will not give it the tooltip functionality.
Now, $('body').on('click','.click', func) is actually binding the click event to the body tag (which should always exist :P), but what happens is it captures whether the click event has passed through an element your target css selector (.click in this case), so since the check is done dynamically, new elements will be captured.
This is a relatively short summary of what's going on... I hope it helped
UPDATE:
Best way for your tooltip thing is to bind tooltip after you have added elements, e.g.
$('#container').load('www.example.com/stuff', function() {
$('.tip', $(this)).tooltip();
});

jQuery: DOM Elements: Attributes: query & create

I have a few questions about jQuery, relating to attributes:
Is there a jQuery or DOM API call that I could use to list or clone all of the attributes of a DOM element. The jQuery.attr() API call lets you do it if you know the name of the attribute, but if you don't, is there a way?
Is there a jQuery or DOM API call that I could use to create a new CSS rule, besides the obvious one of dynamically loading a new script?
It seems possible because when I open up the JS debugger in Google Chrome using CTRL-Shift-J, and click on a DOM element in the elements pane, I can see all of the attributes of the element without having to ask for them by name.
Clone the whole object, that will replicate all attributes as well, http://api.jquery.com/clone/
Add new style section into the head element using append, http://api.jquery.com/append/
The other guys are right that you should use clone - but if you want to list the attributes, you can do it like so:
for(attr in $('#selector')) {
console.log(attr);
}
I don't think you can create a new css rule, but you can do pretty much the same by attaching css to a selector:
$('#selector').css({
display: 'block'
});

In Greasemonkey/javascript, how can I handle new elements added to page?

I have written a Greasemonkey script which manipulates the contents of certain elements with the following selector:
$("span.relativetime").each(function() { $(this).html("TEST"); });
However, sometimes matching elements are added to the page through AJAX, and I don't know how to handle those new elements. I have tried this, but it doesn't work:
$("span.relativetime").live(function() { $(this).html("TEST"); });
The documentation for jQuery live() says that it wants an event (like "click"). But I don't have any event, I just want to know when something matching my selector has been created, and then I want to modify it.
Background: I am encountering this problem with a Greasemonkey script to display StackOverflow's relative timestamps as absolute local timestamps, which you can find on meta-SO. The problem is when you click "show all comments", the new comments are added by AJAX, and I don't know how to find and replace the timestamps in those scripts.
With StackOverflow's setup I find it annoying to handle stuff after the comments. What I've done is put a bind on the Add/Remove comments button that uses setTimeout to wait for the elements to be created, and then modify them.
One thing you could try (although I'm not sure if it would work) is to cache your selection in some global variable like so:
var $relativetime = $("span.relativetime");
Then you would have your .each function:
$relativetime.each(function() { $(this).html("TEST"); });
After your new elements were added to the DOM, you could reselect append to your cached object:
$relativetime.append("<my html>"); //or
$("<my html>").appendto($relativetime);
(P.s. .html() is for setting html. To set text, use .text()

Categories