Sticky List headers

Here is an example I threw together as one potential way this could be done. The code is by no means great but works as a proof of concept. It is based off of iOSList and seems to work ok.

http://codepen.io/anon/pen/LpCyg

5 Likes

Awesome @drastick! @mhartington could we propose this for inclusion into the framework?

I would add my support for that proposal!

Very nice @drastick, awesome job on it.

It would be nice to have this done during list creation instead of how I have it implemented. Currently if you are dynamically generating the list with ng-repeat or something then the directive would have to wait for that to complete before running. If not you run the potential of a race condition where the element it is looking for doesn’t exist yet. Also it needs to track scroll position so its a little messy in my opinion to add the on-scroll to the content container instead of keeping everything necessary for it to work on the list itself.

One other thing is that each block of items in the list including the divider for that block needs to be wrapped in a container like this:

<div class="item-container">
    <div class="item item-divider">One</div>
    <div class="item">A</div>
    <div class="item">B</div>
    <div class="item">C</div>
    <div class="item">D</div>
</div>

So it would be nice if we could come up with a way to make that easier to do while dynamically creating a list. Or maybe thats me overthinking it but I was trying to keep it as simple as possible without introducing another element that needed to be manually added while creating a list.

Lot of topics/issues:





@drastick Awesome work. Excited for this.

One question I have is that currently I am taking data back from an API and sorting it into buckets for ‘Past’, ‘Today’, ‘Future’ using momentjs. I then pass this new mega object into the scope and into a series of ng-repeats. This feels icky and wrong but since I need the data grouped into these sections, I do it. Is there another way to approach this to avoid the nested repeats and instead opt for a single ng-repeat?

Any idea/thoughts on how to implement this on a collection-repeat?

1 Like

