Web Component offsetHeight/offsetWidth zero when connected

I am trying to convert a library of mine into a web component and I have run into a problem with when to tell the component has been laid out.

Web Components have a callback called connectedCallback which looks like it might be what I need, but when inspecting self in the callback, my offset dimensions are both zero:

connectedCallback() {
  console.log(this.offsetWidth, this.offsetHeight);
}

When I queue up the dimensions query, the dimensions are there, so sometime between the web component connecting and the event queue reaching the next item, the web component gets laid out by the browser engine, gaining correct dimension values:

connectedCallback() {
  setTimeout(() => console.log(this.offsetWidth, this.offsetHeight), 0);
}

I am wondering if there is a callback to tell when the layout event has happened?

Upvotes: 2

Views: 652

Answers (2)

Those values depend on the whole DOM so need a DOM repaint.. which hasn't occured yet.

If every element would trigger a DOM repaint we would all be complaining about performance.

The Browser decides (browser specific logic) when to do a DOM repaint.

Usual and accepted way to execute code after a DOM repaint is a setTimeout( FUNC , 0 )

so strictly speaking this does NOT force a DOM repaint, it is still the Browser deciding when to repaint
If you have a long running script.... FUNC will execute after the long running script is done.

Yes an Event would be nice...
but that takes CPU cycles... and in 997 out of a 1000 cases you don't need the info
.. thus there is NO Event

So in the 0,03% of cases where you do need DOM info you do a setTimeout

Note that this is not Custom Element specific, for appendChild calls the same applies.

If you have a sh*pload of components that need this offset info you might want to look into a Framework that does virtual DOM diffing

More background: Medium: What the heck is repaint/reflow (2019)

Upvotes: 2

Intervalia
Intervalia

Reputation: 10975

I created a simple component without shadowDOM. I wrote the following code in a constructor:

this.addEventListener('readystatechange', (evt) => {console.log('readystatechange', evt)});
this.addEventListener('load', (evt) => {console.log('load', evt)});
this.addEventListener('progress', (evt) => {console.log('progress', evt)});
this.addEventListener('DOMContentLoaded', (evt) => {console.log('DOMContentLoaded', evt)});

And got no output. So none of these events trigger on a Web component.

Then I added this code outside the component:

function callback(mutationsList, observer) {
  for(var mutation of mutationsList) {
    if (mutation.type === 'childList') {
      if (mutation.removedNodes.length) {
        console.log(`${mutation.removedNodes.length} child node(s) have been removed.`);
      }

      if (mutation.addedNodes.length) {
        console.log(`${mutation.addedNodes.length} child node(s) have been added.`);
      }
    }
  }
}
var config = { childList: true };
var observer = new MutationObserver(callback);

And this line to the constructor:

observer.observe(this, config);

On my particular component my callback was never called.

I did have a different component that changes its children and the callback was called.

So, I don't see any event or even mutation observer that triggers when the component is fully loaded.

Personally I would use a setTimeout like you did since that seems to be the only sure way of getting a call when the component is done. Although you may have to be specific where you put your setTimeout to make sure you have fully rendered your DOM.

Upvotes: 2

Related Questions