I have a function to modify divs of a specific class, but the problem is these divs are being loaded and unloaded dynamically by a 3rd party WP plugin that I cannot modify, and there are no click actions I can bind to. These divs may appear and disappear at any given point in time, and they come inside a more complex set of elements. The plugin starts by loading one div on page load, and later, on click, it loads other divs into it. These inner divs are timed, and they disappear on timeout, loading another set of divs and so on. The ones I'm trying to modify don't come in the first set on click, so I can't bind to that click.
From my research here on StackOverflow and elsewhere I understand that the old method of using mutation events with something like DOMNodeInsertedis deprecated, and the mutation observer should be used, but I can't get my head around it.
I've tried to adapt two pieces of code I found in answers to other questions here, but it didn't work for me.
The logic I'm using is to watch the first wrapper div that gets loaded on page load .modal-survey-container, and then, whenever it gets new divs loaded into it, my function should fire if my actual target .msnumericanswer is present inside the subtree. So far I've tried this:
var $j = jQuery;
$j(document).ready(function callback(records) {
records.forEach(function (record) {
var list = record.addedNodes;
var l = list.length - 1;
for ( ; l > -1; l-- ) {
if (list[l].nodeClass === '.msnumericanswer') {
$j('.msnumericanswer').each(function(i, obj) {
var t = $j(obj).data('tooltip');
$j(obj).prepend('<div class="labels">' + t + '</div>');
});
console.log(list[l]);
}
}
});
setTimeout(5000);
});
var observer = new MutationObserver(callback);
var targetNode = document.querySelectorAll(".modal-survey-container");
observer.observe(targetNode, { attributes: true, childList: true, subtree: true });
This doesn't work and throws two errors in console saying:
ReferenceError: callback is not defined.
TypeError: records.forEach is not a function.
The alternative logic is to catch the creation of my target's parent div .survey_table, so I also tried this:
var $j = jQuery;
var myElement = $j('.survey_table')[0];
var observer = new MutationObserver(function(mutations) {
if (document.contains(myElement)) {
$j('.msnumericanswer').each(function(i, obj) { //my working jQuery
var t = $j(obj).data('tooltip'); //my working jQuery
$j(obj).prepend('<div class="labels">' + t + '</div>'); //my working jQuery
});
}
});
observer.observe(document, {attributes: true, childList: true, characterData: false, subtree:true});
This doesn't work either, but I see no errors inconsole.
What am I doing wrong here?
As I understand an ajax callback adds a new div to the html on my end. I can see this element added in the html however I can't access it through DOM for some reason. I tried calling it from the console in chrome and a js bookmarklet after the element was already visible/added to the html. I am not sure why the DOM is not picking it up but that's another question I guess...
This is the div:
<div class="recaptcha-checkbox-checkmark" role="presentation"></div>
And I used the below to grab a reference to it but 90% of the time it returns a null (occasionally, it actually returns a reference but I honestly don't know /understand why):
document.querySelector(".recaptcha-checkbox-checkmark");
I have been looking at different ways to bind to this new div so I can execute a click on it after it appears in the html/dom but I am having trouble putting this together as it seems you can bind to a click event but you can't bind to something like a shown event :/
So this is what I currently have but it doesn't work as (I guess) it's based on a click event and I am not actually clicking it?
var host = document.querySelector("head");
if(host){
var importJquery = function(){
var jquery = document.createElement('script');
jquery.setAttribute("src", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js")
host.appendChild(jquery);
}
importJquery();
var bindToCaptcha = function(){
var script = document.createElement("script");
script.innerHTML = '$(document).on("click", ".recaptcha-checkbox-checkmark", function(e) {alert("clicked: %o", this);});';
host.appendChild(script);
}
bindToCaptcha();
}
So just to be clear I want to identify the moment this div shows up in the html and execute a click on it but can't since I am missing a reference to it.
I was considering running a loop at an interval checking whether the div existed but I would rather like to stay away from that approach (and also I am not sure this would work because the DOM doesn't always seem to return a reference this div).
Any help greatly appreciated.
You can use the MutationObserver API to detect when a child is added to a parent element:
// Select the node that will be observed for mutations
var targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true };
// Callback function to execute when mutations are observed
var callback = function(mutationsList) {
for(var mutation of mutationsList) {
if (mutation.type == 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type == 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();
Example taken from https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
I couldn't get the reference because the element I was after was located within an iframe. I feel like this question would have been answered instantly by someone else had I mentioned the iframe in the first place but I am new to this so I wasn't even aware of it until I have done some further digging.
I am using mutation observer on a target node. It fires if any of the child nodes or properties change. But I have a case where I had to handle the target node itself being removed. This is not working. Is there a way to handle this?
MutationObservers can watch for 3 things:
An attribute change
A text content change
A child list change
Then optionally those change events can be bubbled up, so if you want to changes on both the target and all of the target's children, you can do that.
This means what you want cannot be done by listening to your target. You would need to attach an observer to the target's parent node, and listen for a childList change on the parent that removes the node you want to track.
e.g. instead of this fake example of what you were hoping for
var observer = new MutationObserver(callback);
observer(target, {
// Fake non-existent option
parent: true
});
you do
var observer = new MutationObserver(function(mutations){
var targetRemoved = mutations.some(function(mutation){
return mutation.removedNodes.indexOf(target) !== -1;
});
if (targetRemoved) callback();
});
observer(target.parentNode, {
childList: true
});
can-dom-mutate can do this:
import domMutate from "can-dom-mutate";
domMutate.onNodeRemoval(yourEleement, function(){
})
It does this by listening on the entire document for element removal.
Is there a way to fire a function when element loses some custom attribute? For example, when custom_attribute is removed, then show me some alert. What's the way to do it? Plain JS is preferable, although jQuery is also okay.
<div class="someclass" custom_attribute>...</div>
You can use MutationObserver:
// select the target node
var target = document.querySelector('.someclass');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
fire_function();
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true };
// pass in the target node, as well as the observer options
observer.observe(target, config);
// later, you can stop observing
observer.disconnect();
This fires the fire_function() every time an attribute is changed. So you can check, if the particular attribute is missing or changed.
I need to accurately measure the dimensions of text within my web app, which I am achieving by creating an element (with relevant CSS classes), setting its innerHTML then adding it to the container using appendChild.
After doing this, there is a wait before the element has been rendered and its offsetWidth can be read to find out how wide the text is.
Currently, I'm using setTimeout(processText, 100) to wait until the render is complete.
Is there any callback I can listen to, or a more reliable way of telling when an element I have created has been rendered?
The accepted answer is from 2014 and is now outdated. A setTimeout may work, but it's not the cleanest and it doesn't necessarily guarantee that the element has been added to the DOM.
As of 2018, a MutationObserver is what you should use to detect when an element has been added to the DOM. MutationObservers are now widely supported across all modern browsers (Chrome 26+, Firefox 14+, IE11, Edge, Opera 15+, etc).
When an element has been added to the DOM, you will be able to retrieve its actual dimensions.
Here's a simple example of how you can use a MutationObserver to listen for when an element is added to the DOM.
For brevity, I'm using jQuery syntax to build the node and insert it into the DOM.
var myElement = $("<div>hello world</div>")[0];
var observer = new MutationObserver(function(mutations) {
if (document.contains(myElement)) {
console.log("It's in the DOM!");
observer.disconnect();
}
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
$("body").append(myElement); // console.log: It's in the DOM!
The observer event handler will trigger whenever any node is added or removed from the document. Inside the handler, we then perform a contains check to determine if myElement is now in the document.
You don't need to iterate over each MutationRecord stored in mutations because you can perform the document.contains check directly upon myElement.
To improve performance, replace document with the specific element that will contain myElement in the DOM.
There is currently no DOM event indicating that an element has been fully rendered (eg. attached CSS applied and drawn). This can make some DOM manipulation code return wrong or random results (like getting the height of an element).
Using setTimeout to give the browser some overhead for rendering is the simplest way. Using
setTimeout(function(){}, 0)
is perhaps the most practically accurate, as it puts your code at the end of the active browser event queue without any more delay - in other words your code is queued right after the render operation (and all other operations happening at the time).
This blog post By Swizec Teller, suggests using requestAnimationFrame, and checking for the size of the element.
function try_do_some_stuff() {
if (!$("#element").size()) {
window.requestAnimationFrame(try_do_some_stuff);
} else {
$("#element").do_some_stuff();
}
};
in practice it only ever retries once. Because no matter what, by the next render frame, whether it comes in a 60th of a second, or a minute, the element will have been rendered.
You actually need to wait yet a bit after to get the after render time. requestAnimationFrame fires before the next paint. So requestAnimationFrame(()=>setTimeout(onrender, 0)) is right after the element has been rendered.
In my case solutions like setTimeout or MutationObserver weren't totaly realiable.
Instead I used the ResizeObserver. According to MDN:
Implementations should, if they follow the specification, invoke
resize events before paint and after layout.
So basically the observer always fires after layout, thus we should be able to get the correct dimensions of the observed element.
As a bonus the observer already returns the dimensions of the element. Therefore we don't even need to call something like offsetWidth (even though it should work too)
const myElement = document.createElement("div");
myElement.textContent = "test string";
const resizeObserver = new ResizeObserver(entries => {
const lastEntry = entries.pop();
// alternatively use contentBoxSize here
// Note: older versions of Firefox (<= 91) provided a single size object instead of an array of sizes
// https://bugzilla.mozilla.org/show_bug.cgi?id=1689645
const width = lastEntry.borderBoxSize?.inlineSize ?? lastEntry.borderBoxSize[0].inlineSize;
const height = lastEntry.borderBoxSize?.blockSize ?? lastEntry.borderBoxSize[0].blockSize;
resizeObserver.disconnect();
console.log("width:", width, "height:", height);
});
resizeObserver.observe(myElement);
document.body.append(myElement);
This can also we wrapped in a handy async function like this:
function appendAwaitLayout(parent, element) {
return new Promise((resolve, reject) => {
const resizeObserver = new ResizeObserver((entries) => {
resizeObserver.disconnect();
resolve(entries);
});
resizeObserver.observe(element);
parent.append(element);
});
}
// call it like this
appendAwaitLayout(document.body, document.createElement("div")).then((entries) => {
console.log(entries)
// do stuff here ...
});
The MutationObserver is probably the best approach, but here's a simple alternative that may work
I had some javascript that built the HTML for a large table and set the innerHTML of a div to the generated HTML. If I fetched Date() immediately after setting the innerHTML, I found that the timestamp was for a time prior to the table being completely rendered. I wanted to know how long the rendering was taking (meaning I needed to check Date() after the rendering was done). I found I could do this by setting the innerHTML of the div and then (in the same script) calling the click method of some button on the page. The click handler would get executed only after the HTML was fully rendered, not just after the innerHTML property of div got set. I verified this by comparing the Date() value generated by the click handler to the Date() value retrieved by the script that was setting the innerHTML property of the div.
Hope someone finds this useful
suppose your element has classname class="test"
The following function continue test if change has occured
if it does, run the function
function addResizeListener(elem, fun) {
let id;
let style = getComputedStyle(elem);
let wid = style.width;
let hei = style.height;
id = requestAnimationFrame(test)
function test() {
let newStyle = getComputedStyle(elem);
if (wid !== newStyle.width ||
hei !== newStyle.height) {
fun();
wid = newStyle.width;
hei = newStyle.height;
}
id = requestAnimationFrame(test);
}
}
let test = document.querySelector('.test');
addResizeListener(test,function () {
console.log("I changed!!")
});
when you make for example
var clonedForm = $('#empty_form_to_clone').clone(true)[0];
var newForm = $(clonedForm).html().replace(/__prefix__/g, next_index_id_form);
// next_index_id_form is just a integer
What am I doing here?
I clone a element already rendered and change the html to be rendered.
Next i append that text to a container.
$('#container_id').append(newForm);
The problem comes when i want to add a event handler to a button inside newForm, WELL, just use ready event.
$(clonedForm).ready(function(event){
addEventHandlerToFormButton();
})
I hope this help you.
PS: Sorry for my English.
According to #Elliot B.'s answer, I made a plan that suits me.
const callback = () => {
const el = document.querySelector('#a');
if (el) {
observer.disconnect();
el.addEventListener('click', () => {});
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, { subtree: true, childList: true });