0

This has been asked before on Stack Overflow, but for some reason I keep getting errors that certain properties are undefined!

So I have the following controller:

phonecatControllers.controller('AboutCtrl', function($scope, $state) {
    $scope.startListenToScroll = function($scope, $state) {
        $('.subsection').each(function(i) {
            var position = $(this).position();
            $(this).scrollspy({
                min: position.top,
                max: position.top + $(this).height(),
                onEnter: function(element, position) {
                    if (element.id) {
                        $state.transitionTo('about.' + element.id);
                    } else {
                        $state.transitionTo('about');
                    }
                }
            });
        });
    }
    $scope.startListenToScroll($scope, $state);
    $scope.stopListenToScroll = function() {
        $('.subsection').unbind().removeData();
    }
});

Which has two functions for binding and unbinding a scrollspy plugin to an area within my about page.

I also have a directive which scrolls the user to certain sections on click of links:

.directive('scrollTo', function() {
    return {
        link: function(scope, element, attrs) {
            element.bind('click', function() {
                scope.stopListenToScroll();
                var divPosition = $('#' + attrs.scrollTo).offset();
                $('html, body').animate({
                    scrollTop: divPosition.top
                }, "slow", function() {
                    scope.startListenToScroll();
                });
            });
        }
    };
});

As you can see I call the unbind of the scrollspy on click and then re-bind them after the animation is complete. This is stop the scrollspy plugin listening to the scroll caused by the scrollTo animation.

However I get the error: Uncaught TypeError: Cannot read property 'transitionTo' of undefined presumably because it can't see $state.

You'll note I don't pass anything in the directive, but this is because it should be defaulting in the controller right? If not, how can I handle this?

Any ideas?

3
  • the functions Bind and Unbind are deprecated Commented Sep 2, 2014 at 14:31
  • @SjoerdDeWit Any ideas on how I could unbind scrollspy? I'm using this plugin: github.com/sxalexander/jquery-scrollspy Commented Sep 2, 2014 at 14:35
  • @Cameron on and off are the replacements for bind & unbind Commented Sep 2, 2014 at 14:49

4 Answers 4

2

First of all i highly recommend you not to put dom event logic inside a controller, it goes against everything done by the angular team to prevent that from happenning.

What i'd do in your situation would be to create a directive somewhat like this:

.directive('scrollAnchor', function($state) {
    return {
        link: function(scope, element, attrs){
            scope.startListenToScroll = function(){
                var position = element.position();
                element.scrollspy({
                    min: position.top,
                    max: position.top + $(this).height(),
                    onEnter: function(ele, position) {
                        if(ele.id){
                            $state.transitionTo('about.'+ele.id);
                        } else {
                            $state.transitionTo('about');
                        }
                    }
                });
            };
            scope.stopListenToScroll = function(){
                element.off().removeData();
            };
        },
        controller: function($scope){
            this.startListenToScroll = $scope.startListenToScroll;
            this.stopListenToScroll = $scope.stopListenToScroll;
        }
    };
});

And depending on where you are currently using your scrollTo directive, you could make it require scrollAnchor, being able to call it's functions defined with the "this" keyword at it's controller with something like

.directive('scrollTo', function() {
    return {
        require: 'scrollAnchor',
        link: function(scope, element, attrs, scrollAnchorCtrl) {
            element.bind('click', function() {
                scrollAnchorCtrl.stopListenToScroll();
                var divPosition = $('#'+attrs.scrollTo).offset();
                $('html, body').animate({
                scrollTop: divPosition.top
                }, "slow", function(){
                    scrollAnchorCtrl.startListenToScroll();
                });
            });
        }
    };
});

I have not tried this code, just made it here as a proof of concept so that you can check if it applies to your needs.

Cheers!

If this code does not cover all your needs, then i'd suggest moving some of the logic to a service and make it dom-aware. It's not the best practice, but under some circumstances it's an accepted solution.

Example of require usage to share directives logic just cooked for you! http://plnkr.co/edit/IxvLbGdNmkFfroCRX5sO?p=preview

Cheers!

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

7 Comments

Where is scrollAnchorCtrl defined? Or does AngularJS auto-understand the Ctrl appended to the name?
Angular automatically injects as the forth parameter the controller of the directive defined at the require attribute. It doesn't matter how you name the parameter, it will always inject the one defined at require if it is avaiable for that dom item
But how does it relate scrollAnchor and scrollAnchorCtrl as they have different names. And nothing in the scrollAnchor directive says it is called scrollAnchorCtrl
It's simple actually. The steps to understand it are this. 1° The scrollTo directive has a defined dependency to the scrollAnchor directive because of this line: "require: 'scrollAnchor'" 2° If the scrollTo directive finds a scrollAnchor directive on the same dom element, it automatically injects as the forth parameter of the link function it's controller no matter how you name that parameter
the link function parameter injection works by order and not by name like all other dependency injections inside of angularjs
|
1

You don't need to redefine $scope or $state as you have already injected it into your controller:

$scope.startListenToScroll = function() {
    // ...
}

$scope.startListenToScroll();

Comments

1

You are actually overwriting your $state injection in this function:

    $scope.startListenToScroll = function($scope, $state) {

Remove those parameters and you should be fine. Your code will pick up the $scope and $state injection on the controller itself. You only need to inject those once.

$scope.startListenToScroll = function() {

Comments

1

Doing by your way, you are getting the variables $scope and $state from argument, and in your directive you don't have them, because of that you getting error, you can not define them like a parameter and they will be taken from closure

phonecatControllers.controller('AboutCtrl', function($scope, $state) {

            $scope.startListenToScroll = function() {

                $('.subsection').each(function(i) {
                    var position = $(this).position();
                    $(this).scrollspy({
                        min: position.top,
                        max: position.top + $(this).height(),
                        onEnter: function(element, position) {
                            if(element.id){
                                $state.transitionTo('about.'+element.id);
                            } else {
                                $state.transitionTo('about');
                            }
                        }
                    });
                });
            }

            $scope.startListenToScroll();

            $scope.stopListenToScroll = function(){
                $('.subsection').unbind().removeData();
            }

        });

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.