UPDATE: This post has seen a significant change from the first version of the code. It now relies on a much simpler method: a hidden object
element that relays its resize event to your listeners.
DOM Elements! Y U No Resize Event?
During your coding adventures, you may have run into occasions where you wanted to know when an element in your document changed dimensions – basically the window resize event, but on regular elements. Element size changes can occur for many reasons: modifications to CSS width, height, padding, as a response to changes to a parent element’s size, and many more. Before today, you probably thought this was mere unicorn lore, an impossible feat – well buckle up folks, we’re about to throw down the gauntlet.
Eye of Newt, and Toe of Frog
The following is the script provides two methods that take care of everything. To enable our resize listening magic, we inject an object element into the target element, set a list of special styles to hide it from view, and monitor it for resize – it acts as a trigger for alerting us when the target element parent is resized. The first method the script provides is addResizeListener
, it manages all your listeners and monitors the element for resize using the injected object
element. The other method is removeResizeListener
, and it ensures that your listeners are properly detached when you want them removed.
(function(){
var attachEvent = document.attachEvent;
var isIE = navigator.userAgent.match(/Trident/);
console.log(isIE);
var requestFrame = (function(){
var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
function(fn){ return window.setTimeout(fn, 20); };
return function(fn){ return raf(fn); };
})();
var cancelFrame = (function(){
var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
window.clearTimeout;
return function(id){ return cancel(id); };
})();
function resizeListener(e){
var win = e.target || e.srcElement;
if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__);
win.__resizeRAF__ = requestFrame(function(){
var trigger = win.__resizeTrigger__;
trigger.__resizeListeners__.forEach(function(fn){
fn.call(trigger, e);
});
});
}
function objectLoad(e){
this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
}
window.addResizeListener = function(element, fn){
if (!element.__resizeListeners__) {
element.__resizeListeners__ = [];
if (attachEvent) {
element.__resizeTrigger__ = element;
element.attachEvent('onresize', resizeListener);
}
else {
if (getComputedStyle(element).position == 'static') element.style.position = 'relative';
var obj = element.__resizeTrigger__ = document.createElement('object');
obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
obj.__resizeElement__ = element;
obj.onload = objectLoad;
obj.type = 'text/html';
if (isIE) element.appendChild(obj);
obj.data = 'about:blank';
if (!isIE) element.appendChild(obj);
}
}
element.__resizeListeners__.push(fn);
};
window.removeResizeListener = function(element, fn){
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
if (!element.__resizeListeners__.length) {
if (attachEvent) element.detachEvent('onresize', resizeListener);
else {
element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__);
}
}
}
})();
Demo-licious!
Here’s a pseudo code usage of the method.
var myElement = document.getElementById('my_element'),
myResizeFn = function(){
/* do something on resize */
};
addResizeListener(myElement, myResizeFn);
removeResizeListener(myElement, myResizeFn);
Cut to the chase, let’s see this resize thang in action: Demo of resize listeners
Resize ALL The Things!
Now that we’re equipped with a nifty, cross-browser element resize event, what would it be good for? Here’s a few possible uses:
- Resize-proof Web Component UI development
- Per-element responsive design
- Size-based loading of content
- Anything you can imagine!
Leave a Reply
70 Comments on "Cross-Browser, Event-based, Element Resize Detection"
Demo works fine in Firefox on a Win7/64bit machine. And, thanks hugely for this post. Very handy, and now Bookmarked for use.
Yeah, works in Firefox on OSX as well, just not in Chrome or Safari (OSX 10.8.2).
@JayDiablo Just checked it out on Mountain Lion Safari, no problems.
@potch
Interesting, I can’t get it to change the text “No resizing detected yet!” to the “X seconds since the epoch” that it does in firefox. The resizing works, of course, but the resize detection doesn’t. I don’t typically use Safari, so don’t think it’s a setting I have enabled across both Chrome and Safari. Loading the Fiddle in a new window doesn’t seem to help either.
@JayDiablo @potch Try again in Chrome with the latest fiddle, all I changed was setting the list element’s margin instead of the parent element’s padding: http://jsfiddle.net/3QcnQ/26/
Chrome apparently has a layout bug and fails to recompute the list element’s size – this is not something I am causing. You can see it fail to change the block element’s flow width on the old fiddle.
@JayDiablo @potch What’s odd, is that Chrome fails to adjust the width according to the flow, and it is visible, but if you resize the window it is in, the layer rendered will snap the element to the right width.
FYI, GWT uses a similar technique but with scroll events: it creates 2 divs with overflow:scroll and size 100%; within each of them it creates another div. The first is sized a bit more than 100% to account for the scrollbars (computed, in pixels), the second 200%; and their scrollTop and scrollLeft are maximized. Whenever the element’s size changes, the inner scrollable elements are also resized. As a result, their inner elements’ position will change and scroll events will fire.
The source (in Java) with detailed comments about how it all works, can be read at https://code.google.com/p/google-web-toolkit/source/browse/trunk/user/src/com/google/gwt/user/client/ui/ResizeLayoutPanel.java?r=11480 (ImplStandard)
@tbroyer I checked out this implementation and found the events to really lag performance for some reason – it seems like it causes a ton of layer invalidation or something. I’ll look into it further to see if there’s a definitive reason why that seems to be the case.
JohnnyHauser I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
Permonix I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
staaky I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
sdecima That’s awesome! Thank you for the reply and tip. I really appreciate the heads up 🙂
sdecima gjjones I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
csuwldcat Thanks! The new code works great on IE11 (and all other browsers).
Just one thing, there’s a typo on the attachEvent function call, the proper event name for it to work on IE10 and below is “onresize” and not “resize”. I tested it on all browsers to confirm.
csuwldcat provided jsfiddle example isn’t working on ie 10.
looks like problem is in line
if (attachEvent) element.attachEvent(‘resize’, fn);
any ideas how to fix this?
alextat csuwldcatthe event name in the attacheEvent logic for old IE was wrong, it was attaching ‘resize’ instead of ‘onresize’. I’ve changed it in the post, should work fine now.
csuwldcat It isn’t changed in the jsFiddle (or it links to an old version), so there it still doesn’t work.
Really nice work all in all. I’m really surprised there are no standard events for this. Even the new MutationObserver doesn’t observe size changes. So thanks a lot for this!
Could you explain why scroll works here? I’m not quite connecting the dots.
rawbear there are hidden divs with scrollbars, resizing causes them
… ok, got it, rAF is used to postpone handler invocation so that
it’s invoked just one time for multiple consecutive scroll events (e.g.
both triggers fired). Still can’t understand why expand-trigger is
needed though, i.e. can’t quite imagine the situation where
contract-trigger doesn’t fire.
csuwldcat tbroyer- I am trying to understand how this would work, but it lags, and also, this method only works if I down-size the element, not if it’s size becomes greater than before…
http://jsbin.com/nahulose/1/edit
vsync csuwldcattbroyerI don’t see that issue, and your jsbin example doesn’t have any of the code in it – what is that testing? The method now uses a mechanism similar to what @tbroyer alluded to, and I haven’t experienced issues. Can you make a jsBin example that actually shows what you are indicating?
The scroll hack doesn’t work if the element that is being watch hides (or any of its parents), because browsers reset the scrollTop and scrollLeft values. Safari, Firefox and Chrome could use the underflow/overflow hack, <IE11 can use the onresize event, but IE11 is left out. You can test this by just setting display:none and setting is back again to default, or by removing the element and adding it back again. Anybody have a solution?
zenorbi I have actually come up with a new method that is probably 1/3 the code weight, and flawlessly works as far back as IE6! Give me a week to get a new post together.
As for this issue, the display: none; I didn’t test for such a case, so I have to do that. I reworked the code to operate correctly in IE11, but the display: none; thing is a separate issue. I’m not going to put too much more time into this version, given I have something that is a silver bullet for the whole resize event topic.
csuwldcat zenorbi Any update on the new version of listening for resize events?
csuwldcat zenorbi How is the new version coming along? Would be really useful right now.
csuwldcat zenorbi +1, quite interested in the new approach, too. i’m having trouble getting this one to work at all with IE10 and down. :[
This is awesome, thank you sir! I’m keen to see the new method you’ve spoken about in the other comments.
tenderloin420 I just updated the post with the new method – can you let me know if it works for you?
We’ve been using this technique extensively in Craft’s (http://buildwithcraft.com) Control Panel, and it works great.
I would like to recommend one addition to the CSS:
.resize-triggers > div::-webkit-scrollbar {
display: none; /* Fixes the Chrome scrollbar bug */
}
This will fix a bug where the resize trigger scrollbars are sometimes visible in Chrome.
Brandon Kelly hey Brandon, great to hear this helped you. I’ve just overhauled the code a lot and changed the approach. It now uses a hidden object element to sniff out the resize occurrences, which is faster, simpler, and less code than the last version. Can you test it for me and let me know if it works well for you? There is now no need to add any external CSS bits, everything is provided by the script.
csuwldcat Brandon Kelly The object approach doesn’t work for Safari and Chrome (both on a Mac) but if I change the object to and iframe, so the detection is on the iframe emulated window, it works great.
Line 60 is trying to remove the resize event from the element rather than the resize trigger. It should read `element.__resizeTrigger__.contentDocument.defaultView.removeEventListener(‘resize’, resizeListener);`
dsrw ‘element’ and ‘element.__resizeTrigger__’ are references to the same element 😉
csuwldcat It isn’t, unless I’m doing something wrong. Element is the element that I’m actually monitoring (a DIV, in my case). element.__resizeTrigger__ is the object tag that gets inserted by addResizeListener.
I get “Cannot read property ‘defaultView’ of undefined” errors unless I change line 60.
Ahh, yeah, you’re totally right – I just changed the paste error in the post’s code. Thank you!
The jsfiddle doesn’t work for chrome unless I’m missing something.
eggers I’ve updated the jsFiddle link to show the non-iframed ‘show/light’ version of the demo. Chrome has a bug where elements with onload do not fire when in a nested iframe. Should work in Chrome now if you try it. Hopefully you don’t need to use this on an element that is located inside an iframe :/
Hi Daniel
FYI
I created a webcomponent (using polymer) that uses the technique you explained.
http://px10.github.io/px10-element-resize-detector/components/px10-element-resize-detector/demo.html
https://github.com/PX10/px10-element-resize-detector
First of all, thanks very much for you work, but I can’t seem to get it to work on img elements..? I’ve done it on a few different img tags and can’t get it to work, am I missing something or is it just a bug of this snippet?
bedeo You cannot use this method for img elements as the img element is a void. Void elements cannot have child elements. This technique relies on adding an iframe as a child element with 100% width and height and watching the iframe’s resize event.
each iframe element you create (to get the “resize” event) adds an approximately 0.5MB in memory. This is a bit much.
Probably not a good idea to put this into a UI toolkit where every control has one of these iframes.
It looks like the created object is focusable under Firefox.
Adding the following code under addResizeListener fixes the issue.
obj.setAttribute(‘tabindex’, ‘-1’);
Curious if anyone has been able to get this to work with Angular directives (I see someone pulled it off with Polymer, thats interesting). I have found that a div works fine. A div with an angular directive as an attribute works fine, but if the directive is set to E to make an element, it won’t work. This is interesting, when I inspect the dom, my angular element does actually have the object injected, but the object element isn’t “awake”.
How about adding this to Bower/Github?
One thing of note – in IE11, this was not working for me if I added a listener to an element, then added that element to the page. It seems the element to watch must already be in the page, FWIW.
Thanks for this. Though I can find where your getComputedStyle() method is defined?
My bad, misread the error. It is correctly referring to window.getComputedStyle(). Is there an npm module somewhere of this code?
Lango https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
petah Lango Thanks for that!
I have used above plugin but it is not working when div element is collapsed to some with
I’m getting an error in Edge
Unable to get property ‘defaultView’ of undefined or null reference
From this line (line 60 in your example):
element.__resizeTrigger__.contentDocument.defaultView.removeEventListener(‘resize’, resizeListener);
contentDocument seems to be null.
Lango I think that means you are removing the element before you remove the event.
petah Lango It’s on a react component, and I’ll calling in on the componentWillUnmount, so it should be there.If the order was wrong, shouldn’t it fail in the other browsers as well?
componentWillUnmount: function() {
ResizeListener.removeResizeListener(this.refs.bodyInput.getDOMNode(), this._handleResize);
},
I changed line 60 to the following to get rid of the error, but it seems like this event doesn’t fire at all in edge
var contentDocument = element.__resizeTrigger__.contentDocument;
contentDocument && contentDocument.defaultView.removeEventListener(‘resize’, resizeListener);
Here’s a gist that will hook this up to jQuery’s event system: https://gist.github.com/brandonkelly/cc316fe617a6de996b40
With that code in place, jQuery’s on() and off() functions will support a ‘resize’ event type that maps to this script’s addResizeListener() and removeResizeListener() functions.
Actually, that ends up creating another JS error if resizeListener() somehow ends up getting called.
Another approach is just to update this conditional:
if (!element.__resizeListeners__) {
to:
if (!element.__resizeListeners__ || !element.__resizeTrigger__) {
Looks like this is working w/out generating any errors after setting/unsetting resize events on the same element multiple times.
This doesn’t work currently in the MS Edge browser, any ideas for a solution?
ejfrancis I believe my implementation https://www.npmjs.com/package/element-resize-detector works well in Edge. It’s based on Daniels original implementation and has a few bug fixes and performance improvements. Kudos to Daniel for coming up with this cool hack!
wnr that looks like exactly what I was looking for, I’ll give it a try tonight. thanks
lamtranweb I discovered this as well, through testing. Most disappointing!
Can someone explaine me how this work. I see some this this.contentDocument.defaultView.addEventListener(‘resize’, resizeListener);
Does resize work only when you resize whole document?
I also see this onresize event and guess that this is some custom event. What triggers this event?
Any way to expand this to detect CSS zoom element changes as well?