Posts Tagged ‘DOM’

MutationObserver limitations

The observers

MutationObserver, along with its siblings ResizeObserver and IntersectionObserver, are great tools for working with the DOM. I don’t think there’s necessarily broad usage of these observers in frontend development but, when building applications that need insight into lower-level DOM changes, they are powerful interfaces and allow you to avoid the typical/hacky solution of polling for changes.

Interface

All the observer classes share a common, fairly simple interface. There’s just a few key aspects needed to use them:

  • The constructor takes a callback, which is called whenever the DOM state change corresponding to the class (mutation, resize, etc.) is observed.
  • The observe() method takes a target DOM element to observer

On an observed state change (e.g. mutation) the given callback is called with an appropriate record (e.g. MutationRecord)

Missing context

The records surfaced on an observed state change contain information about the change but nothing around who or what triggered the change. Looking at a comparable scenario, this is typically the case for most DOM events as well, but it’s generally a non-issue because you can reasonably assume that the event was triggered by the user interacting with the browser. When it comes to changes detected by an observer, there’s a bit more ambiguity as to how the change came about and you can’t always assume the change was from the end-user.

Note: yes, you an programmatically force DOM events to be emitted as well (e.g. element.click()) but I think this is almost always an anti-pattern

For example, let’s say you have an application where you allow users to enter content into a contenteditable <div> but you’d also programmatically surface and incorporate content coming from the server (from other internet users using the application). Ideally, you could use a MutationObserver on the <div> to detect changes and see if there’s new content that needs to be sent to the server, but you’d need to distinguish:

  • What changes are coming from the user interacting with the browser
  • What changes are being made programmatically (i.e. coming from other server / coming from other users)

Unfortunately, you can’t make this distinction with the information surfaced in a MutationRecord.

While DOM events don’t necessarily map to an actor model, I tend to think what’s conceptually missing here is knowing the originating actor of the events/message and, in any user-facing system, there’s going to be at least 2 actors:

  • The system
  • The end-user interacting with the system

Once you’re within a system dealing with mutating state, knowing who the originating actor is incredibly valuable information.

Hacking around this

I’m prototyping a hacky, but reasonable, solution for ScratchGraph:

Overall, this seems to work but I hate patterns like this where I have to purposely introduce latency.

Triggering reflow to toggle a CSS3 transition

My previous post on triggering reflow focused on forcing reflow to allow for transitions when an element is put into the DOM render tree. This post doesn’t present any new concepts, but provides an example of triggering reflow, in the same manner, to allow for applying style changes to a DOM element instantly and without a transition, then turning the transition back on.

The changes to an element are typically:

  • Turn off the transition on the element
  • Update some properties of the element (that are part of the transition), and force reflow so that the changes are applied instantly without transition
  • Turn on the transition again

In working with transitions, this scenario has come up frequently for me, as much as dealing with DOM elements being put into the render tree.

One classic example where this becomes necessary is in constructing a circular carousel, as the trick to make it circular is to make the first slide a copy of the last slide – so the carousel starts on the second slide and when you reach the last slide, the carousel instantly jumps to the first slide and slides over to the second slide. This is all invisible to the user, and it appears that the carousel has transitioned smoothly from the last slide to the first slide.

The code for a working circular carousel with 3 slides (plus a copy of the last slide) is presented below, with the classes for the different states of the slides being slide1pre (copy of slide 3), slide1, slide2, and slide3.

Note the point where it is necessary to trigger reflow in order to invisibly jump to the first slide.

.carousel-viewport { width:70px; height:70px; overflow:hidden; }
.carousel-container { width:1000px; }

.carousel-container.notransition { transition:transform 0s; }
.carousel-container.dotransition { transition:transform 0.5s; }

.carousel-container.slide1pre { transform:translateX(0px); }
.carousel-container.slide1 { transform:translateX(-70px); }
.carousel-container.slide2 { transform:translateX(-140px); }
.carousel-container.slide3 { transform:translateX(-210px); }

