Search code examples
javascriptangularjsanchor-scroll

AngularJS: Custom $anchorScroll provider execution


I need to scroll to a specific anchor tag on page reload. I tried using $anchorScroll but it evaluates $location.hash(), which is not what I needed.

I wrote a custom provider based on the source code of $anchorScrollProvider. In it, it adds a value to the rootScope's $watch list, and calls an $evalAsync on change.

Provider:

zlc.provider('scroll', function() {
  this.$get = ['$window', '$rootScope', function($window, $rootScope) {
    var document = $window.document;
    var elm;

    function scroll() {
      elm = document.getElementById($rootScope.trendHistory.id);
      if (elm) elm.scrollIntoView();  
    }

    $rootScope.$watch(function scrollWatch() {return $rootScope.trendHistory.id;},
      function scrollWatchAction() {
        if ($rootScope.trendHistory.id) $rootScope.$eval(scroll);
      });

    return scroll;
  }];
});

Now, when I try to call the scroll provider in my controller, I must force a digest with $scope.$apply() before the call to scroll():

Controller:

//inside function called on reload
$scope.apply();
scroll();

Why must I call $scope.$apply()? Why isn't the scroll function evaluating in the Angular context when called inside the current scope? Thank you for your help!


Solution

  • I'm not sure what your thinking is behind using $rootScope.$eval(scroll) - since the scroll() function is already executing in a context where it has direct access to the $rootScope.

    If I understand correctly, you want to be able to scroll to a particular element as denoted by an id which is stored in $rootScope.trendHistory.id.

    When that id is changed, you want to scroll to that element (if it exists on the page).

    Assuming this is a correct interpretation of what you are trying to achieve, here is how I might go about implementing it:

    app.service('scrollService', function($rootScope) {
      $rootScope.trendHistory = {};
    
      $rootScope.$watch('trendHistory.id', function(val) {
        if (val) {
          elm = document.getElementById($rootScope.trendHistory.id);
          if (elm) elm.scrollIntoView();
        }
      });
    
      this.scrollTo = function(linkId) {
        $rootScope.trendHistory.id = linkId;
      }
    });
    

    This is a service (like your provider, but using the simpler "service" approach) which will set up a $watch on the $rootScope, looking for changes to $rootScope.trendHistory.id. When a change is detected, it scrolls to the element indicated if it exists - that bit is taken directly from your code.

    So to use this in a controller, you'd inject the scrollService and then call its scrollTo() method with the ID as an argument. Example:

    app.controller('AppController', function($scope, scrollService) {
    
            scrollService.scrollTo('some_id');
    
    });
    

    In your question, you mention this needing to occur on reload, so you'd just put the call into your reload handler. You could also just directly modify the value of $rootScope.trendHistory.id from anywhere in the app and it would also attempt to scroll.

    Here is a demo illustrating the basic approach: http://plnkr.co/edit/cJpHoSemj2Z9muCQVKmj?p=preview

    Hope that helps, and apologies if I misunderstood your requirements.