1

I have a tree of DOM nodes, and delete a few children. I'd like to determine the updated size of the DOM tree.

MutationObserver doesn't work because it notifies on DOM model changes, but the rendering hasn't actually completed yet so the size isn't updated yet.

There are hacky ways to do this (setTimeout(delay), or multiple nested requestAnimationFrame()), but I don't want to run into unexpected sizing issues later on down the line.

Is there any non-hacky way to do this?

hajimemash
  • 11
  • 2
  • 1
    Pretty sure `requestAnimationFrame` followed by `setTimeout` *is* the way to do it, though I too hope there's a nicer way – CertainPerformance Jan 11 '20 at 01:54
  • There is [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) but is not supported by all browsers. – Titus Jan 11 '20 at 02:18

1 Answers1

2

The updating of the box-layout, a.k.a "reflow" can be triggered synchronously, and doesn't require a "full render". You can see this answer for more details on the synchronicity of the rendering operations.

So in your case, you can directly get the new sizes of your elements right after you did your DOM update, because this size getting will itself trigger a reflow; it's not even necessary to use a MutationObserver.

const elem = document.getElementById('elem');
console.log('initial', elem.offsetHeight);
elem.querySelectorAll('p:not(.notme)').forEach(el=>el.remove());
// getting '.offsetHeight' triggers a reflow
console.log('after removing', elem.offsetHeight);
<div id="elem">
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p class="notme">Hello</p> 
</div>

Though beware, triggering a reflow has some costs in terms of performance, so if you fear other things may also modify the DOM afterward, you may want to wait for the painting event-loop iteration, using requestAnimationFrame before calling one of the methods that do trigger it.

const elem = document.getElementById('elem');
console.log('initial', elem.offsetHeight);
elem.querySelectorAll('p:not(.notme)').forEach(el=>el.remove());

// wait the painting frame in case other operations also modify the DOM
requestAnimationFrame(()=> {
  console.log('after removing', elem.offsetHeight);
});
<div id="elem">
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p>Hello</p>
  <p class="notme">Hello</p> 
</div>

Ps: for the ones still interested in the Y of this XY problem, you can have a look at this Q/A which demonstrates the use of the upcoming requestPostAnimationFrame and offers a monkey-patch for it.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Interesting info. None of the reflow-triggering methods (offsetHeight, computedStyle, boundingClientRect), which are called in a chained `then()` block after a DOM element is removed, have the updated value. It seems that only 2+ rAF() calls or a setTimeout(7ms) return the correct one. According to the 3-part redraw process you mentioned in an answer, the value seems to be updated after the second part, reflow. Could some CSS rules be changing it after, such as a selector `.class:first`? – hajimemash Jan 11 '20 at 19:55
  • Please post an [mcve] of what you have. These getters and methods will always have the updated values, so there must be something in your setup that does change the height of your element *after* you got these values. – Kaiido Jan 12 '20 at 01:05