6

I am attempting to use the bootstrap scollspy to highlight list items generated by an angular repeater.

The problem I'm running into is that I'm refreshing the scrollspy plugin from an angular controller, before angular has applied the model changes to the view.

What is the angular way to ensure the scrollspy('refresh') call happens after the DOM itself has been updated (not just the angular model)?

Template:

<div class="span2" id="addressList">
    <ul class="nav nav-tabs nav-stacked affix">
       <li ng-repeat="addr in addresses"><a href="#{{addr.id}}">{{addr.id}}</a></li>
    </ul>
</div>

Controller:

$scope.httpSuccessCallback = function (data) 
     $scope.addresses.push(data);
     $('[data-spy="scroll"]').scrollspy('refresh'); //calls $('#addressList .nav > li > a')
 }
4
  • This has probably been said 1,000 times. But you don't want to do DOM manipulation from your controller functions. What you want to do is create a directive that handles all of it. Then use a $watch on whatever it is that triggers the update to update it. Commented Jan 11, 2013 at 18:31
  • I'm not doing any DOM manipulation in my controller. I'm trying to update a jQuery plugin at the appropriate point in the angular lifecycle. Seems like $watch might still be the right mechanism? Commented Jan 11, 2013 at 18:37
  • 4
    You're selecting an element from the DOM, and binding events, etc... That's really what is the no-no I meant by "DOM manipulation". The Angular docs go over this fairly well. You shouldn't be referencing the DOM in your controllers. Commented Jan 11, 2013 at 18:39
  • 1
    Here, see the section called "Using Controllers Correctly": docs.angularjs.org/guide/dev_guide.mvc.understanding_controller Commented Jan 11, 2013 at 18:39

4 Answers 4

5

Without knowing anything about scroll spy, here's how you generally want to use a JQuery plugin in Angular:

app.directive('scrollSpy', function (){
   return {
     restrict: 'A',
     link: function(scope, elem, attr) {
         elem.scrollSpy({ /* set up options here */ });

         //watch whatever expression was passed to the
         //scroll-spy attribute, and refresh scroll spy when it changes.
         scope.$watch(attr.scrollSpy, function(value) {
              elem.scrollSpy('refresh');
         });
     }
   };
});

Then in HTML:

<div scroll-spy="foo">Do something with: {{foo}}</div>

The above example is VERY generic, but it will basically apply your plugin to an element, and call 'refresh' or whatever on it every time $scope.foo changes.

I hope that helps.

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

9 Comments

So when the $watch fires, is it guaranteed that the view has been rendered and DOM manipulation is complete with the object's changes in effect? The DOM must be updated before we can refresh.
I'm getting unrecognized expression error when doing this. I'm using the model to update the anchor (href="#category{{cat.id}}") and I think the $watch callback fires before Angular updates the DOM. The Bootstrap scroll plugin tries to use the #category{{cat.id}}, which hasn't resolved yet, giving the error.
The angular docs mention a pre and post-linking function available from the compile() function of a directive declaration. Post-link is supposed to be safe for DOM work. I'm not sure exactly how to use it yet. docs.angularjs.org/guide/directive
Sure, see "Writing directives (long version)" in the documentation here.
@blesh - in my example, $scope.addresses is an array I push values into from a text input. In the directive, my scope.$watch('addresses', ...) call never fires when I modify $scope.addresses. When I changed my code to scope.$watch('myTextInput',...), it fires when I modify the text input. Is this an implicit limitation of the $watch function?
|
4

How I solved this, using Blesh's answer

Template:

  <body ng-app="address" ng-controller="AddressCtrl" scroll-spy="addresses">
    <div class="container">
      <form ng-submit="lookupAddress()">
        <input type="text" ng-model="addressText" />
        <button id="addressQueryBtn">Submit</button>
      </form>

      <div id="addressList">
        <ul>
          <li ng-repeat="addr in addresses">{{addr}}</li>
       </ul>
      </div>

    </div>
  </body>

Angular JS:

angular.module('address', []).
directive('scrollSpy', function($timeout){
  return function(scope, elem, attr) {
    scope.$watch(attr.scrollSpy, function(value) {
      $timeout(function() { elem.scrollspy('refresh') }, 200);
    }, true);
  }
});

function AddressCtrl($scope, $http) {
  $scope.addresses = [];

  $scope.lookupAddress = function() {
    $scope.addresses.push($scope.addressText);
    $scope.addressText = '';
  };
}

The third argument to scope.watch(...) is needed when the watched scope var is an object or array. Unfortunately, this solution still results in the unrecognized expression problem randomguy mentions in the comments. I ultimately resolved this by using a timeout in the watch function.

Comments

1

There are a few challenges using Scrollspy within Angular (probably why AngularUI still doesn't include Scrollspy as of August 2013).

  1. You must refresh the scrollspy any time you make any changes to the DOM contents.

    This can be done, as has been suggested here, by $watching the element and setting a timeout for the scrollspy('refresh') call.

  2. You need to override the default behaviour of the nav elements if you wish to be able to use the nav elements to navigate the scrollable area.

This can be accomplished by using preventDefault to to prevent navigation and attaching a scrollTo function.

I've thrown a working example up on Plunker: http://plnkr.co/edit/R0a4nJi3tBUBsluBJUo2?p=preview

Comments

-1

I have been looking for a solution to this problem for a while but I wasn't able to find one. I ended up implementing my own by creating a scroll spy service with a few directives.

The service will keep track of all the spied sections and their offset. The directives will create a scroll event, highlight nav items and tell the service where all the sections are located. Since this implementation doesn't watch a scope variable, the application needs to broadcast a message to update all sections' offsets.

You can find my example on Github.

Comments

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.