Skip to content

Instantly share code, notes, and snippets.

@colllin
Last active November 29, 2020 17:13
Show Gist options
  • Save colllin/1a0c3a91cc641d8e578f to your computer and use it in GitHub Desktop.
Save colllin/1a0c3a91cc641d8e578f to your computer and use it in GitHub Desktop.
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();
}
});
}
}
});
@tangentlin
Copy link

This is extremely helpful and it is the only solution I have found working after trying iOSList, StickySectionHeader etc., most would not work well with mobile interface, or hard to integrate with Angular. Your solution is also simple enough that it does not even depend on any external CSS.

I have noticed that when I used the directive in ion-nav-view, somehow position: fixed does not work, but by changing it to position: absolute, it works like a charm.

@guinetik
Copy link

cudos man. this was very useful.

@aliok
Copy link

aliok commented Apr 17, 2015

Hi @Collins,

I improved the code here and converted it into a project (bower, demos, etc).

See the project page: http://www.aliok.com.tr/projects/2015-04-17-ion-affix.html
Demos: http://codepen.io/collection/DrxWPr/

It is MIT licensed and your name exists in the copyright owners.

Here are things I have done:

  • made the code more beautiful
  • documented the code
  • got rid of dependencies to Underscore.js and jQuery
  • fixed a couple of alignment issues (e.g. when there are fixed or absolute positioned elements already on top.
    or when the affix container had bottom margin)
  • published it as a Bower package so that folks can use it w/o copy-pasting the Gist and creating the file etc.
  • implemented a couple of demos

@Poordeveloper
Copy link

Try out this one, just add ion-sticky to ion-content, it will detect dividers and make the active one sticky.
https://github.com/Poordeveloper/ion-sticky

@shaikhspear16
Copy link

anyone done this for ionic 2?

@rdsmartins
Copy link

It will be perfect if we could port it to Ionic 2 ....

@rico
Copy link

rico commented Aug 10, 2016

+1 for an Ionic 2 implementation.

@jonaszuberbuehler
Copy link

We needed this feature in one of our projects. Created a small directive out of it: https://github.com/jonaszuberbuehler/ion-affix. Works with Ionic 2/3 on ion-list-header, might be helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment