98

I need to accurately measure the dimensions of text within my web app, which I am achieving by creating an element (with relevant CSS classes), setting its innerHTML then adding it to the container using appendChild.

After doing this, there is a wait before the element has been rendered and its offsetWidth can be read to find out how wide the text is.

Currently, I'm using setTimeout(processText, 100) to wait until the render is complete.

Is there any callback I can listen to, or a more reliable way of telling when an element I have created has been rendered?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
funkybro
  • 8,432
  • 6
  • 39
  • 52
  • 2
    In case someone finds this question, good answers can be found here http://stackoverflow.com/a/5629730/697388 and here http://stackoverflow.com/a/850995/697388 – Georgii Ivankin Apr 23 '13 at 08:25
  • possible duplicate of [How to check if element exists in the visible DOM?](http://stackoverflow.com/questions/5629684/how-to-check-if-element-exists-in-the-visible-dom) – iConnor Sep 15 '13 at 16:13
  • possible duplicate of [Is is possible to determine when an element has been rendered using JavaScript?](http://stackoverflow.com/questions/3667991/is-is-possible-to-determine-when-an-element-has-been-rendered-using-javascript) – Purefan Jan 10 '14 at 11:25

8 Answers8

86

The accepted answer is from 2014 and is now outdated. A setTimeout may work, but it's not the cleanest and it doesn't necessarily guarantee that the element has been added to the DOM.

As of 2018, a MutationObserver is what you should use to detect when an element has been added to the DOM. MutationObservers are now widely supported across all modern browsers (Chrome 26+, Firefox 14+, IE11, Edge, Opera 15+, etc).

When an element has been added to the DOM, you will be able to retrieve its actual dimensions.

Here's a simple example of how you can use a MutationObserver to listen for when an element is added to the DOM.

For brevity, I'm using jQuery syntax to build the node and insert it into the DOM.

var myElement = $("<div>hello world</div>")[0];

var observer = new MutationObserver(function(mutations) {
   if (document.contains(myElement)) {
        console.log("It's in the DOM!");
        observer.disconnect();
    }
});

observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});

$("body").append(myElement); // console.log: It's in the DOM!

The observer event handler will trigger whenever any node is added or removed from the document. Inside the handler, we then perform a contains check to determine if myElement is now in the document.

You don't need to iterate over each MutationRecord stored in mutations because you can perform the document.contains check directly upon myElement.

To improve performance, replace document with the specific element that will contain myElement in the DOM.

Elliot B.
  • 17,060
  • 10
  • 80
  • 101
  • @ErikGrosskurth document.contains works with `TextNode` as well. Here's an example: jsfiddle.net/ef9yub3y But according to the MDN, it might not work with older IE. If you're having some trouble with your implementation, feel free to post on SO and I'd be happy to take a look. – Elliot B. Mar 06 '18 at 18:00
  • 2
    The thing with text is the dom element is rendered and then the text further mutates which the observer has already entered through to the if statement so the observer holds the initial snapshot. It may just be a chrome thing. Not sure – Erik Grosskurth Mar 06 '18 at 19:32
  • I was using a package and didn't know when it was going to load my html into its innards. I tossed an "if (document.getElementById..." before the ".contains", and it worked great! – wordragon Mar 24 '20 at 15:04
  • 12
    OP wanted the time when the element has been "rendered". When the mutation record fires you can be pretty sure that the element has NOT been rendered yet. It is in the DOM yes, but you are just one microtask after the synchronous call to append(), and the renderer didn't had a chance to kick in yet. `setTimeout()` indeed is not a good solution, but it's obligatory closer to the rendering than mutation records. And if they were only looking to know when the element is in the DOM, it's easy: right after the call to append(). – Kaiido Jun 08 '21 at 08:04
  • In case you are looking for the Angular version of this - https://nitayneeman.com/posts/listening-to-dom-changes-using-mutationobserver-in-angular/ – Mauricio Gracia Gutierrez Aug 15 '21 at 07:08
  • I think it is more complex and hard to use in parcite. – JaeIL Ryu Feb 25 '22 at 03:00
62

