Forced Repaints In Directive Can Cause Accidental Scrolling In AngularJS
The other day, I started getting an odd behavior in an AngularJS application that had a tabbed interface. Normally, with a tabbed interface, when the user switches from tab to tab, the scroll offset of the browser should remain the same. And, this is how our application was working. But then suddenly, we started seeing a "scroll-to-top" behavior when the active tab pane was switched. After an hour of ripping code out of the app, I finally figured out what it was - a newly-added directive was forcing a browser repaint which was accidentally causing the browser to scroll up.
If you remember from my blog post on CSS class transisions and timing, the browser optimizes rendering by grouping multiple UI (User Interface) changes into a single repaint (when possible). However, if you ask for UI deminsions in the middle of a series UI mutations, the browser is forced to repaint in order to query the most accurate dimensions from the DOM (Document Object Model).
This is basically what was happening in our AngularJS application. A newly-added directive was querying for DOM dimensions when a tab pane was activated. This forced the browser to repaint before AngularJS had a chance to render the content of the activated tab pane. And, since the tab pane had no content, it reduced the height of the document body, which caused the window to scroll up to the new content height.
Of course, the tab pane was rendered in the next event loop, leaving the change in content height too fast to be noticed by the naked eye. That's what made debugging this so irksome.
Anyway, to see this in action take a look at the video above which demonstrates the code below. In this demo, we have a directive that queries the tab pane width during the linking phase. This executes before the nested ngRepeat directive has a chance to respond to the model.
NOTE: I believe this happens because the $watch() expression used to monitor the ngRepeat collection is invoked asynchronously (as are all $watch() expressions).
To get around this, we ended up putting the DOM-querying method behind a $timeout(). This allowed the tab pane content to render before its dimensions were calculated. This kept the content height consistent which allowed the scroll offset of the window to remain the same.
This isn't a bug in AngularJS; it's just an interesting interaction of browser optimizations, the DOM, directives, and the AngularJS digest lifecycle. You just need to know why it's happening so you can debug it when it happens to you.
Want to use code from this post? Check out the license.
console.log( "Pane Width:", element.width() );
TypeError: Object [object Object] has no method 'width'
Did i miss something?
Are You using jQuery in addition?
That's really odd! I am using jQuery, but it's being loaded from the remote CDN (right before the main Script tag). Perhaps the CDN was down when you ran the code? It should definitely be supported.
Hey, thanks for the post. Lead me to finding a very similar bug. Strangely, mine was in the CSS of a package I had included. They were using a "bugfix" that used
and this made the page jump up. I imagine it was causing a repaint. I changed it to padding: 0 to padding: 0 as suggested here: http://css-tricks.com/webkit-sibling-bug/ and my scrolling bug was fixed. Bizzare.
Bingo! I had the exact same problem with a directive causing issues with re-painting.
Thanks for the writeup and fix.
I had a similar issue with repainting in a directive, and your post led me to the fix. Thanks for writing!