The Infinite Path of Scrolling

Building the ng-scroller AngularJS directive

Jan Kuča
8 min readAug 16, 2013

--

For the past few weeks, I have been interning at Google as a member of the AngularJS team and researching and solving the issue described below has been my first intern project. I am going to talk about the different approaches I have taken and I would very much like to hear some feedback about the final design from you, the community.

If one has a large data set that they need to display in a web app in an efficient manner, they usually handle this issue one of the following ways: pagination or lazy loading when a user reaches the end of the page (think Facebook).

The second approach could be called “infinite scrolling” but in most cases the scrolling experience gets worse and worse as one scrolls further down the page. There is the ngInfiniteScroll directive that handles the lazy loading. What it does not however do is making sure the performance does not worsen as more and more content is being added to the page. The UITableView component in iOS solves the performance issue by removing the UITableCell child components that are scrolled out of the viewport and reusing them later. This could definitely be applied to web-based scrolling lists as well.

ngInfiniteScroller vs. UITableView or my solution

I started by implementing a very naïve solution that worked with constant item height and items of one type. The problem was that this was too restrictive obviously. Since the first prototype, I would always have a separate data store object that the scroller component would ask for data when needed.

I was also thinking about separating the “core” and the actual DOM-related logic. My second implementation did just that and I tried creating several different “styler” implementations for various cases (a list, a continuous icon grid, column-groupped list). It was a lot of extra code that one would have to write to implement their own style.

The solution was utilizing CSS transforms (translations) to achieve the scrolling (even though it could have just been using relatively positioned carousel element). It did not however account for the “edges” of the data set which meant that the user could scroll past the end. There was also no concept of waiting until the data store actually returned any data which in practice meant that the user would scroll to an empty space and the data would appear after that. I trashed this one and started from scratch.

I expect this component to be used for ever-updating streams and having implemented an index driven pagination API between the scroller and the data store object, I would get to negative indices when I’d added new items to the top and in general, it did not feel right. I decided to go with a token-based pagination where the offset is specified as the identifier of the previous (or next if we are requesting newer items) record and the maximum number of items we would like to obtain. That way, the pagination is not relative to the initial state.

My current implementation looks pretty good so far. I am trying to make it look as native as possible—the ultimate goal is to make the user totally unaware of the technical solution. One of the vital features is replacing removed items with empty space so that we do not mess with the native scroll position and make it look for the user like there is actual content beyond the visible portion of the data set.

There are basically two types of an infinite scroller—a widget that has its own scroll position and an in-page content that scrolls with the page. These two can be present in one document at the same time. Facebook is a good example of this—there is the page content and the fixedly positioned ticker on the side. The only difference between these two is just the element that is considered to be the viewport. In case of the in-page variation, the viewport is the actual inner browser window whereas the widget one works within an element inside the page.

List Scroller Diagram

The idea is that we start with an empty container into which we insert the first chunks of the data set to fill it up. There is a configurable number or items to request from the data store at once. Then, when the user scrolls down and reaches the end of the buffer, more items are requested. A loading spinner is optionally displayed at the bottom.

As the user scrolls down the page, items above the viewport are removed from the DOM and moved to a pool of unused items. When the request to the data store is resolved and new data is received, the component first looks if there are any reusable elements and either replaces the view model data with the new items or creates a whole new element. These elements are then added to the end of the item container.

The user can scroll back in the other direction (upwards) and the exact same thing happens in reverse. Elements that are not visible at the end are removed and placed in the pool. More items are requested from the data store before the first visible item and the removed elements are reused when we receive the data.

One might argue that removing items immediately when they are out of the viewport is not a good idea as the need for requesting them from the data store again even if the user scrolls just a few items in the opposite direction is not a good idea from the user experience point of view. I feel like requesting the items again is the correct thing to do as the app-specific data store object can decide what to cache and there is no delay if the items are just taken out of the cache of the data store.

Object Model

