Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
ion-list sticky headers

What this does:

Within an <ion-scroll>, this directive creates sticky item headers that get bumped out of the way by the next item.

Demo: http://cl.ly/2u2X390s0H1a

Requirements:

  • Needs UnderscoreJS for its _.throttle utility.
  • Must be used within an <ion-scroll> or <ion-content>
  • You must group each header and contents together within a container element (this container element defines the area in which the header should stay).
  • Not tested with collection-repeat -- only with ng-repeat (please let me know if it works and I'll update)
  • This directive works by cloning the "sticky header" and appending it between the outer scroll container and inner scroll container -- as a sibling of the scrollbar, for reference. Thus, you might need to edit your CSS if it doesn't already apply correctly to the cloned header element.

Example: If you want to render a list of posts with sticky post titles, you could create markup like this:

<article class="post" ng-repeat="post in posts track by post.id">
    <h1>Post Title</h1>
    <main>
        <p>Long post body with many paragraphs...</p>
    </main>
</article>

Then all we need to do is make sure it's within an <ion-scroll>, and add the affix-within-container=".post" attribute to the <article>. (See affixWithinContainer.html below)

Where affix-within-container=".post" tells the <h1> that it is the sticky header for everything within it's closest .post ancestor. i.e. $(element).closest('.post').

<ion-content scroll-event-interval="5"><!-- or <ion-scroll> -->
<article class="post" ng-repeat="post in posts track by post._id">
<h1 affix-within-container=".post">
{{post.title}}
</h1>
<main>
{{post.body}}
</main>
</article>
</ion-content>
.directive('affixWithinContainer', function($document, $ionicScrollDelegate) {
var transition = function(element, dy, executeImmediately) {
element.style[ionic.CSS.TRANSFORM] == 'translate3d(0, -' + dy + 'px, 0)' ||
executeImmediately ?
element.style[ionic.CSS.TRANSFORM] = 'translate3d(0, -' + dy + 'px, 0)' :
ionic.requestAnimationFrame(function() {
element.style[ionic.CSS.TRANSFORM] = 'translate3d(0, -' + dy + 'px, 0)';
});
};
return {
restrict: 'A',
require: '^$ionicScroll',
link: function($scope, $element, $attr, $ionicScroll) {
var $affixContainer = $element.closest($attr.affixWithinContainer) || $element.parent();
var top = 0;
var height = 0;
var scrollMin = 0;
var scrollMax = 0;
var scrollTransition = 0;
var affixedHeight = 0;
var updateScrollLimits = _.throttle(function(scrollTop) {
top = $affixContainer.offset().top;
height = $affixContainer.outerHeight(false);
affixedHeight = $element.outerHeight(false);
scrollMin = scrollTop + top;
scrollMax = scrollMin + height;
scrollTransition = scrollMax - affixedHeight;
}, 500, {
trailing: false
});
var affix = null;
var unaffix = null;
var $affixedClone = null;
var setupAffix = function() {
unaffix = null;
affix = function() {
$affixedClone = $element.clone().css({
position: 'fixed',
top: 0,
left: 0,
right: 0
});
$($ionicScroll.element).append($affixedClone);
setupUnaffix();
};
};
var cleanupAffix = function() {
$affixedClone && $affixedClone.remove();
$affixedClone = null;
};
var setupUnaffix = function() {
affix = null;
unaffix = function() {
cleanupAffix();
setupAffix();
};
};
$scope.$on('$destroy', cleanupAffix);
setupAffix();
var affixedJustNow;
var scrollTop;
$($ionicScroll.element).on('scroll', function(event) {
scrollTop = (event.detail || event.originalEvent && event.originalEvent.detail).scrollTop;
updateScrollLimits(scrollTop);
if (scrollTop >= scrollMin && scrollTop <= scrollMax) {
affixedJustNow = affix ? affix() || true : false;
if (scrollTop > scrollTransition) {
transition($affixedClone[0], Math.floor(scrollTop-scrollTransition), affixedJustNow);
} else {
transition($affixedClone[0], 0, affixedJustNow);
}
} else {
unaffix && unaffix();
}
});
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment