Drew Noakes
Drew Noakes

Reputation: 310907

Detecting when a DOM element's parent changes

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);

Upvotes: 2

Views: 3747

Answers (3)

ZiyadCodes
ZiyadCodes

Reputation: 428

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

Upvotes: 3

Martin Ernst
Martin Ernst

Reputation: 3279

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);

Upvotes: 1

PiSquared
PiSquared

Reputation: 29

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.

Upvotes: 2

Related Questions