2

Here is an example where I use the method element.appendChild() and then continue as if the element has been appended synchronously.

But as the logs show, it has not. At least all browser calculations related to its rendering have not been completed.

How can I wait for the element to have fully loaded before continuing execution? Of course sleeping 1000 ms works in this case, but that is not reliable in the generalized case.

const serializedSvg = `<svg style="width:100%;height:100%;">
  <line x1="25%" y1="25%" x2="75%" y2="75%" style="stroke: rgb(234, 243, 234);stroke-width: 5;"></line>
</svg>`

const svgDom = new DOMParser().parseFromString(serializedSvg, 'text/html').querySelector('svg')
const container = document.querySelector("#container")
container.appendChild(svgDom)

const lineEl = document.querySelector('line')
console.log("fail:" + lineEl.x1.baseVal.value)
setTimeout(() => {
 console.log("success:" + lineEl.x1.baseVal.value)
}, 1000)
<div id="container">

</div>
user1283776
  • 19,640
  • 49
  • 136
  • 276

4 Answers4

0

To clarify, it is not that the svg is not loaded as explained by the two links about reflow and SVG properties. The values are behaving as intended as stated in the specs.

A possibility, so that you can replicate the accuracy of setTimeout(1000) taking into account any layout changes due to order of operations and be completely "accurate" instead of depending on non-standard behavior, would be to use MutationObserver or ResizeObserver:
https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver

In practice, I would recommend just using setTimeout(0) as has been suggested and live with it, because basically all browsers will behave correctly (kind of like how object properties can be traversed in order). You can trigger a reflow whenever you update the layout to make sure those values are correct when they are run, and make sure the SVG stuff runs only after everything has been rendered and you've triggered a layout.
I'm guessing setTimeout(1000) is working for you because some other layout affecting code is running afterwards, just like how the values on these snippets are off by a couple pixels on here when console/preview is hidden/shown due to layout changes when the console/preview div is hidden or shown.

What forces layout/reflow: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
SVG properties: https://trac.webkit.org/wiki/SVG%20properties

According to the document on SVG properties the attributes are lazily calculated until required, and are taken based on a "tear-off" snapshot (x1 is a SVGAnimatedLength object).

Forcing reflow below, which satsifies "until required", so that the percentages are calculated and updated before accessing them.

const serializedSvg = `<svg style="width:100%;height:100%;"">
  <line x1="25%" y1="25%" x2="75%" y2="75%" style="stroke: rgb(234, 243, 234);stroke-width: 5;"></line>
</svg>`

const svgDom = new DOMParser().parseFromString(serializedSvg, 'text/html').querySelector('svg')
const container = document.querySelector("#container")
container.appendChild(svgDom)

const lineEl = svgDom.querySelector('line')

// force layout/reflow
lineEl.getBBox()

console.log("fail:" + lineEl.x1.baseVal.value)

setTimeout(() => {
 console.log("success:" + lineEl.x1.baseVal.value)
}, 1000)
<div id="container">

</div>
user120242
  • 14,918
  • 3
  • 38
  • 52
  • Just to further explain what's happening, % values depend on layout. But the x1 value has been essentially cached using the "tear-off" value, and there is nothing to trigger it updating the calculated x1 SVGAnimatedLength property, which is "updated from the reflection SVG property when required". Which is why I have suggested triggering a layout/reflow which should guarantee that the value is up to date on retrieval. – user120242 May 13 '20 at 15:07
  • 1
    This is the only of the solutions proposed this far that seems to consistently work with various examples in my real application. I want to give you an extra thank you for providing so much context with you answer. I inserted an svg.getBBox() to force reflow. – user1283776 May 14 '20 at 06:10
-1

You can use Promise.resolve().then() for getting the async values. The .resolve() will pass the data into .then() when it's completed. You can do something like:

const lineEl = document.querySelector('line');

Promise.resolve(lineEl).then(res => {
    console.log(res.x1.baseVal.value);
});
Sajeeb Ahamed
  • 6,070
  • 2
  • 21
  • 30
  • Why would that work reliably? The querySelector method doesn't return a promise. I would think that the reason it might work in the given example is that it will cause the console.log statement to execute in the next event loop cycle which incidentally is enough time for the line to load. – user1283776 May 13 '20 at 13:01
-1

Already mentioned in one of the comments: The Event Loop

Read: https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif

Basically it comes down to the Browser engine optimizing what is written to the DOM.

That means some statements are bundled and you can't read something immediately after it was written to the DOM

So your code is a valid way to wait for the Event Loop to finish

setTimeout(() => {
    console.log("success:" + lineEl.x1.baseVal.value)
}, 1000)

AND You can change that 1000 to 0 !!!

You could also use requestAnimationFrame, it does the same: Waits for the Event Loop

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • 2
    Just want to note that this notion (which has been repeated often) can be disproven. Try using rAF and you will see that it is inconsistent and will sometimes return 0. Event Loop delay only works because (basically all) browsers prioritize UI updates, but it is theoretically not accurate. – user120242 May 13 '20 at 14:52
  • 1
    I presume you only mean ``requestAnimationFrame``. The **setTimeout 0** does work (in practice)? – Danny '365CSI' Engelman May 13 '20 at 15:08
  • PS, I don't know who **-1** all the other answers, wasn't me – Danny '365CSI' Engelman May 13 '20 at 15:10
  • Maybe the OP. He's probably not satisfied with the answers. What he's probably "wanting" is a hook for layout that delays or changes the values fed to his SVG manipulation code that will guarantee his code uses the latest values of what is rendered on the page. Unfortunately that would still require a time machine that can take the values retrieved after layout and place them before his code executes. – user120242 May 13 '20 at 16:07
  • Ah yes... it will happen once JavaScript ``0.1 + 0.2`` outputs the correct answer... – Danny '365CSI' Engelman May 13 '20 at 17:56
-2

You need to wait for the load event for the document you're inserting into.

const serializedSvg = `<svg style="width:100%;height:100%;">
  <line x1="25%" y1="25%" x2="75%" y2="75%" style="stroke: rgb(234, 243, 234);stroke-width: 5;"></line>
</svg>`

const svgDom = new DOMParser().parseFromString(serializedSvg, 'text/html').querySelector('svg')
const container = document.querySelector("#container")
container.appendChild(svgDom)

window.addEventListener("load", function(){
const lineEl = document.querySelector('line')
console.log("success:" + lineEl.x1.baseVal.value)
}); 
<div id="container">

</div>
Robert Longson
  • 118,664
  • 26
  • 252
  • 242