.test-block { width:50px; height:50px; float:left; margin:10px; color:#fff; background:#222; }
<p><a class="ia-start-color-change" href="#">Rotate carousel</a></p>

<
div class="carousel-viewport">
<
div class="ia-carousel-container carousel-container dotransition slide1">
<
div class="ia-slide-pre test-block">3</div>
<
div class="ia-slide-1 test-block">1</div>
<
div class="ia-slide-2 test-block">2</div>
<
div class="ia-slide-3 test-block">3</div>
</
div>
</
div>
var curSlide = 1; // start at slide1

$('.ia-start-color-change').click(function (event) {

var testBlockElem = $('.ia-carousel-container');

// slide 1 to slide 2
if (curSlide == 1) {
testBlockElem.removeClass(
'slide1').addClass('slide2');
}

// slide 2 to slide 3
if (curSlide == 2) {
testBlockElem.removeClass(
'slide2').addClass('slide3');
}

// slide 3 to slide1pre, then slide1pre to slide1
if (curSlide == 3) {

// turn off transition
testBlockElem.removeClass('dotransition').addClass('notransition');
// go to slide1pre invisibly and instantly (w/o transition)
testBlockElem.removeClass('slide3').addClass('slide1pre');

//
// Force reflow to commit style changes to DOM
// allows us to re-apply transition rule, with the DOM updated to the carousel on slide1pre with no transition rule
//
testBlockElem[0].offsetWidth; // force reflow
//

// re-apply transition rule
testBlockElem.removeClass('notransition').addClass('dotransition');
// transition over to slide1
testBlockElem.removeClass('slide1pre').addClass('slide1');

// reset slide counter
// (note subsequent increment in this function, so we'll be at curSlide=1 when this function finishes)
curSlide = 0;
}

curSlide++;

event.preventDefault();
});

Triggering reflow for CSS3 transitions

There’s a lot written on minimizing reflow of the DOM, but one interesting situation is actually trying to force reflow in order to apply a CSS transition rule when the element is pushed into the DOM render tree (e.g. display:none to display:block, display:inline, etc.).

Problem

.test-block { width:50px; height:50px; background:#000; margin:10px; opacity:1; transition:opacity 0.5s; }
.test-block.invisible { opacity:0; }
.test-block.hide { display:none; }
<div class="ia-fadein-block test-block invisible hide"></div> $('.ia-fadein-block').removeClass('hide');
$(
'.ia-fadein-block').removeClass('invisible');

If we remove the hide and invisible classes from the div (as shown above), you’d assume that the div would transition smoothly to opacity:1 in 0.5s; however, this is not the case, the div will go to opacity:1 instantly.

What’s happening is that the removal of both classes occur prior to the browser adding the div to the DOM render tree; when the div gets rendered, it’s as if it never had the hide nor invisible class.

Reflow needs to be triggered before the invisible class is removed in order for the transition to work as expected.

Deprecated solution: setTimeout(func,0)

A setTimeout() with a delay of 0 was my go-to solution for the longest while. Here, the function argument specified would handle whatever needed to occur after the element was added to the DOM render tree (for the example presented above, this would mean the invisible class being removed in the function specified). The thinking behind using setTimeout() was that reflow would occur at some point in the current execution context and before the execution of the setTimeout() callback. This seemed true for a while, but with one of the Firefox releases last year (I don’t know specifically which) I noticed things breaking, with the Javascript engine executing the callback before reflow occured.

I did some quick hacks to fix existing code by increasing the time span before the timeout was triggered (typically 50ms-100ms), but this made a nasty hack even nastier and I wanted a more reliable solution.

Current solution: Trigger reflow manually

There are a number of calls that will trigger reflow on the DOM, but the most reliable calls, that always force reflow, seem to be the ones that return any sort of measurement that must be calculated (e.g. offsetWidth) on an element. With this in mind, the Javascript code can be modified as follows:

$('.ia-fadein-block').removeClass('hide');

// force reflow
$('.ia-fadein-block')[0].offsetWidth;
//

$('.ia-fadein-block').removeClass('invisible');

The code is for demonstration purposes; for repeated use, the hack to force reflow should really be encapsulated in a function, as there is no guarantee that the reflow behavior triggered by querying offsetWidth will remain consistent.

Ideal solution: API Method to trigger reflow or delay certain style changes post-reflow

The solution presented above is still a hack and may very well break in the future, as there is no guarantee that browsers won’t cache properties like offsetWidth in further attempts to minimize reflow. A robust, future-proof, solution is needed and it’s amazing one hasn’t come to fruition as yet.

A Mozilla bug report documents this issue and David Baron presents some workable solutions.