JavaScript. Detect when html element is loaded from another js script - javascript

I am trying to detect html element which is loaded from 3rd party library for example:
div with title "box". It should should be detected without using delays.
function createBox() {
var parser = new DOMParser();
var domString =
'<div title="box" style="border:solid" class="container"><button>btn</button><span class="intro">Hello</span> <span id="name"> World!</span></div>';
var html = parser.parseFromString(domString, "text/html");
document.body.append(html.body.firstChild);
}
setTimeout(function() {
// Do something after 2 seconds
createBox();
}, 2000);
let box = document.querySelector('[title="box"]');
if (box != null) {
console.log("box is detected");
}

If you know where the element is going to be appended, you can attach a MutationObserver to the parent, which will run a callback when a child is appended:
function createBox() {
var parser = new DOMParser();
var domString =
'<div title="box" style="border:solid" class="container"><button>btn</button><span class="intro">Hello</span> <span id="name"> World!</span></div>';
var html = parser.parseFromString(domString, "text/html");
document.body.append(html.body.firstChild);
}
console.log('start');
setTimeout(function() {
// Do something after 2 seconds
createBox();
}, 2000);
new MutationObserver(() => {
const box = document.querySelector('[title="box"]');
if (box) {
console.log("box is detected");
}
})
.observe(document.body, { childList: true });
If you don't know where the element is going to be appended, but you can identify the element (such as with a selector), you can still use a MutationObserver, but it'll have to watch for deep changes via subtree: true, which can be expensive on large pages which change a lot:
console.log('start');
function createBox() {
var parser = new DOMParser();
var domString =
'<div title="box" style="border:solid" class="container"><button>btn</button><span class="intro">Hello</span> <span id="name"> World!</span></div>';
var html = parser.parseFromString(domString, "text/html");
outer.append(html.body.firstChild);
}
setTimeout(function() {
// Do something after 2 seconds
createBox();
}, 2000);
new MutationObserver((_, observer) => {
const box = document.querySelector('[title="box"]');
if (box) {
console.log("box is detected");
observer.disconnect();
}
})
.observe(document.body, { childList: true, subtree: true });
<div id="outer">outer</div>

Related

How to wait for appendChild() to update and style the DOM, before trying to measure the new element's width

I'm appending a DOM element like this:
this.store.state.runtime.UIWrap.appendChild(newElement)
When I immediately try to measure the new element's width I get 2.
I tried:
setTimeout()
double nested window.requestAnimationFrame()
MutationObserver
The above works very unreliably, like 50% of the time. Only when I set a large timeout like 500ms it worked.
This happens only on mobile.
This is the workaround that I'm using, but it's ugly:
function getWidthFromStyle(el) {
return parseFloat(getComputedStyle(el, null).width.replace('px', ''))
}
function getWidthFromBoundingClientRect(el) {
return el.getBoundingClientRect().width
}
console.log(getWidthFromBoundingClientRect(newElement))
while (getWidthFromBoundingClientRect(newElement) < 50) {
await new Promise(r => setTimeout(r, 500))
console.log(getWidthFromBoundingClientRect(newElement))
}
I tried with both functions getWidthFromStyle() and getWidthFromBoundingClientRect() - no difference. The width gets calculated properly after a couple of cycles.
I also tried using MutationObserver without success.
Is there a way to know when the DOM is fully updated and styled before I try to measure an element's width/height?
P.S. I'm not using any framework. this.store.state.runtime... is my own implementation of a Store, similar to Vue.
EDIT: The size of the element depended on an image inside it and I was trying to measure the element before the image had loaded. Silly.
it can done with MutationObserver.
doesn't this method solve your problem?
const div = document.querySelector("div");
const span = document.querySelector("span");
const observer = new MutationObserver(function () {
console.log("new width", size());
});
observer.observe(div, { subtree: true, childList: true });
function addElem() {
setTimeout(() => {
const newSpan = document.createElement("span");
newSpan.innerHTML = "second";
div.appendChild(newSpan);
console.log("element added");
}, 3000);
}
function size() {
return div.getBoundingClientRect().width;
}
console.log("old width", size());
addElem();
div {
display: inline-block;
border: 1px dashed;
}
span {
background: gold;
}
<div>
<span>one</span>
</div>
You can use something like this:
export function waitElement(elementId) {
return new Promise((resolve, reject) => {
const element = document.getElementById(elementId);
if (element) {
resolve(element);
} else {
let tries = 10;
const interval = setInterval(() => {
const element = document.getElementById(elementId);
if (element) {
clearInterval(interval);
resolve(element);
}
if (tries-- < 0) {
clearInterval(interval);
reject(new Error("Element not found"));
}
}, 100);
}
});
}

