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!
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.
I ran into an interesting scenario. It seems that this requires the element you pass into the add function to contain at least one character or even a list bullet. Without one, the event won’t fire. I’d love some insight into this. I “solved” it by appending this this:
<span style=”font-size:0; height:0″>a</span>
Obviously a hack, but it works. Here’s your fiddle with the text removed from the list and list-style set to none.
http://jsfiddle.net/3QcnQ/48/
You can see that the event no longer triggers on resize. Adding back the list bullets or un-commenting my span (added to the list) will fix it.
All that said, this function is totally awesome. I’ve been bragging on you to some friends for a few days – this trick is just downright clever. Also, this may very well be the only way of dynamically updating an iframe, which is exactly what I needed.
JohnnyHauser I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
I successfully used event-based resize detection for table cell resize detection in Chrome and Opera, but I have problem with table cell resize detection in Firefox.
Problem is that Firefox ignores “position: relative” in table cell (td, th). It means that it is not posible use cell as offset parent of resize-sensor. I tried create wrapper div inside table cell and use this div as parent of resize-sensor. It works partially, but there is problem with height detection. It is not possible to force wrapper div to grab 100% height of cell (if cell height is not explicitly set in pixels). So this solution is not universally applicable.
If someone knows the ultimate solution of event-based table cell resize detection in Firefox, please tell me. Thanks.
Permonix I’ve completely reworked the code to use scroll events, it seems 100% reliable across even more (and older) browsers.
Thanks for this, it gave me some ideas for alternatives since I couldn’t get it to work everywhere reliably.
I had more luck using an iframe, its window has a resize event that can be used. I basically replaced the overflow logic with a dynamically created iframe, written into it is a window.onresize function that checks viewport dimensions and reports back to the parent. I also pass along an ID that helps find the correct callback when reporting back to the parent window.
Works perfectly in Firefox, but unfortunately the resize event triggers way too often in Chrome, even when iframes aren’t resizing (I should probably report that bug). So that being buggy drags down performance.
An alternative is using MutationObserver and a fallback to MutationEvents to check dimensions. The problem with those is that changes to layout caused by things like a CSS :hover don’t trigger events, so approaches that check the actual size still seem like the best way to go.
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.
Ok, I got thoroughly confused when playing when the code. I’ve added some console.log() calls to scroll event handlers, and contract handler was always firing because of resetTriggers().
In fact, it works as follows: the monitored element is covered by an invisible (visibility: hidden) div (.resize-triggers) that has two scrollable (overflow: auto) divs of the same size (.expand-trigger and .contract-trigger).
Both .expand-trigger and .resize-trigger have maximum possible values of scrollLeft and scrollTop.
.expand-trigger’s content is set to be just one pixel wider and higher than .expand-trigger itself, thus when .expand-trigger’s height or width increases just by one pixel, the corresponding scrollbar on .expand-trigger disappears, its scrollTop or scrollLeft becomes 0 causing ‘scroll’ event to fire. On the other hand, when width or height decreases, scrollTop and scrollLeft of .expand-trigger don’t change, thus no events are fired.
.contract-trigger’s content is set to be twice as wide and twice as high as .contract-trigger itself, so when width or height decrease, the scrollbar is pushed to the left, decreasing scrollLeft / scrollTop and thus causing ‘scroll’ event. When width or height increase, some free space appears to the right / below the scrollbar, but scrollLeft / scrollTop don’t change, so no scroll events appear.
When ‘scroll’ event is caught, triggers are reset by adjusting .expand-trigger content size and setting scrollLeft / scrollTop values of the triggers to their maximum possible values. This by itself may cause more scroll events, but this appears not to be causing endless loops (still need to wrap my head around this a bit). Actual resize handling is postponed via requestAnimationFrame() (or setTimeout() when rAF is not available), multiple consecutive scroll events are aggregated into one.
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.
csuwldcat Hah, bad timing on my part then 🙂
This new approach works really well. Noticeably faster, and the code is definitely much simpler! Unfortunately, on a page with heavy use of custom resize events, Chrome is actually crashing on me now. I’ve spent the majority of the day trying to narrow it down, but browser crashes are much tougher to debug than normal JS errors/exceptions.
I ended up fixing this by updating resizeListener() to stop using requestAnimationFrame(). Now it just checks to make sure the element has actually resized before calling the event handler, by storing its offsetWidth/Height on `win`. You can see my work here: https://github.com/pixelandtonic/Garnish/commit/ac6e2da2c3d207061ed8f55871012e2913792773 (It’s modified quite a bit because I’m working it into our UI library’s custom addListener() method, but should be easy enough to grasp.)
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 have used above plugin and made knok out custom binding I work only in expanded mode not in collapsed to some widt. code is below-
/**
* ElementResizeBinding
*/
;
(function (factory) {
if (typeof define === ‘function’ && define.amd) {
// AMD anonymous module
define([‘knockout’, ‘jquery’, ‘knockout.postbox’, ‘jquery.elementresize’, ‘jquery.throttle’], factory);
} else {
//throw new Error(‘ElementResizeBinding -> AMD Loader is required!’);
factory(ko, jQuery, ko.postbox);
}
})(function (ko, $, postbox) {
/**
* <div data-bind=”elementresize: {topic:String, message: Object, listener:Function}”></div>
*
*/
ko.bindingHandlers.elementresize = {
init: function init(element, valueAccessor, allBindings, viewModel, bindingContext) {
var $el = $(element),
values = valueAccessor(),
topic = ko.utils.unwrapObservable(values.topic),
message = ko.utils.unwrapObservable(values.message),
listener = ko.utils.unwrapObservable(values.listener);
function elementResizeHandler(trailing, event, params) {
var msg = $.extend({}, message, {
trailing: trailing,
params: {
size: params.size
}
});
if(topic) {
postbox.publish(topic, msg);
}
if(typeof listener === ‘function’) {
listener(msg);
}
}
$el.elementresize().on(‘elementresize’, $.throttle(elementResizeHandler.bind(this, false), 5, null, true));
$el.elementresize().on(‘elementresize’, $.debounce(elementResizeHandler.bind(this, true), 250, null, true));
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
var data = $el.data();
if(data && data.elementresize) {
$el.elementresize(‘destroy’);
}
});
}
};
});
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.
csuwldcat Lango dsrw Figured out the underlying issue with this “Unable to get property ‘defaultView’ of undefined†bug that has been referenced in the comments.
removeResizeListener() is removing the resizeTrigger object and setting element.__resizeTrigger__ to false, but it is not not modifying element.__resizeListeners__, besides splicing it down to an empty array. Therefore, the next time that addResizeListener() is called for that element, this condition will *not* pass:
if (!element.__resizeListeners__)
because empty arrays still equate to `true`.
Because the condition fails, the resizeTrigger object doesn’t get recreated, so no resize events will actually be triggered.
Then, if you call removeResizeListener() again for that element, this line will cause the ‘undefined’ error:
element.__resizeTrigger__.contentDocument.defaultView.removeEventListener(‘resize’, resizeListener);
because `element.__resizeTrigger__` is still set to false, from the first time removeResizeListener() was called.
The simple fix here is to make sure that `element.__resizeListeners__` gets deleted or set to `false` if it’s going to be an empty array at the end of removeResizeListener(): https://github.com/pixelandtonic/Garnish/commit/44820fa125147160e346f8d1c6e3d926314688fa
Hope that helps!
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?