I need to dynamically update the content of an element, and then perform an animation once the element and its content are loaded. In this particular case the element has an opacity of 0, and once the content is loaded I set its opacity to 1 with a CSS transition to fade it in. The element's content has a large number of images, in both <img>
tags and as background-image
s of sub elements. It is very important that the animation is only allowed to start once all the images and background-images have loaded. Otherwise the fade-in effect is broken, as empty placeholder elements are animated in with their image content appearing an instant later, as shown below.
To determine when the element's content is loaded I am using a MutationObserver
to observe the element and fire a callback that changes its opacity when a change is detected.
var page = document.getElementById( "page" );
var observer = new MutationObserver( function(){
page.style.opacity = "1"; // begin animation on change
} );
var config = { characterData: true,
attributes: false,
childList: false,
subtree: true };
observer.observe( page, config );
page.innerHTML = newContent; // update content
However, by itself a MutationObserver
is not a comprehensive solution to the requirement. As the MDN page states:
The MutationObserver method observe() configures the MutationObserver callback to begin receiving notifications of changes to the DOM that match the given options.
DOM changes fire once the subtree is modified and when all resources that would prevent the DOM from being parsed are loaded. This does not include images. Thus, MutationObserver
s will fire the callback without regard as to whether the content's images are fully loaded.
If I set the childList
parameter to true
, within the callback I could look at every child that changes and attach a load
event listener to each <img>
, keeping track of the total number that I attach. Once all the load
events have fired I would know that every image has loaded.
var page = document.getElementById( "page" ),
total = 0,
counter = 0;
var callback = function( mutationsList, observer ){
for( var mutation of mutationsList ) {
if ( mutation.type == 'childList' ) {
Array.prototype.forEach.call( mutation.target.children, function ( child ) {
if ( child.tagName === "IMG" ) {
total++; // keep track of total load events
child.addEventListener( 'load', function() {
counter++; // keep track of how many events have fired
if ( counter >= total ) {
page.style.opacity = "1";
}
}, false );
}
} );
}
}
}
var observer = new MutationObserver( callback );
var config = { characterData: true,
attributes: false,
childList: true,
subtree: true };
observer.observe( page, config );
page.innerHTML = newContent; // update content
However, there are elements with background images that must also be loaded. Since load
cannot be attached to such elements, this solution is useless when detecting if background-images have loaded.
How can I use a MutationObserver
to determine when all resources of a dynamically updated element are loaded? In particular, how can I use it to ascertain that implicitly dependent resources such as background images have loaded?
If this cannot be achieved with MutationObserver
s, how else could this be achieved? Using setTimeout
to delay the animation is not a solution.
A solution was suggested in the comments to detect the background images loading. Although the exact implementation was not suitable as I'm not using jQuery, the method was transferable enough to work with a MutationObserver
.
The MutationObserver
must detect changes to the element and all its children. Every child that changes must be investigated. If the child is an image then a load
event listener was attached to it, keeping track of the total number that I attach. If the child was not an image, it was checked to see if it had a background image. If so, a new image was created, a load
event listener attached as above, and the src
of the image was set to the child's backgroundImage
url. Browser caching meant that once the new image had loaded, so too would the background image.
Once the number of load
events that fired was equal to the total number of events attached, I knew that all images were loaded.
const page = document.getElementById( "page" )
let total = 0
let counter = 0
const mutCallback = ( mutationsList, observer ) => {
for( const mutation of mutationsList ) {
if ( mutation.type === 'childList' ) {
for ( const child of mutation.target.children ) {
const style = child.currentStyle || window.getComputedStyle( child, false )
if ( child.tagName === "IMG" ) {
total++ // keep track of total load events
child.addEventListener( 'load', loaded, false )
} else if ( style.backgroundImage !== "none" ) {
total++; // keep track of total load events
const img = new Image()
img.addEventListener( 'load', loaded, false )
// extract background image url and add as img src
img.src = style.backgroundImage.slice( 4, -1).replace(/"/g, "" )
}
} );
}
}
function loaded() {
counter++; // keep track of how many events have fired
if ( counter >= total ) {
// All images have loaded so can do final logic here
page.style.opacity = "1";
}
}
}
const observer = new MutationObserver( mutCallback )
const config = {
characterData: true,
attributes: false,
childList: true,
subtree: true
}
observer.observe( page, config );
// update content, and now it will be observed
page.innerHTML = newContent;
This solution seems to be a bit of a hack. A new case needs to be implemented for each type of resource. For example, if I also needed to wait until scripts had loaded, I would have to identify the scripts and attach load
event listeners to them, or if I had to wait for fonts to load I would have to write a case for them.
It would be great if there was a native function to determine when all resources have loaded, rather than attaching event listeners to everything. Service workers look interesting, but for now I think the hackish solution is the easiest and most reliable.