There is currently no DOM event indicating that an element has been fully rendered (eg. attached CSS applied and drawn). This can make some DOM manipulation code return wrong or random results (like getting the height of an element).

Using setTimeout to give the browser some overhead for rendering is the simplest way. Using

setTimeout(function(){}, 0)

is perhaps the most practically accurate, as it puts your code at the end of the active browser event queue without any more delay - in other words your code is queued right after the render operation (and all other operations happening at the time).

zrooda
  • 3,610
  • 1
  • 30
  • 38
  • 7
    worth pointing out that sometimes you will need to pass in some time. I had an issue with trying to obtain the scroll height of a container, and the property was sometimes 0 and sometimes correct. Adding a 500 millisecond delay to grabbing this property fixed the issue. – markthewizard1234 Nov 24 '17 at 15:16
  • Sounds like you're doing more than fits into one repaint, @markthewizard1234. – zrooda Nov 29 '17 at 14:38
  • 2
    I think this answer as accepted without the complete truth to it. The OP asked for 'rendered event' and this could (in theory, if it works) only let you know that the element is in the DOM, but not that all rendering has been completed. I wonder if [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) would do what is needed however? – oliver Nov 30 '21 at 19:06
  • @oliver ResizeObserver reports its first event when the observer is initialized, which can be at any point in time. Can you elaborate on the lack of truth? – zrooda Dec 15 '21 at 13:26
  • Nice advice! It working in next sequence: 1. i'm change dom element, for example - remove class. 2. setTimeout(doNext, 0). In `doNext` function my expectation, that a dom element changes implemented and rendered - come true. – Sergio Belevskij Dec 22 '21 at 19:09
  • It works, but it is more intuitive. https://stackoverflow.com/a/67621688/12365658 – JaeIL Ryu Feb 25 '22 at 03:05
  • 2
    none of this is working on a slow machine. not even the ResizeObserver works. it is also triggers at the wrong moment. I had to go with a 150ms delay option in the end :( browser bug? (chrome os 98) – Zectbumo Mar 05 '22 at 20:17
  • In practice it doesn't always happen that the element added to the DOM will render immediately, so a setTimeout with a 0 timer will not necessarily give you the exact time when the element has finished getting rendered. Example: if you add a PDF HTML object to the DOM dynamically, browsers take about 100 milliseconds or a bit more before they finish rendering the file, especially if it's large. So you won't see the content inside it for a while. This is very much visible. It can add a flicker to what you see on the screen in that specific box. This might also vary per machine. – dolanator Apr 25 '23 at 22:05
24

This blog post By Swizec Teller, suggests using requestAnimationFrame, and checking for the size of the element.

function try_do_some_stuff() {
    if (!$("#element").size()) {
        window.requestAnimationFrame(try_do_some_stuff);
    } else {
        $("#element").do_some_stuff();
    }
};

in practice it only ever retries once. Because no matter what, by the next render frame, whether it comes in a 60th of a second, or a minute, the element will have been rendered.

You actually need to wait yet a bit after to get the after render time. requestAnimationFrame fires before the next paint. So requestAnimationFrame(()=>setTimeout(onrender, 0)) is right after the element has been rendered.

spongebob
  • 8,370
  • 15
  • 50
  • 83
Yuval A.
  • 5,849
  • 11
  • 51
  • 63
  • Also, 'size' didn't work for me, instead, I had to check the height of the element (which was an editable div) if (!$("#element").height()) – Treadmeister Dec 12 '19 at 13:55
16

In my case solutions like setTimeout or MutationObserver weren't totaly realiable. Instead I used the ResizeObserver. According to MDN:

Implementations should, if they follow the specification, invoke resize events before paint and after layout.

So basically the observer always fires after layout, thus we should be able to get the correct dimensions of the observed element. As a bonus the observer already returns the dimensions of the element. Therefore we don't even need to call something like offsetWidth (even though it should work too)

const myElement = document.createElement("div");
myElement.textContent = "test string";

const resizeObserver = new ResizeObserver(entries => {
  const lastEntry = entries.pop();

  // alternatively use contentBoxSize here
  // Note: older versions of Firefox (<= 91) provided a single size object instead of an array of sizes
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1689645
  const width = lastEntry.borderBoxSize?.inlineSize ?? lastEntry.borderBoxSize[0].inlineSize;
  const height = lastEntry.borderBoxSize?.blockSize ?? lastEntry.borderBoxSize[0].blockSize;

  resizeObserver.disconnect();

  console.log("width:", width, "height:", height);
});

resizeObserver.observe(myElement);

document.body.append(myElement);

This can also we wrapped in a handy async function like this:

function appendAwaitLayout(parent, element) {
  return new Promise((resolve, reject) => {
    const resizeObserver = new ResizeObserver((entries) => {
      resizeObserver.disconnect();
      resolve(entries);
    });

    resizeObserver.observe(element);

    parent.append(element);
  });
}

// call it like this
appendAwaitLayout(document.body, document.createElement("div")).then((entries) => {
  console.log(entries)
  // do stuff here ...
});
Robbendebiene
  • 4,215
  • 3
  • 28
  • 35
  • Thanks! This worked perfectly for me. If I close and then reopen the browser, the document.documentElement's clientWidth and clientHeight were not initialized, even after running after the `load` event. I used this resizeObserver with a Promise to let my code await the resize event before proceeding. For anyone who'd like to see which screen dimension values are initialized before and after initial layout, use the code values from [Get the size of the screen, current web page and browser window](https://stackoverflow.com/a/62278401/1238901). – jigritsn Jun 15 '23 at 21:52
  • As the question is about "accurately measure the dimensions of text within my web app", this is the best answer. Better than setTimeout, requestAnimation, and MutationObserver – Chester Fung Aug 18 '23 at 09:09
9

The MutationObserver is probably the best approach, but here's a simple alternative that may work

I had some javascript that built the HTML for a large table and set the innerHTML of a div to the generated HTML. If I fetched Date() immediately after setting the innerHTML, I found that the timestamp was for a time prior to the table being completely rendered. I wanted to know how long the rendering was taking (meaning I needed to check Date() after the rendering was done). I found I could do this by setting the innerHTML of the div and then (in the same script) calling the click method of some button on the page. The click handler would get executed only after the HTML was fully rendered, not just after the innerHTML property of div got set. I verified this by comparing the Date() value generated by the click handler to the Date() value retrieved by the script that was setting the innerHTML property of the div.

Hope someone finds this useful

Marcello B.
  • 4,177
  • 11
  • 45
  • 65
hanksterr7
  • 105
  • 1
  • 4
1

suppose your element has classname class="test" The following function continue test if change has occured if it does, run the function

        function addResizeListener(elem, fun) {
            let id;
            let style = getComputedStyle(elem);
            let wid = style.width;
            let hei = style.height;
            id = requestAnimationFrame(test)
            function test() {
                let newStyle = getComputedStyle(elem);
                if (wid !== newStyle.width ||
                    hei !== newStyle.height) {
                    fun();
                    wid = newStyle.width;
                    hei = newStyle.height;
                }
                id = requestAnimationFrame(test);
            }
        }
        let test = document.querySelector('.test');
        addResizeListener(test,function () {
            console.log("I changed!!")
        });
relaxslow
  • 29
  • 5
0

when you make for example

var clonedForm = $('#empty_form_to_clone').clone(true)[0];

var newForm = $(clonedForm).html().replace(/__prefix__/g, next_index_id_form);

// next_index_id_form is just a integer 

What am I doing here?
I clone a element already rendered and change the html to be rendered.

Next i append that text to a container.

$('#container_id').append(newForm);

The problem comes when i want to add a event handler to a button inside newForm, WELL, just use ready event.

$(clonedForm).ready(function(event){
   addEventHandlerToFormButton();
})

I hope this help you.

PS: Sorry for my English.

0

According to @Elliot B.'s answer, I made a plan that suits me.

const callback = () => {
  const el = document.querySelector('#a');
  if (el) {
    observer.disconnect();
    el.addEventListener('click', () => {});
  }
};

const observer = new MutationObserver(callback);
observer.observe(document.body, { subtree: true, childList: true });
weiya ou
  • 2,730
  • 1
  • 16
  • 24