One of the vital features of the scroller is supporting multiple item element templates so that there can be items of different types in the “stream”. Facebook, for instance, would like to have a text-only item, item with a photo, item with a link and so on.

To finally move over to some actual code, here is the minimal HTML structure required for a scroller:

<div ng-scroller>
<div ng-scroller-repeat=”post in posts”>
<div>{{post.author}}: {{post.text}}</div>
</div>
</div>

The ng-scroller element is considered to be the viewport element, the ng-scroller-repeat element is the item container (blue in the scroller diagram above) and the innermost element is an item template that is going to be duplicated and linked with a child scope created for each item.

The posts scope key in this case has to point to a data store object that implements the following interface for querying for items. If the key points to an Array, for convenience, it gets internally wrapped by an object that implements the required interface.

IScrollerDataStore {
void getRangeAfter(prev_id, length, function(Error, Array));
void getRangeBefore(next_id, length, function(Error, Array));
}

If an app requires multiple templates, it should utilize the ng-if and ng-switch directives as much as possible. At one point, I had a dedicated multiple template logic implemented in the scroller but in most cases, the existing means of achieving this are sufficient enough.

<div ng-scroller>
<div ng-scroller-repeat=”post in posts”>
<div ng-class=”{ link: post.link, photo: post.photo }”>
{{post.author}}:
<a ng-if="post.link" href=”{{post.link}}”>{{post.link}}</a>
<img ng-if="post.photo" ng-src=”{{post.photo}}”>
</div>
</div>
</div>

Originally, I thought I would let developers to define their own scroller extensions—they would extend the styler, not the core, so that they would not get access to some important internal things. I am still not totally convinced it is a bad idea to allow extensions as there can definitely be special use cases that require custom logic. What if some app wanted to implement a photo browser similar to the one in iOS—would they want to use this directive or would they rather implement it on their own? And what about the widely used “pull-to-refresh” behavior? The same goes for Reeder for iOS-like experience where it is possible to pull previous/next articles from the top/bottom of the open article.

Scroller Edge Action Examples: “Pull-to-Refresh” and pull-based navigation between articles

Should the directive provide a simple way for defining these edge actions and other extensions or should we expect developers to implement these things on their own as separate components?

Personally, I would say that the “pull-to-refresh” feature is so closely related to the scroller that it would make sense to support it somehow. What I’m not sure though is the ideal way to do that while keeping the overall component clean and simple.

Scrolling containers are often decorated with sticky elements such as a sticky header row. I can think of several types of sticky elements that are used in practice: sticky table headers, sticky footers (with status info, buttons or whatever), sticky cells or columns (this means that the element is sticky in the horizontal direction) and contact list or Instagram-like sticky item headers that are sticky only during the time the item is the topmost visible item in the viewport.

Common Sticky Element Types

It seems like a good idea for the scroller to handle these as the performance would certainly be better as opposed to each sticky element to be a component checking the scroll position itself.

With position:sticky landing in WebKit, we might actually be able to make the first two types with pure CSS. The last type is a bit tricky because the stickiness is actually dynamically modified while scrolling and the behavior of one item pushing another one out of the viewport must be taken into account. So again, should this be a part of the feature set of this directive?

When I was discussing the topic of the scroller with a fellow developer working on a similar component internal to Google, he mentioned that I should also focus on handling the state of the items I remove and re-add again later. Think about the Twitter feed for instance: You can click on a tweet and uncover the whole conversation. What if you open a tweet, scroll down so that it gets removed from the DOM and later scroll back up. Is the tweet still open or has the state been reset?

There are basically two ways to handle this. I can either store the state inside the scope key created via ng-scroller-repeat (i.e. tweet.open=true) or somehow track changes to the scope itself, keeping a map of these in relation to individual items and restoring them on item reusal.

For now, I chose the former approach as I could not think of any case where it would not be sufficient.

Please check out the code on GitHub and play with the demos:

I would appreciate any input on the issues discussed in this post. Also any general ideas or comments on the overall solution is welcome. Thanks!

Unlisted

--

--