Detecting when a DOM element's parent changes - javascript

Is there a DOM event that fires when an element's parentElement changes? If not, is there any way better than polling with a timeout?
I'm specifically interesting in knowing when the parentElement changes from null to some defined element. That is, when a DOM element is attached to the document tree somewhere.
EDIT
Given the questions in the comments, here is an example that shows how to create an element with a null parentElement:
var element = document.createElement('div');
console.assert(element.parentElement == null);
The parent is only set once it's added to the DOM:
document.body.appendChild(element);
console.assert(element.parentElement != null);
Note too that elements created using jQuery will also have a null parent when created:
console.assert($('<div></div>').get(0).parentElement == null);

Afaik there's no such "parent listener".
Yet, I found a hack that might be helpful. At least it's worth reading, since the idea is clever.
http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
He uses CSS #keyframes during the insertion and listens for the resulting animation event which tells him, that the element got inserted.

1) Such a parentElementHasChanged event doesn't exist.
2) The workaround PISquared pointed to would work but looks very strange to me.
3) In practise there is no need for such an event. A parentChange would only appear to an element if it's position in the DOM changes.To make this happen you have to run some code on the element doing this, and all that code has to use native parent.removeChild(),
parent.appendChild, parent.insertBefore() or parent.replaceChild() somewhere. The same code could run a callback afterwards so the callback would be the event.
4) You are building library code. The library could provide a single function for all DOM-insertions/removals, which wraps the four native functions and "triggers the event". That's the last and only what comes in my mind to avoid a frequently lookup for parentElement.
5) If there's a need to include the native Event API, you may create a parentChanged event with CustomEvent
element.addEventListener('parentChanged', handler); // only when Event API needed
function manipulateElementsDOMPosition(element, target, childIndex, callback, detail) {
if (!target.nodeType) {
if (arguments.length > 4) return element;
detail = callback; callback = childIndex; childIndex = target; target = null;
}
if (typeof childIndex === 'function') detail = callback, callback = childIndex;
var oldParent = element.parentElement,
newParent = target,
sameParent = oldParent === newParent,
children = newParent.children,
cl = children.length,
ix = sameParent && cl && [].indexOf.call(children, element),
validPos = typeof childIndex === 'number' && cl <= childIndex;
if (childIndex === 'replace') {
(newParent = target.parentElement).replaceChild(element, target);
if (sameParent) return element;
} else {
if (samePar) {
if (!oldParent || ix == childIndex ||
childIndex === 'first' && ix === 0 ||
childIndex === 'last' && ix === (cl - 1)) return element;
oldParent.removeChild(element);
} else if (oldParent) oldParent.removeChild(element);
if (!cl || childIndex === 'last') {
newParent.appendChild(element);
} else if (childIndex === 'first') {
newParent.insertBefore(element, children[0])
} else if (validPos) {
newParent.insertBefore(element, children[childIndex]);
} else return element;
}
console.log(element, 'parentElement has changed from: ', oldParent, 'to: ', newParent);
element.dispatchEvent(new CustomEvent('parentChanged', detail)); // only when Event API needed
if (typeof callback === 'function') callback.call(element, oldParent, newParent, detail);
return element;
}
some example usage (detail may be anything you want to pass to the event/callback). Function always return element.
// remove element
manipulateElementsDOMPosition(element /*optional:*/, callback, detail);
// prepend element in target
manipulateElementsDOMPosition(element, target, 'first' /*optional:*/, callback, detail);
// append element in target
manipulateElementsDOMPosition(element, target, 'last' /*optional:*/, callback, detail);
// add element as third child of target, do nothing when less than two children there
manipulateElementsDOMPosition(element, target, 3 /*optional:*/, callback, detail);
// replace a target-element with element
manipulateElementsDOMPosition(element, target, 'replace' /*optional:*/, callback, detail);

