Cameron Cameron - 1 month ago 9
AngularJS Question

Call controller functions inside directive in AngularJS

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?

Answer

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!