Best way to check when an element is not undefined

While I'm observing a web-page there is a button that after I click an element appears.
I already have the id of that element, what I want to do in a single code:
press the button, wait for the specific element to appear (become defined), perform an action.
What I tried to do is this:
btn = document.getElementById("btn");
btn.click();
while(document.getElementById("id") == undefined){
continue;
}
console.log("element is loaded!!");
That code didn't work for me (the browser got stuck).
I thought also to pause the code for specific time that it gets to the element to appear (sleep), but is there a better way?
Again, I don't have access to the code of the web-page, so I can't rais a flag when this element is loaded.
Try using a Promise:
btn = document.getElementById("btn");
btn.click();
new Promise((resolve, reject) => {
while (document.getElementById("id") == undefined) {}
resolve();
}).then(() => {
console.log("element is loaded!!");
});
globally, if you want to check if the variable is set :
if(variable)
{
// Do stuff
}
You could set an interval
btn = document.getElementById("btn");
btn.click();
let intv = setInterval(() => {
if (!!document.getElementById("id")) {
console.log('the element is here');
clearInterval(this);
}}, 200);
}
console.log("element is loaded!!");
Use a MutationObserver to check whether the element exists everytime a change in the DOM occurs:
let observer = new MutationObserver(() => document.getElementById('id') ? console.log("loaded") : '');
observer.observe(document.body, {
childList: true
});
Demo:
let observer = new MutationObserver(() => document.getElementById('id') ? console.log("loaded") : '');
observer.observe(document.body, {
childList: true
});
/* below is simply for demonstration */
document.getElementById("btn").addEventListener('click', () => {
setTimeout(() => {
document.body.appendChild(Object.assign(document.createElement("p"), {id: 'id',innerHTML: 'Hello World!'}));
}, 1000)
})
<button id="btn">Click to add an element in 1 second</button>
Your browser is probably crashing because the code is executed too many times in the while loop.
You could try using MutationObserver to listen for change in DOM.
Or
Add use setInterval instead of the while loop.
let interval;
function handleClick() {
// add the new element after 5 seconds
const divNode = document.createElement('div');
divNode.id = 'newElement';
window.setTimeout(() => {
document.body.append(divNode);
}, 5000);
// every second check for the element
interval = window.setInterval(() => {
checkIfElementIsInDom();
}, 1000);
}
function checkIfElementIsInDom() {
console.log('Checking...');
const newNode = document.getElementById('newElement');
if (newNode) {
console.log('completed!');
// stop the interval
clearInterval(interval);
}
}
const buttonNode = document.getElementById('button');
buttonNode.addEventListener('click', handleClick.bind(this));
<button type="button" id="button">Click</button>
Here is an example of using a MutationObserver
const targetNode = document.querySelector('.test');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
// Check for our new item
const itm = document.querySelector('.itm');
if (itm) {
console.log('we found our item');
observer.disconnect();
}
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
document.querySelector('button').addEventListener('click', () => {
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
setTimeout(() => {
// Add an item.
document.querySelector('.test').innerHTML = '<div class="itm">Here it is</div>';
}, 5000);
});
<section>
<div>
<button>Click Me</button>
</div>
</section>
<section class="test">
</section>

Cannot get an img element that is dynamically added to a page