You can use a MutationObserver like this:
const trackedElement = document.getElementById('tracked-element');
const parent1 = document.getElementById('parent1');
const parent2 = document.getElementById('parent2');
startObserver();
function changeParent() {
if (trackedElement.parentElement == parent1) {
parent1.removeChild(trackedElement);
parent2.appendChild(trackedElement);
} else {
parent2.removeChild(trackedElement);
parent1.appendChild(trackedElement);
}
}
function startObserver() {
let parentElement = trackedElement.parentElement
new MutationObserver(function(mutations) {
if (!parentElement.contains(trackedElement)) {
trackedElement.textContent = "Parent changed";
setTimeout(() => {
trackedElement.textContent = "Parent not changed";
}, 500);
startObserver();
this.disconnect();
}
}).observe(parentElement, {
childList: true
});
}
<!doctype html>
<html lang="en">
<body>
<div id="parent1">
<p id="tracked-element">Parent not changed</p>
</div>
<div id="parent2"></div>
<button onclick="changeParent()">ChangeParent</button>
</body>
</html>
You can right click on the Parent not changed text and click inspect to make sure that it is actually changing parents
If you want to know how the .observer function works, there's VERY good documentation on it here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe

Related

How to get and pass the clicked element as THIS inside a addEventListener

So I'm building a small custom JS library like jQuery but I ran into an wall.
I have build a event delegation function which I will be using for a simple click event.
Within this prototype part I will run some logic(in this example a addClass) but it does not work with the this keyword. As I need to add for example a class to the the clicked element.
Constructor.prototype.on = function (eventName , elementSelector, callback) {
document.addEventListener(eventName, function(e) {
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
callback.call(target, e);
break;
}
}
}, false);
};
// X is the plugin
X('body').on('click','.someClass',function(){
X(this).addClass('clicked');// not going to work
});
You need to change your Constructor function so that it allows you to provide a DOM element, not just a selector, as a parameter. Then you can use X(this) to create an instance of your class that contains this element.
var Constructor = function(selector) {
if (!selector) return;
if (typeof selector != string) {
this.nodes = [selector];
} else if (selector === 'document') {
this.nodes = [document];
} else if (selector === 'window') {
this.nodes = [window];
} else {
this.nodes = document.querySelectorAll(selector);
}
this.length = this.nodes.length;
};

Custom Element getRootNode.closest() function crossing multiple (parent) shadowDOM boundaries