yes. add second directive and make some changes

 .directive("onRepeatDone", function ($timeout) {
    return {
        restriction: 'A',
        link: function ($scope, element, attributes) {
            //console.log("onRepeatDone");
            if ($scope.$last) {
                console.log("onRepeatDone");
                $scope.$emit(attributes["onRepeatDone"] || "repeat_done", element);
                //$timeout(function () {
                //    $scope.$emit(attributes["onRepeatDone"] || "repeat_done", element);
                //});
            };
        }
    }
})
.directive('sticky', function ($ionicScrollDelegate, $timeout, $compile) {
    var options,
        defaults = {
            classes: {
                animated: 'item-animated',
                container: 'item-wrapper',
                hidden: 'item-hidden',
                stationaryHeader: 'item item-divider'
            },
            selectors: {
                groupContainer: 'item-container',
                groupHeader: 'item-divider',
                stationaryHeader: 'div'
            }
        };
   

 return {
        restrict: 'A',

        link: function (scope, element, attrs, ctrl) {

            scope.$on('eventTransactionsRendered', function (domainElement) {
                scope.updateItems();
            });

            var items = [],
                options = angular.extend(defaults, attrs),
                $element = angular.element(element),
                $fakeHeader = angular.element('<div class="' + options.classes.stationaryHeader + '"/>');

            

            scope.updateItems = function () {
                console.log("updateItems");
               var 
                $groupContainer = angular.element($element[0].getElementsByClassName(options.selectors.groupContainer));

                $element.addClass('list-sticky');

                angular.element($element[0].getElementsByClassName('list')).addClass(options.classes.container);

                $element.prepend($fakeHeader);

                items = [];
                angular.forEach($groupContainer, function (elem, index) {

                    var $tmp_list = $groupContainer.eq(index);

                    var $tmp_listHeight = $tmp_list.prop('offsetHeight'),
                    $tmp_listOffset = $tmp_list[0].getBoundingClientRect().top,
                    $tmp_header = angular.element($tmp_list[0].getElementsByClassName(options.selectors.groupHeader)).eq(0)
                            ;
                    if ($tmp_header.text().indexOf("{{")==-1) //not found
                    items.push({
                        'list': $tmp_list,
                        'header': $tmp_header,
                        'listHeight': $tmp_listHeight,
                        'headerText': $tmp_header.text(),
                        'headerHeight': $tmp_header.prop('offsetHeight'),
                        'listOffset': $tmp_listOffset,
                        'listBottom': $tmp_listHeight +$tmp_listOffset
                        });
                    });

                $fakeHeader.text(items[0].headerText);
            }

            

            
            scope.checkPosition = function () {

                var i = 0,
                    topElement, offscreenElement, topElementBottom,
                    currentTop = $ionicScrollDelegate.$getByHandle('scrollHandle').getScrollPosition().top;

                topElement = items[0];
                var delta = items[0].listOffset;
                //while ((items[i].listOffset - currentTop) <= 0) {
                while ((items[i].listOffset - currentTop) <= delta) {
                //while ((items[i].listOffset ) <= 50) {
                    topElement = items[i];
                    topElementBottom = -(topElement.listBottom - currentTop);

                    if (topElementBottom < -topElement.headerHeight) {
                        offscreenElement = topElement;
                    }

                    i++;

                    if (i >= items.length) {
                        i--;
                        break;
                    }
                }

                //$fakeHeader.text(topElement.headerText);
                //delta += 30;
                if (topElement) {
                    if (topElementBottom + delta < 0 && topElementBottom + delta > -topElement.headerHeight) {
                        $fakeHeader.addClass(options.classes.hidden);
                        //angular.element(topElement.list).addClass(options.classes.animated);
                        var listElem = document.getElementById(angular.element(topElement.list).attr("id"));
                        listElem.className = listElem.className.replace(options.classes.animated, ""); // first remove the class name if that already exists
                        listElem.className = listElem.className + " " + options.classes.animated;
                    } else {
                        $fakeHeader.removeClass(options.classes.hidden);
                        if (topElement) {
                            //angular.element(topElement.list).removeClass(options.classes.animated);
                            var listElem = document.getElementById(angular.element(topElement.list).attr("id"));
                            listElem.className = listElem.className.replace(options.classes.animated, "");
                        }
                    }
                    $fakeHeader.text(topElement.headerText);
                }
            }
        }

    }

and use like

<ion-content delegate-handle="scrollHandle" on-scroll="checkPosition()" sticky>
        <div class="list">

            <div class="transaction-list item-container"
                 ng-repeat="group in transactions  | groupBy: 'Day' | toArray:true | orderBy:'$key' : true   "
                 id="gr_k_{{group.$key}}"
                 on-repeat-done="eventTransactionsRendered">
                <div class="item item-divider">{{group.$key | date:'MMMM d, yyyy EEEE' }}</div>
                <div class="item transaction-item" ng-repeat="transaction in group">
                    <a ng-click="open(transaction)" clas class="navigate-right">                       
                                {{transaction.Description}} 
                    </a>
                </div>
            </div>
        </div>

    </ion-content>

@max @Ben : any progress with this as an official feature?

It’s being worked on, but it probably won’t happen until after the 1.0 stable release of Ionic. Too many other things that rank a bit higher! :slight_smile:

1 Like

Lets vote people! =D

I found this through Google and @drastick’s codepen was extremely helpful. I wanted to submit a minor modification for anyone else who stumbles upon this thread through a search engine or whatever.

By adding an else to the very end of checkPosition() you can hide the static fakeheader when overscrolling.

codepen.io/anon/pen/EawXRO

It would be great to have this as an official feature, but until then, drastick’s code is working well enough for us. So thank you!

edit: Fixed link, https seems to not work with Codepen/Ionic.

edit edit:
If you want to use this within an ng-repeat or collection-repeat with @rour’s code just change the line near the end:

if (topElement) {

to

if (topElementBottom)

Because topElement will keep existing when scrolling up too far, but topElementBottom will become undefined.
(You can also explicitly check for ‘undefined’ instead of the implicit test, but where’s the fun?)

1 Like

Hi, I am trying to use sticky dividers with ng_repeat. (the codepens on here work great for me without ng_repeat). I tried to do what @neilgoldman305 suggested but nothing seems to change. My app has 2 dividers and the second one jumps to the top where the first should be, and then there are 2 together where the sections meet… its a big mess. Does anyone have this working and can share all of your js code?
thanks!

rtsbeacon. I had to do some work on the code that was posted by the other users on this thread but I finally got it to work correctly. I can probably post a codepen for folks who want to get this working. The reason the dividers were doing funny things is because of the digest loop on the ng-repeat. I ended up doing one time binding in my examples and a few other modifications and it seems to work nicely now. Let me know if you still need to see an example

@chale I would love to see an example (codepen) explaining this

Ok. So I went ahead and created the codepen and tried to create a github repo for it in case anyone wants to use it. I havent tested the bower install but I believe I did it right. Feel free to criticize the code. I am really not the best at writing directives but I can definitely take what others have done and try to get it working.

1 Like

Has anyone used this with collection-repeat ?
When I try to use this with collection-repeat, it gives the following error -

Uncaught TypeError: Cannot read property 'listOffset' of undefined

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

  • Good performance
  • No external dependencies : just Ionic
  • Available on Bower

(couldn’t embed the demo here and couldn’t find any instructions on how to do it )

More demos: http://codepen.io/collection/DrxWPr/

4 Likes

Awesome work, trying it right now!