12

Let's say I have a feed of articles that stack:

<div ng-repeat="article in articles">
  <h1> {{article.title}} </h1>
  <p> {{article.body}} </p>
</div>

When scrolling past each article:

<div scroll="atNewArticle(article)" ng-repeat="article in articles">
  ...
</div>

I want to execute a function:

.controller('AppCtrl', ['$scope', function($scope) {
  $scope.atNewArticle = function(){
    console.log(article);
  }
}])

I'm having difficulty figuring out the right way to do this because I'm not sure if I should bind the scroll event to the window or detect the offset of the directive element itself.

Here's what I've tried so far: http://jsfiddle.net/6VFJs/1/

3 Answers 3

22

There are a few things that are probably a good idea (depending on your exact use case). Firstly, a few non-Angular-specific ones:

  • Use a "debounced" version of the listener to the scroll event, so that the function only executes once the user has stopped scrolling. You can use lodash/underscore library for this.

    var debounced = _.debounce(function() { .... }, 500); // Executes 500ms after last call of the debounced function.
    angular.element($document).on('scroll', debounced);
    
  • Use a library to determine position/visibility of any element. verge.js is a small lightweight library I've found for this. One way of using this is:

    var visible = verge.inViewport(element);
    

    But depending on what exactly you want to treat as "visible", you might need to change this.

  • Keep track of what is visible / not visible at every event (as you have a function "atNewArticle", which suggests you only want it called when an item comes into view). So you'll need to create an array of visible elements, and test it on scroll.

    var index = visibleElements.indexOf(scope.scrollItem);
    

The Angular-specific point:

  • Pass 2 options to the directive. 1) the callback function when a new item comes into view, and 2) the article item itself, so it can be passed back to the callback.

    scope: {
         scroll: '&',
         scrollItem: '='
    }
    

    These can be used as:

    <div ng-repeat="article in articles" class="block" scroll="atNewArticle(item)" scroll-item="article">
    

    This gives the directive's scope a variable scrollItem, and a function scroll. So the directive can call, at the appropriate point:

    scope.scroll({item:scope.scrollItem});
    

    You should note the syntax of this: it's an key-object map.

You can see all this in action at http://jsfiddle.net/Pb8t4/4/

Edit: updated the link to a jsfiddle that includes a call to scope.$apply so that any changes are properly noticed by Angular.

Sign up to request clarification or add additional context in comments.

Comments

6

You can find this example helpful.

The idea is to use: scroll binding with if statement that filters scope.$apply(attrs.scroll); calling

1 Comment

Thank you for posting. This is a great solution with an excellent working example.
2

You should bind scroll event to document as document is the element that is scrolling. Then you can detect the scrollTop of document and compare with the offset of the repeating items.

JSFiddle Demo: http://jsfiddle.net/daiweilu/nzBS5/2/

I only use jQuery and Angular 1.2.6. Basically I tested the top border of ng-repeat item against window top border. If an item starts to leave window, the function on ng-repeat item is executed once. If the item came back and leave again, the function will be triggered again.

HTML

<div ng-controller="MyCtrl" scrollable-container>
    <div ng-repeat="article in articles" class="block" scrollable-item="atNewArticle(article)">
        <h1>{{article.title}}</h1>
        <p>{{article.body}}</p>
    </div>
</div>

JS var app = angular.module('myApp', []);

app.directive('scrollableContainer', function($window, $document) {
    return {
        link: function(scope, element, attrs) {
            angular.element($document).on('scroll', function() {
                var children = element.children();
                var $leftChild;
                for (var i = 0; i < children.length; i++) {
                    var $child = $(children[i]);
                    var childTop = $child.offset().top;
                    var docScrollTop = $document.scrollTop();
                    var scope = $child.scope();
                    if (childTop - docScrollTop < 0) {
                        if (!scope._left) {
                            scope._left = true;
                            $leftChild = $child;
                        }
                    } else {
                        delete scope._left;
                    }
                }
                if ($leftChild) {
                    $leftChild.scope().$eval( $leftChild.attr('scrollable-item') );
                }
            });
        }
    };
});


function MyCtrl($scope) {

    $scope.articles = [
        {title:'article1',body:'body of article1'},   
        {title:'article2',body:'body of article2'},
        {title:'article3',body:'body of article3'}
    ];

    $scope.atNewArticle = function (article) {
        //i only execute when transitioning back and forth between articles
        console.log(article);
    };
}

3 Comments

It would be really helpful if you could provide a fork off of that fiddle to demonstrate this approach.
@Dan Kanze , I will make one after I got my computer.
@DanKanze just added the demo. Mine is different from Michal's, it will execute atNewArticle function immediately when the top border of repeating element is above the top border of the window.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.