I spent some time searching but have only seen too many regular "walk the DOM" blogs or answers that only go one level UP with getRootnode()
Pseudo code:
HTML
<element-x>
//# shadow-root
<element-y>
<element-z>
//# shadow-root
let container = this.closest('element-x');
</element-z>
</element-y>
</element-x>
The standard element.closest() function does not pierce shadow boundaries;
So this.closest('element-x') returns null because there is no <element-x> within <element-z> shadowDom
Goal:
Find <element-x> from inside descendant <element z> (any nested level)
Required:
A (recursive) .closest() function that walks up the (shadow) DOMs and finds <element-x>
Note: elements may or may not have ShadowDOM (see <element y>: only lightDOM)
I can and will do it myself tomorrow; just wondered if some bright mind had already done it.
Resources:
https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode
https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/host
Update
This is the UNminified code from the answer below:
closestElement(selector, base = this) {
function __closestFrom(el) {
if (!el || el === document || el === window) return null;
let found = el.closest(selector);
if (found)
return found;
else
__closestFrom(el.getRootNode().host);
}
return __closestFrom(base);
}
Update #2
I changed it to a method on my BaseElement:
closestElement(selector, el = this) {
return (
(el && el != document && el != window && el.closest(selector)) ||
this.closestElement(selector, el.getRootNode().host)
);
}
Events
As Intervalia comments; yes Events are another solution.
But then... an Event needs to be attached to an ancestor... How to know which ancestor to use?
This does the same as .closest() from inside any child (shadow)DOM
but walking up the DOM crossing shadowroot Boundaries
Optimized for (extreme) minification
//declared as method on a Custom Element:
closestElement(
selector, // selector like in .closest()
base = this, // extra functionality to skip a parent
__Closest = (el, found = el && el.closest(selector)) =>
!el || el === document || el === window
? null // standard .closest() returns null for non-found selectors also
: found
? found // found a selector INside this element
: __Closest(el.getRootNode().host) // recursion!! break out to parent DOM
) {
return __Closest(base);
}
Note: the __Closest function is declared as 'parameter' to avoid an extra let declaration... better for minification, and keeps your IDE from complaining
Called from inside a Custom Element:
<element-x>
//# shadow-root
<element-y>
<element-z>
//# shadow-root
let container = this.closestElement('element-x');
</element-z>
</element-y>
</element-x>
Excellent examples! Wanted to contribute a TypeScript version that has a minor difference -- it follows assignedSlot while traversing up the shadow roots, so you can find the closest matching element in a chain of nested, slotted custom elements. It's not the fanciest way to write the TypeScript, but it gets the job done.
closestElement(selector: string, base: Element = this) {
function __closestFrom(el: Element | Window | Document): Element {
if (!el || el === document || el === window) return null;
if ((el as Slotable).assignedSlot) el = (el as Slotable).assignedSlot;
let found = (el as Element).closest(selector);
return found
? found
: __closestFrom(((el as Element).getRootNode() as ShadowRoot).host);
}
return __closestFrom(base);
}
The equvalent in JS is:
closestElement(selector, base = this) {
function __closestFrom(el) {
if (!el || el === document || el === window)
return null;
if (el.assignedSlot)
el = el.assignedSlot;
let found = el.closest(selector);
return found
? found
: __closestFrom(el.getRootNode().host);
}
return __closestFrom(base);
}
Something like this should do the trick
function closestPassShadow(node, selector) {
if (!node) {
return null;
}
if (node instanceof ShadowRoot) {
return this.closestPassShadow(node.host, selector);
}
if (node instanceof HTMLElement) {
if (node.matches(selector)) {
return node;
} else {
return this.closestPassShadow(node.parentNode, selector);
}
}
return this.closestPassShadow(node.parentNode, selector);
}
just a to endolge legibility / code style. this should be typescript friendly as well.
const closestElement = (selector, target) => {
const found = target.closest(selector);
if (found) {
return found;
}
const root = target.getRootNode();
if (root === document || !(root instanceof ShadowRoot)) {
return null;
}
return closestElement(selector, root.host);
};

JavaScript: for loop in dynamic rollover function not working