Let's say I am trying to run code bellow on 9gag to get images that are dynamically added form infinite scroll. I am trying to figure out how to get img element.
//want to do something useful in this function
checkIfImg = function(toCheck){
if (toCheck.is('img')) {
console.log("finaly");
}
else {
backImg = toCheck.css('background-image');
if (backImg != 'none'){
console.log("background fynaly");
}
}
}
//that works just fine, since it is not for dynamic content
//$('*').each(function(){
// checkIfImg($(this));
//})
//this is sums up all my attempts
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
switch (mutation.type) {
case 'childList':
Array.prototype.forEach.call(mutation.target.children, function (child) {
if ( child.tagName === "IMG" ) {
console.log("img");
}
child.addEventListener( 'load', checkIfImg, false );
console.log("forEachChild");
console.log(child);
checkIfImg($(child));
$(child).each(function(){
console.log("inside each");
console.log($(this));
if ($(this).tagName == "IMG"){
console.log("img");
}
checkIfImg($(this));
})
});
break;
default:
}
});
});
observer.observe(document, {childList: true, subtree: true});
Observer gets lots of different elements but I can't seem to find any img among them.
You need to check for img elements deeper down the tree, not only the direct children of mutation.children (each of them may contain additional children).
You could do this with a $.find('img'), and use an array to eliminate duplicates:
let imageList = [];
//want to do something useful in this function
function checkIfImg(toCheck) {
// find all images in changed node
let images = toCheck.find('img');
for(let image of images) {
let imageSource = $(image).attr('src');
if(!imageList.includes(imageSource)) {
imageList.push(imageSource);
console.log("image:", imageSource);
}
}
};
// get existing images
checkIfImg($(document));
// observe changes
var observer = new MutationObserver(function(mutations) {
mutations.forEach(mutation => checkIfImg($(mutation.target)));
});
observer.observe(document, { childList: true, subtree: true });

Wait for specific text in DOM element before continuing

I have a function that should wait for some text to change before it returns a value:
function getElementText() {
while(isLoading()) {
}
return $('#element').text();
}
function isLoading() {
return $('#element')[0] &&
$('#element').text().indexOf('Loading') >= 0;
}
However I think the empty while is not a good option (will it block the event loop?)
No need of jQuery or any other external library, you can simply use MutationObserver: https://developer.mozilla.org/en/docs/Web/API/MutationObserver
Here a simple example, you will have a notice of type characterData when your text changes (after 5 seconds in my example):
// select the target node
var target = document.getElementById('some-id');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true, attributes: true, subtree: true };
// pass in the target node, as well as the observer options
observer.observe(target, config);
// later, you can stop observing
//observer.disconnect();
setTimeout(function() {
target.innerText = 'Changed text!';
}, 5000);
<div id="some-id">
AAA
</div>
Remove from the config of the observer all the properties you don't need to spy for changes
Elegant way with rxjs:
var source = document.getElementById('source');
var target = document.getElementById('target');
Rx.Observable.fromEvent(source, 'keyup')
.filter( (e) => e.target.value === 'Admin' )
.subscribe( () => target.innerText = "Matched." );
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.0/Rx.min.js"></script>
<input id="source" /> <strong>input 'Admin' to trigger</strong>
<p id="target">Not match.</p>
The DOMSubtreeModified event will be triggered when a change occurs in the element, so you can use that to detect if the text is loaded.
You can use a callback function to return the value when it has loaded. Or even better: (jQuery) promises!
var element = document.getElementById('element');
function isLoading() {
return element.innerText.indexOf('Loading') >= 0;
}
function getElementText() {
var def = $.Deferred();
if (!isLoading()) {
def.resolve(element.innerText);
return def.promise();
}
$(element).on('DOMSubtreeModified', function () {
if (!isLoading()) {
def.resolve(element.innerText);
}
});
return def.promise();
}
getElementText().then(function (text) {
// Text is loaded!
alert(text);
});
// Load text after 3 seconds for demonstration
setTimeout(function () {
element.innerText = 'Changed!';
}, 3000);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="element">Loading</div>

DOM Mutation Observer Callback and Child class for changed events

