Is there a way to detect when the parent of an element changes (namely when changing from null to !null -- i.e., when the element is initially added to the DOM) using a MutationObserver? I can't find any documentation that shows how this could be achieved.
I am programmatically creating elements with document.createElement(). I return the created element from a function, but want to create a listener from within the function to react when the element is eventually added to the DOM, without knowing where or which parent it will be added to.
I'm not quite sure how else to phrase this, honestly.
const elem = document.createElement('div');
let added = false;
elem.addEventListener('added-to-dom', () => { added = true; });
// ^ how do I achieve this?
assert(added == false);
document.body.addChild(elem);
assert(added == true);
I don't see what's so hard about understanding this or why it was closed.
An easy but inelegant way is to monkeypatch Node.prototype.appendChild (and, if necessary, Element.prototype.append, Element.prototype.insertAdjacentElement, and Node.prototype.insertBefore) to watch for when an element is added to the DOM:
const elementsToWatch = new Set();
const { appendChild } = Node.prototype;
Node.prototype.appendChild = function(childToAppend) {
if (elementsToWatch.has(childToAppend)) {
console.log('Watched child appended!');
elementsToWatch.delete(childToAppend);
}
return appendChild.call(this, childToAppend);
};
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
elementsToWatch.add(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
Mutating built-in prototypes generally isn't a good idea, though.
Another option would be to use a MutationObserver for the whole document, but this may well result in lots of activated callbacks for a large page with frequent mutations, which may not be desirable:
const elementsToWatch = [];
new MutationObserver(() => {
// instead of the below, another option is to iterate over elements
// observed by the MutationObserver
// which could be more efficient, depending on how often
// other elements are added to the page
const root = document.documentElement; // returns the <html> element
const indexOfElementThatWasJustAdded = elementsToWatch.findIndex(
elm => root.contains(elm)
);
// instead of the above, could also use `elm.isConnected()` on newer browsers
// if an appended node, if it has a parent,
// will always be in the DOM,
// instead of `root.contains(elm)`, can use `elm.parentElement`
if (indexOfElementThatWasJustAdded === -1) {
return;
}
elementsToWatch.splice(indexOfElementThatWasJustAdded, 1);
console.log('Observed an appended element!');
}).observe(document.body, { childList: true, subtree: true });
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
div.textContent = 'foo';
elementsToWatch.push(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
You could listen for the DOMNodeInserted-event and compare the elements id.
Notice: This event is marked as Depricated and will probably stop working in modern modern browsers at some point in the near
future.
let container = document.getElementById('container');
let button = document.getElementById('button');
document.body.addEventListener('DOMNodeInserted', function(event) {
if (event.originalTarget.id == button.id) {
console.log('Parent changed to: ' + event.originalTarget.parentElement.id);
}
});
button.addEventListener('click', function(event) {
container.appendChild(button);
});
#container {
width: 140px;
height: 24px;
margin: 10px;
border: 2px dashed #c0a;
}
<div id="container"></div>
<button id="button">append to container</button>
Related
I am having some trouble detecting attribute changes of an html element with js and MutationObserver. This is the code:
document.addEventListener("DOMContentLoaded", function() {
const checkForLoading = setInterval(function () {
let loading = document.getElementById("sequence");
if (loading) {
console.log("loading detected");
const loadingObserver = new MutationObserver(function (mutations) {
console.log("mutation detected");
if (loading.getAttribute('data-dash-is-loading') === 'true') {
console.log("loading");
// loading.style.visibility = 'hidden';
} else {
console.log("not loading");
// loading.style.visibility = 'visible';
}
});
const observerOptions = {
attributes: true,
}
loadingObserver.observe(loading, observerOptions);
clearInterval(checkForLoading);
}
}, 100);
});
Because the element is not available immediately I have the checkForLoading loop set up. The attribute 'data-dash-is-loading' is only set when an element is loading and otherwise not available. This code only works if the loop keeps on running after the sequence element is detected and clearInterval(checkForLoading) is not called. However I would like to avoid running this loop constantly. Any help to fix this issue is greatly appreciated.
It usually means the element is recreated, so you need to observe its ancestor higher up the DOM tree and add subtree: true.
For example, a parent element:
loadingObserver.observe(loading.parentElement, {attributes: true, subtree: true});
If this doesn't help at once you can try document.body first to make sure the mutation actually happens, then try various ancestor elements in-between to find the one that stays the same.
In the callback you'll need to verify that the mutation occurred on your desired element:
for (const m of mutations) {
if (m.target.id === 'foo') {
// it's the desired element, do something about it here and stop the loop
break;
}
}
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);
}
});
}
I want to animate my navbar using JavaScript. I have created separate navbar.html and included it using csi.min.js.
When I try to getElementById my navbar, show and hide button. it returns null and when try it on dev console it works.
navbar.html:
<nav>
<div class="site-mobile-menu collapse navbar-collapse show" id="main-navbar">
navbar content
</div>
<nav>
index.html:
<div data-include="/src/navbar.html"></div>
<script src="/src/js/navbar.js"></script>
navbar.js:
window.addEventListener("load", function() {
// debugger;
var mobNav = document.getElementById("main-navbar");
var showNavBtn = document.querySelector("#show-nav");
var hideNavBtn = document.getElementById("hide-nav");
console.log(mobNav + " " + showNavBtn + " " + hideNavBtn);
if (!mobNav == null || !showNavBtn == null || !hideNavBtn == null) {
showNavBtn.onclick = function() {
console.log("clicked");
mobNav.classList.add("nav-shown");
}
} else {
console.log("Error Opening Mobile Navbar");
}
}, false);
When the window load, the elements such as #main-navbar are not in the DOM yet, csi adds them later.
Ideally, csi would give an option to notified when a [data-include] element is loaded but it doesn't. I can suggest 2 ways:
Re-implementing csi
// this is csi re-implementation
function fetchTemplates() {
return Promise.all(
[...document.querySelectorAll('[data-include]')].map(async el => {
const response = await fetch(el.getAttribute('data-include'));
const html = await response.text();
el.outerHTML = html;
}));
}
// now you can call fetchTemplates and wait for it to done
(async () => {
await fetchTemplates()
console.log('templates are loaded. you can now find #main-navbar');
console.log(document.querySelector('#main-navbar'));
})();
https://codesandbox.io/s/zealous-jepsen-12ihl?file=/re-implement-csi.html
Use MutationObserver to catch when csi replace the element with the fetched content.
const div = document.querySelector('[data-include="/src/navbar.html"]');
const observer = new MutationObserver(function (mutationsList, observer) {
const templates = document.querySelectorAll('[data-include]');
if (templates.length === 0) {
console.log('all templates loaded, now you can find #main-navbar');
console.log(document.querySelector('#main-navbar'));
}
// we no longer should observe the div
observer.disconnect();
});
observer.observe(div.parentElement, {
childList: true,
});
https://codesandbox.io/s/zealous-jepsen-12ihl?file=/index.html
Personally I recommend the first option because
It's a very old package implemented with very old API (XMLHttpRequest).
If there are a lot of html elements in your site's DOM it might hurt performance.
Assuming I am unable to see below code, I have no idea how long does the timeout set, and I am unable to change the original code
document.querySelector('button').addEventListener('click', function(){
setTimeout(function(){
document.querySelector('.old').classList = 'old new';
}, 100);
});
<div class="old">OLD</div>
<button>Click</button>
What I'd like to achieve is that once the new class is added then I want to change the TEXT line, the hack code as below
document.querySelector('button').addEventListener('click', function(){
setTimeout(function(){
document.querySelector('.old').classList = 'old new';
}, 100);
});
document.querySelector('button').addEventListener('click', function(){
if(document.querySelectorAll('.new').length !== 0) {
document.querySelector('.old').innerText = "123"
}
});
<div class="old">OLD</div>
<button>Click</button>
document.querySelector('button').addEventListener('click', function(){
if(document.querySelectorAll('.new').length !== 0) {
document.querySelector('.old').innerText = "123"
}
});
Since I am unable to know how long the timeout secs, so my first click won't work as it executes right away. So I have to add the timeout seconds bigger than the original. Is there an efficient way to detected if new class is added?
or timeout is the best solution for this use case? Thanks
You might use Mutation Observer for this purpose:
document.querySelector('button').addEventListener('click', function() {
setTimeout(function() {
document.querySelector('.old').classList = 'old new';
}, 500);
});
document.querySelector('button').addEventListener('click', function() {
var observer = new MutationObserver(function(mutationsList, observer) {
for (var mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class' &&
mutation.target.classList.contains('new')
) {
mutation.target.innerText = "123";
observer.disconnect();
}
}
});
observer.observe(document.querySelector('.old'), {attributes: !0});
});
<div class="old">OLD</div>
<button>Click</button>
Use ClassList API to get this done. As new class will be add after some time. If new class is not added to dom then you can't get reference of this node to compare. The ClassList provides a handy method to solve this issue. use contains method to check if new class added or not. If added then change the text.
use below code
document.querySelector('button').addEventListener('click', function(){
if(document.querySelectorAll('.old')[0].classList.contains("new")) {
document.querySelector('.old').innerText = "123"
}
});
Solution
I want to overwrite the function focus() for all HTML elements in JavaScript.
How do I do that?
The question asks for just how to override the .focus() method for HTML elements, so that will be answered first. There are however, other ways to trigger "focus" on elements, and overriding those methods are discussed below.
Please also note, it is probably not a good idea to override global prototypes.
Overriding the .focus() method on HTML elements
You can override the focus function for all HTMLElement's by modifying its prototype.
HTMLElement.prototype.focus = function () {
// ... do your custom stuff here
}
If you still want to run the default focus behavior and run your custom override, you can do the following:
let oldHTMLFocus = HTMLElement.prototype.focus;
HTMLElement.prototype.focus = function () {
// your custom code
oldHTMLFocus.apply(this, arguments);
};
Overriding the focus event set via handlers on HTML elements
This is done in a similar fashion to overriding the .focus() method. However, in this case, we will target the EventTarget constructor and modify the addEventListener prototype:
let oldAddListener = HTMLElement.prototype.addEventListener;
EventTarget.prototype.addEventListener = function () {
let scope = this;
let func = arguments[1];
if (arguments[0] === 'focus') {
arguments[1] = function (e) {
yourCustomFunction(scope);
func(e);
};
}
oldAddListener.apply(this, arguments);
};
If you don't want original behavior at all, you can remove the func call (func(e)).
Overriding the focus event set via the onfocus attribute
Doing this is actually quite tricky, and will most likely have some unforseen limitations. That said, I could not find a way to override this by modifying prototypes, etc. The only way I could get this work was by using MutationObservers. It works by finding all elements that have the attribute onfocus and it sets the first function to run to your override function:
let observer = new MutationObserver(handleOnFocusElements);
let observerConfig = {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['onfocus']
};
let targetNode = document.body;
observer.observe(targetNode, observerConfig);
function handleOnFocusElements() {
Array
.from(document.querySelectorAll('[onfocus]:not([onfocus*="yourCustomFunction"])'))
.forEach(element => {
let currentCallbacks = element.getAttribute('onfocus');
element.setAttribute('onfocus', `yourCustomFunction(this); return; ${currentCallbacks}`);
});
}
If you want to stop the original onfocus from firing its events completely, you can just empty the onfocus attribute entirely on any mutation changes, and just set the value to your desired function.
An example with all of the code snippets together:
(function() {
window.yourCustomFunction = function(target) {
console.log('OVERRIDE on element', target);
};
let observer = new MutationObserver(handleOnFocusElements);
let observerConfig = {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['onfocus']
};
let targetNode = document.body;
// Start overriding methods
// The HTML `.focus()` method
let oldHTMLFocus = HTMLElement.prototype.focus;
HTMLElement.prototype.focus = function() {
yourCustomFunction(this);
oldHTMLFocus.apply(this, arguments);
};
// Event Target's .addEventListener prototype
let oldAddListener = HTMLElement.prototype.addEventListener;
EventTarget.prototype.addEventListener = function() {
let scope = this;
let func = arguments[1];
if (arguments[0] === 'focus') {
arguments[1] = function(e) {
yourCustomFunction(scope);
func(e);
};
}
oldAddListener.apply(this, arguments);
};
// Handle the onfocus attributes
function handleOnFocusElements() {
Array
.from(document.querySelectorAll('[onfocus]:not([onfocus*="yourCustomFunction"])'))
.forEach(element => {
let currentCallbacks = element.getAttribute('onfocus');
element.setAttribute('onfocus', `yourCustomFunction(this); return; ${currentCallbacks}`);
});
}
window.addEventListener('load', () => {
handleOnFocusElements();
observer.observe(targetNode, observerConfig);
});
})();
let input = document.querySelector('input');
input.addEventListener('focus', function() {
console.log('Add Event Listener Focus');
});
function attributeHandler() {
console.log('Called From the Attribute Handler');
}
<input type="text" onfocus="attributeHandler()">
In pure JS:
HTML:
<input id="test" type="textfield" />
JS:
var myInput = document.getElementById("test");
myInput.addEventListener("focus",deFocus(myInput),true);
function deFocus(element) {
element.setAttribute('readonly','readonly');
return false;
}
Working fiddle
In JQuery:
HTML
<input id="test" type="textfield" />
JQuery
$('#test').focus(function() {
$(this).blur();
});
Working fiddle
You can add the EventListener to whatever you want, class, id, etc. and run it through the function or trigger something else to unlock them. JQuery can also handle this pretty smoothly, though if you're going that route I would suggest using a class as a selector and then you can add and remove the class dynamically to disable focus.