I'm trying to create a dynamic function using a for-loop in javascript, which will fire rollovers. I am using JS vs. CSS as the amount of images which will be rollovers is growing, and I figure a function is easier to maintain than x number of selectors.
This is creating a on method similar to jQuery.
var on = function(event, elem, callback, capture) {
if (typeof elem === 'function') {
capture = callback;
callback = elem;
elem = window;
}
capture = capture ? true : false;
elem = typeof elem === 'string' ? document.querySelector(elem) : elem;
if (!elem) return;
elem.addEventListener(event, callback, capture);
};
These are my rollOver and rollOut functions:
function rollOver(elem) {
document.getElementById(elem).src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.slice(length-1)}.jpg?$staticlink$`
}
function rollOut(elem) {
document.getElementById(elem).src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_NO_HOVER_${elem.slice(length-1)}.jpg?$staticlink$`
}
And this is where my for-loop lives:
document.addEventListener("DOMContentLoaded", function(event) {
var rollOverCollectionA = document.getElementById('roll-over-collection-a').querySelectorAll('img');
rollOverCollectionA = Array.prototype.slice.apply(rollOverCollectionA);
for (var i = 0; i < rollOverCollectionA.length; i++) {
on('mouseover', rollOverCollectionA[i].id, function(){
console.log( rollOverCollectionA[i].id)
rollOver(rollOverCollectionA[i].id);
});
on('mouseout', rollOverCollectionA[i].id, function(){
rollOut(rollOverCollectionA[i].id);
});
}
});
The main problems I saw were:
elem.slice(length - 1); should be elem.slice(elem.length - 1) otherwise you're subtracting 1 from undefined
elem.slice should be replaced with elem.substr(elem.lastIndexOf('-') + 1) otherwise any images over 9 will start back at 0 because you would only get the last character of the id.
When you pass a string as elem to on, it uses document.querySelector, but you pass the id without the hash symbol (#). You don't need this anyway as you already have a reference to the image element, you can just pass that.
I also tidied it up and modernized it a little bit.
The glaring problem I neglected to mention was the use of var and the for(;;) loop. Thanks to #tranktor53 for pointing that out. I always instinctively replace for(;;) loops with for...in or for...of loops where I see them, and var with let or const, I didn't even notice that it was part of the problem.
function on({ type, element = window, callback, capture = false }) {
if (typeof element === 'string') element = document.querySelector(element);
if (!element) return;
element.addEventListener(type, callback, capture);
};
function rollOver({ element, id }) {
element.src = `https://via.placeholder.com/200x100?text=${ id }+hover`;
}
function rollOut({ element, id }) {
element.src = `https://via.placeholder.com/200x100?text=${ id }+no+hover`;
}
document.addEventListener("DOMContentLoaded", _ => {
const elements = document.querySelectorAll('#roll-over-collection-a img');
for(let element of elements) {
const id = element.id.substr(element.id.lastIndexOf('-') + 1);
on({ type: 'mouseover', element, callback: _ => rollOver({ element, id }) });
on({ type: 'mouseout' , element, callback: _ => rollOut({ element, id }) });
}
});
<div id="roll-over-collection-a">
<img id="roll-over-1" src="https://via.placeholder.com/200x100?text=1+no+hover">
<img id="roll-over-2" src="https://via.placeholder.com/200x100?text=2+no+hover">
<img id="roll-over-3" src="https://via.placeholder.com/200x100?text=3+no+hover">
<img id="roll-over-4" src="https://via.placeholder.com/200x100?text=4+no+hover">
<img id="roll-over-5" src="https://via.placeholder.com/200x100?text=5+no+hover">
<img id="roll-over-6" src="https://via.placeholder.com/200x100?text=6+no+hover">
</div>
The main problem is using var in the for loop and assuming that the value of i seen in the event handler will match that of i when th call back function was created. This is incorrect, since i will have the value it reached when the for loop completed, at a later time when the handler gets executed.
In current browsers the solution is to use let instead of var and hence starting the loop as
for (let i = 0; i < rollOverCollectionA.length; i++) ...
For discussion and older solutions see JavaScript closure inside loops – simple practical example
In regards the image source string calculation
/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.slice(length-1)}.jpg?$staticlink$
please review what is needed - if you need to copy the entire value of elem as a string, don't include the call to slice. If a part of the string is required, the posted code may be correct. Slicing from a start index of elem.length-1 will copy the last letter of the element id (of course) and may also be correct.
Update: The need to capture the value of i during loop iteration can be eliminated in at least two ways:
In the mouseover and mouseout event handlers replace rollOverCollectionA[i]with this. There is no need for reverse lookup of an HTML collection based on a captured index value to determine the element an event handler is attached to.
Use event delegation with a single listener attached to on the images' DOM container. Using the same on function, and elem.id.slice(length-1) as a possible image source suffix, similar to:
document.addEventListener("DOMContentLoaded", function(event) {
var rollOverCollectionA = document.getElementById('roll-over-collection-a');
on('mouseover', rollOverCollectionA, function( event){
var elem = event.target;
if( elem && elem.id && elem.tagName === 'IMG') {
elem.src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.id.slice(length-1)}.jpg?$staticlink$`;
}
});
on('mouseout', rollOverCollectlonA, function(event) {
var elem = event.target;
if( elem && elem.id && elem.tagName === 'IMG') {
elem.src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_NO_HOVER_${elem.id.slice(length-1)}.jpg?$staticlink$`;
}
});
});

Is there a pure Javascript way to apply one function to multiple element's events?