I have the following DOM Mutation Observer code:
<script type="text/javascript">
var targetNodes = $("#history");
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var myObserver = new MutationObserver (mutationHandler);
var obsConfig = { childList: true, characterData: true, attributes: true, subtree: true };
//--- Add a target node to the observer. Can only add one node at a time.
targetNodes.each ( function () {
myObserver.observe (this, obsConfig);
} );
function mutationHandler (mutationRecords) {
console.info ("mutationHandler:");
mutationRecords.forEach ( function (mutation) {
$("span.badge").show();
} );
}
</script>
It is working fine when events changes are detected in #history id.
<p id="history"></p>
The problem is that i have some p.class inside #history as follows:
<p id="history">
<p class="mine"></p>
<p class="theirs"></p>
</p>
i need to detect the observer changes only in p class="theirs".
How is that possible only with child class, rather than observing DOM changes in #history id as a whole...
Introduction:
when you use the format:
$("#history").each ( function () {
myObserver.observe (this, obsConfig);
});
this is useless. $("#history") returns one element or nothing.
To test if a value is returned you may use
$("#history").length
This value in your case is 1. Remember that cannot exist more than one element with the same id (refer to: # selector).
In the each loop you use the this keyword. The value of this keyword is "Node target" element required from observe function.
So, because you have only one history element it's completely useless to cycle con only one element (refer: each function). Use the value by itself.
This value can be searched also with:
var target = document.getElementsByClassName('theirs')[0];
or
target = document.querySelectorAll('.theirs')[0];
or
target = $('.theirs').get(0);
Of course, if you do not have such new element on which to observe you cannot use the observe function.
For details see MutationObserver
The best way is to test the return value of the selected element, for instance:
if ($('.theirs').length == 0) {
// raise error and stop
} else {
target = $('.theirs').get(0);
}
Instead, if you have more than one element you may continue to use the each loop:
$(".theirs").each ( function () {
myObserver.observe (this, obsConfig);
});
My proposal:
According to HTML Paragraph tag you cannot have nested paragraphs.
If you need to observe only what happens for your 'theirs' paragraph you need simply to change a bit your code:
$(function () {
var targetNodes = $(".theirs");
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var myObserver = new MutationObserver(mutationHandler);
var obsConfig = {childList: true, characterData: true, attributes: true, subtree: true};
// get the target node on which to observe DOM mutations
var target = document.getElementsByClassName('theirs')[0];
// another way to get the target is
target = document.querySelectorAll('.theirs')[0];
// another way to get the target is
if ($('.theirs').length == 0) {
// raise error and stop
} else {
target = $('.theirs').get(0);
}
myObserver.observe(target, obsConfig);
function mutationHandler(mutationRecords) {
alert("mutationHandler:");
mutationRecords.forEach(function (mutation) {
$("span.badge").show();
});
}
$('#btn').on('click', function (e) {
$('.theirs').text('Date Now is: ' + Date.now());
});
});
<script src="https://code.jquery.com/jquery-1.12.1.min.js"></script>
<div id="history">
<p class="mine"></p>
<p class="theirs"></p>
</div>
<button id="btn">Add text to theirs</button>
If you are interested in changes happening only for paragraphs added or changed inside the div with class theirs, according to the MutationRecord in the following I report only a demo on how to filter the events for new such added nodes (remember to start and stop the observer):
var myObserver = null;
function mutationHandler(mutationRecords, mutationInstance) {
// new node added
if (mutationRecords.length == 1 && mutationRecords[0].addedNodes.length == 1) {
// if element added is a paragraph with class theirs....
var eleAdded = $(mutationRecords[0].addedNodes[0]);
if (eleAdded.is('p.theirs')) {
alert("mutationHandler: added new paragraph with class theirs: " + eleAdded.text());
}
}
// if you need to listen for other events like attribute changed or element removed... please read the documentation regarding mutationRecords object
}
$(function () {
$('#startObserver').on('click', function(e) {
var targetNodes = $('#history');
var target = null;
if (targetNodes.length != 1) {
alert('Cannot start Observer on no element!')
return;
} else {
target = targetNodes.get(0);
}
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
myObserver = new MutationObserver(mutationHandler);
var obsConfig = {childList: true, characterData: true, attributes: true, subtree: true};
myObserver.observe(target, obsConfig);
});
$('#stopObserver').on('click', function(e) {
if (myObserver === null) {
alert('Cannot stop an Observer never started!')
} else {
myObserver.disconnect();
myObserver = null;
}
});
$('#btnMine').on('click', function (e) {
var txt = $('#mineInput').val();
$('#history').prepend('<p class="mine">Added mine paragraph with text: ' + (txt.trim() ? txt : 'empty text!') + '</p>');
});
$('#btnTheirs').on('click', function (e) {
var txt = $('#theirsInput').val();
$('#history').append($('<p class="theirs">Added theirs paragraph with text: ' + (txt.trim() ? txt : 'empty text!') + '</p>'));
});
});
<script src="https://code.jquery.com/jquery-1.12.1.min.js"></script>
<div id="history">
</div>
Mine paragraphs text: <input id="mineInput" type="text"><br>
Theirs paragraphs text: <input id="theirsInput" type="text"><br>
<button id="btnMine">Add new Mine paragrapgh</button>
<button id="btnTheirs">Add new Theirs paragrapgh</button><br><br>
<button id="startObserver">Start Observer</button>
<button id="stopObserver">Stop Observer</button>

Categories