3

As soon as I try to inject raw html in the document's body, a saved instance of an element retrieved with .querySelector(); abruptly resets it's .clientHeight and .clientWidth properties.

The following page shows the problem

<head>

<script>
    window.addEventListener('load', pageInit);

    function pageInit() {
        // saving instance for later use
        var element = document.querySelector('#element')

        alert(element.clientHeight);   // returns 40

        document.querySelector('body').innerHTML += '<p>anything</p>';

        alert(document.querySelector('#element').clientHeight);  // returns 40
        alert(element.clientHeight);  // returns 0
    }
</script>

<style>
    #element {
        height: 40px;
    }
</style>
</head>

<body>

    <div id="element"></div>

</body>

Why exactly the instance properties of the element variable gets reset? As pointed in this question addEventListener gone after appending innerHTML eventListeners gets detached but this still doesn't explain why that element node still exist and why it's properties were zeroed out.

Community
  • 1
  • 1
Row Rebel
  • 267
  • 3
  • 11
  • 1
    Note that [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) returns an Element; [`querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) returns a non-live `NodeList`. – Heretic Monkey Mar 27 '17 at 21:11
  • thanks for pointing it out, i'll edit the question – Row Rebel Mar 27 '17 at 21:22
  • Possible duplicate of [addEventListener gone after appending innerHTML](http://stackoverflow.com/questions/2684956/addeventlistener-gone-after-appending-innerhtml) – Andre Figueiredo Mar 27 '17 at 21:54

2 Answers2

3

It would seem that when you reset the innerHTML of the body element, you're causing the entire page to be re-rendered. Thus, the div#element that is shown on the page is not the same element that the JavaScript variable element points to; rather, the one you see is a new element that was created when the page was re-rendered. However, the div#element that element points to still exists; it hasn't yet been destroyed by the browser's garbage collector.

To prove this, try replacing the last alert (the one that alerts a 0) with console.log(element), and then attempting to click on the element in the console window. You'll see <div id="element"></div>, but when you click on it, nothing happens, since the div is not on the page.

Instead, what you want to do is the following:

const newEl = document.createElement('p');
newEl.innerHTML = 'anything';
document.querySelector('body').appendChild(newEl);

instead of setting the body.innerHTML property.

FYI you should probably switch the alerts for console.logs, since alerts annoy the hell out of people and the console is the way to test things out in development.

joepin
  • 1,418
  • 1
  • 16
  • 17
  • the posted snippet was just a debug refactoring of a larger website in which I preferred to directly inject raw svg symbol definitions at the end of the body – Row Rebel Mar 27 '17 at 21:47
  • or also: `document.querySelector('body').insertAdjacentHTML("beforeend", '

    anything

    ');` or any other non-destructive insertion method. - Anyway, yes, in other words, the most important part is to understand that `.innerHTML +=` will 1: get a string representation of the element's innerHTML, 2. append another string to it and 3. return that composite **string** into the element's *inner 'body'* thus (as rightly pointed out) will make any previous references useless.
    – Roko C. Buljan Mar 27 '17 at 21:49
  • Gotcha. I just assumed you wanted to add elements to the body, my bad – joepin Mar 27 '17 at 21:50
  • @RokoC.Buljan here's something interesting - when I tested appending a new element to the body, the old reference to the `div#element` now pointed to the new element automatically. I'm gonna assume, then, that chrome was smart enough to realize that the old div already exists, and it doesn't need to re-create it. Just thought this was cool enough to share :) – joepin Mar 27 '17 at 21:53
  • chrome version 56.0.2924.87 still replicates the same behaviour, I guess I'll just stick to non-destructive insertions from now on – Row Rebel Mar 27 '17 at 22:03
1

You're running into a problem where the browser has reflowed the doc and dumped its saved properties. Also known as Layout Thrashing. Accessing certain DOM properties (and/or calling certain DOM methods) "will trigger the browser to synchronously calculate the style and layout". That quote is from Paul Irish's Comprehensive list of what forces layout/reflow. Though innerHTML isn't included there, pretty sure that's the culprit.

In your sample, the first alert works for obvious reasons. The second works because you're asking the browser to go and find the element and get the property again. The third fails because you're relying on a stored value (that's no longer stored).

The simplest way around it is to use requestAnimationFrame when using methods/properties that will force a reflow.

  window.addEventListener('load', pageInit); 
  function pageInit() {

      // saving instance for later use
      var element = document.querySelector('#element');

      console.log("before:",  element.clientHeight);   // returns 40

      requestAnimationFrame(function() {
        document.querySelector('body').innerHTML += '<p>anything</p>';
      });

      console.log("WITH rAF after:", element.clientHeight);  // returns ~0~ 40!
      
      // out in the wild again
      document.querySelector('body').innerHTML += '<p>anything</p>';
      
      // oh no!
      console.warn("W/O rAF after:", element.clientHeight);  // returns 0 

  }
#element {
   height: 40px;
}
<div id="element"></div>
Will
  • 4,075
  • 1
  • 17
  • 28