Efficiently Detecting When an Element Changes Size

While working on MediaFaker, an experimental idea for displaying the effects of media queries inside any DOM node without the use of iframes, I needed a method of detecting when an element changes size. My initial thoughts lead me to a pretty naive solution involving requestAnimationFrame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hasChangedSize(rect1, rect2) {
  return rect1.width != rect2.width || rect1.height != rect2.height;
}

function watchForResize(element, cb) {
    var startingSize = element.getBoundingClientRect();

    function checkForRezize() {
        var currentSize = element.getBoundingClientRect();
        if(hasChangedSize(startingSize, currentSize)) {
            startingSize = currentSize;
            cb();
        }
        requestAnimationFrame(checkForRezize);
    }
    requestAnimationFrame(checkForRezize);
}

For a rough prototype this method worked but I knew that this would not scale well when watching many elements. This method could be optimised to remove additional requestAnimationFrame calls when watching more than one element but it would still be making calls to getBoundingClientRect each frame which is on the naughty list of things to avoid causing layout / reflow. As you can see from one of my tests with 50 elements there is a lot of work being done 60 times a second.

What I wanted was a method that wouldn’t require the use of requestAnimationFrame or getBoundingClientRect. So I did what all good developers would do, I thought long and hard about the problem and used my extensive domain knowledge to come up with a solution that’s simple and elegant. I googled it. I found an extremely interesting approach by Marc J. Schmidt in his CSS-Element-Queries polyfill. The ResizeSensor module uses only native scroll events to detect when an element changes size, How I thought?

The method that the ResizeSensor uses works by adding hidden children to the element.

1
2
3
4
5
6
7
8
<div class="resize-sensor" style="position: absolute;
                                  left: 0px; top: 0px; right: 0px; bottom: 0px;
                                  overflow: hidden; z-index: -1; visibility: hidden;">
    <div style="position: absolute;
                left: 0px; top: 0px; 
                transition: 0s;
                width: 10000px; height: 10000px;"></div>
</div>

These elements consist of a containing div, positioned absolutely covering the entire parent element. As well as a child element that is also positioned absolutely however this child has its height and width set to 10000px making its huge.

How does this help us determine if an element has changed size though? The massive child element overflows its parent causing the parent to have scrollbars, these are hidden with overflow auto.

This is where the solution gets clever, once the hidden element are added to the DOM the javascript sets the parents scroll offset (scrollLeft and scrollTop properties) to 10000 effectively scrolling all the way to the end. The actual scroll offset set wont actually be 10000 though because the maximum scroll offset is 10000px - elementSize = maximum scroll offset. Noticing this formular can tell what will happen when the element changes size.

When the element changes size so does the scroll offset because we are already at the maximum, which in turn triggers an onScroll event!

Pretty neat.

Save .json Files From Chrome Developer Tools

I spend a lot of my time inside of chrome developer tools and using the javascript console trying things out and testing various things I am working on. The other day I wanted to persist some data that I had built up while playing around in chrome javascript console but I couldn’t find an easy way other than JSON.stringify and copy&paste. So I decided to extend the global console object to include a save method that would save any object I passed to it as a json file. Here is the code I wrote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(function(console){
  console.save = function(data, filename){
    if(!data) {
      console.error('Console.save: No data')
      return;
    }

    if(!filename) filename = 'console.json'

    if(typeof data === ""object""){
      data = JSON.stringify(data, undefined, 4)
    }

    var blob = new Blob([data], {type: 'text/json'}),
        e    = document.createEvent('MouseEvents'),
        a    = document.createElement('a')

    a.download = filename
    a.href = window.URL.createObjectURL(blob)
    a.dataset.downloadurl =  ['text/json', a.download, a.href].join(':')
    e.initMouseEvent('click', true, false,
                     window, 0, 0, 0, 0, 0,
                     false, false, false, false, 0, null)
    a.dispatchEvent(e)
  }
})(console)

But I didn’t feel like copy and pasting this code into my console every time I wanted to use it. So I threw together a chrome extension to inject this code into every page. I put all this code up on github check it out.

If you found this method useful Console.save is now part of devtools-snippets which contains even more useful snippets for chromes developer tools.