I want to bind a single function to multiple events using pure Javascript.
In jQuery I would use:
$('.className').click(function(e){ //do stuff });
So using pure JS I tried:
document.getElementsByClassName('className').onclick = function(e){ //do stuff };
Which doesn't work, because getElementsByClassName returns an array, not a DOM object.
I can loop through the array, but this seems overly verbose and like it shouldn't be necessary:
var topBars = document.getElementsByClassName('className');
for(var i = 0; i < topBars.length; i++){
topBars[i].onclick = function(e){ //do stuff };
}
Is there a standard way to accomplish this with pure Javascript?
You could add the event handler to the parent element, and then determine whether one of the children elements with the desired classname is clicked:
var parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
if ((' ' + e.target.className + ' ').indexOf(' item ') !== -1) {
// add logic here
console.log(e.target);
}
});
Example Here
or...
var parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
Array.prototype.forEach.call(parent.querySelectorAll('.item'), function (el) {
if (el === e.target) {
// add logic here
console.log(e.target);
}
});
});
Example Here
The above snippets will only work when you are clicking on the element with the specified class. In other words, it won't work if you click on that given element's child. To work around that, you could use the following:
var parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
var target = e.target; // Clicked element
while (target && target.parentNode !== parent) {
target = target.parentNode; // If the clicked element isn't a direct child
if (!target) { return; } // If element doesn't exist
}
if ((' ' + target.className + ' ').indexOf(' item ') !== -1){
// add logic here
console.log(target);
}
});
Alternative Example
var parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
var target = e.target; // Clicked element
while (target && target.parentNode !== parent) {
target = target.parentNode; // If the clicked element isn't a direct child
if (!target) { return; } // If element doesn't exist
}
Array.prototype.forEach.call(parent.querySelectorAll('.item'), function (el) {
if (el === target) {
// add logic here
console.log(target);
}
});
});
Example Here
As a hack, where the DOM is implemented using a prototype inheritance model (most browsers but not all), you can add an iterator to the NodeList constructor:
if (NodeList && NodeList.prototype && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function(callback, thisArg) {
Array.prototype.forEach.call(this, callback, thisArg)
}
}
Then:
document.getElementsByClassName('item').forEach(function(el){
el.addEventListener('click', someFn, false);
})
or
document.querySelectorAll('.item').forEach(function(el){
el.addEventListener('click', someFn, false);
})
Of course you shouldn't do this in production on the web (don't mess with host objects and all that), but wouldn't it be nice if it was OK? Or iterators were added to DOM lists and collections?
var elems = document.querySelectorAll('.className');
var values = Array.prototype.map.call(elems, function(obj) {
return obj.onclick = function(e){ //do stuff };
});
This (adapted example from MDN) is how you could do it without a traditional loop.

Can I know if element is in document or not?

var myElement = document.querySelector('div.example');// <div..></div>
/*
* A lot time after, codes executed, whatever
*/
if( myElement.isInDocument )
{
// Do something
}
Is there a easy way to know if 'myElement' still in document?
From Mozilla:
function isInPage(node) {
return (node === document.body) ? false : document.body.contains(node);
}
Since every element in the document is a child of the document, check to see if your element is:
function isInDocument(e) {
while( e.parentNode) e = e.parentNode;
return e === document;
}
One way is to use contains()
var myElement = document.querySelector('div.example');
console.log("elment ", myElement);
console.log("contains before ", document.body.contains(myElement));
myElement.parentNode.removeChild(myElement);
console.log("contains after ", document.body.contains(myElement));
JSFiddle
You can first see if the .contains() method exists and use it if available. If not, walk the parent chain looking for the document object. From a prior project using code like this, you can't just rely on parentNode being empty (in some versions of IE) when you get to document so you have to also explicitly check for document like this:
function isInDocument(e) {
if (document.contains) {
return document.contains(e);
} else {
while (e.parentNode && e !== document) {
e = e.parentNode;
}
return e === document;
}
}
For modern browsers the answer is myElement.isConnected
https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected

Categories