15
votes

Normal use cases in angular

If you have a parent directive and a child directive you create methods in the controller of the parent directive and require the parent controller in your child directive. Angular will pass the parent controller into your child directives link function.

My use case

I have a use case where the child directive is a parent for another directive. I have on directive on the top that is required by the directive in the middle. The middle directive is required by the last directive at the bottom.

In an easy world I could just create a link method and a controller for the middle directive. The link method handles everything with the top controller and the middle controller is passed to the bottom directive.

In my case the methods in the controller of the middle directive must call methods in the parent so I need the top-controller in the middle-controller and not in the link function of the middle directive!

The Question

How can I inject required controller into the controller instead of the link function

angular.module('app').directive('top', function () {
    return {
        $scope: true,
        templateUrl: "top.html",
        controller: function() {
            this.topMethod = function() {
                // do something on top
            }
        }
    }
});

angular.module('app').directive('middle', function () {
    return {
        $scope: true,
        templateUrl: "middle.html",
        require: "^top",
        controller: function($scope, $attrs, topController) {
            this.middleMethod = function() {
                // do something in the middle

                // call something in top controller, this is the part that makes everything so complicated
                topController.topMethod();
            }
        }
    }
});

angular.module('app').directive('bottom', function () {
    return {
        $scope: true,
        templateUrl: "bottom.html",
        require: "^middle",
        link: function(scope, element, attrs, middleController) {
            $scope.bottomMethod = function() {
                // do something at the bottom

                // call something in the parent controller
                middleController.middleMethod();
            }
        }
    }
});
3

3 Answers

13
votes

Actually there is another way that is less verbose and is used by the angular ngModel itself:

var parentForm = $element.inheritedData('$formController') || ....

Basically they use the fact that the controllers are stored into the data property of the directive dom element.

Still a bit wired, but less verbose and easier to understand.

I don't see a reason why you cannot pass the required controllers into the injection locals for the directive controller.

11
votes

The question is in what order directives are compiled and linked. Suppose we have a html structure like this:

<div top>
   <div middle>
      <div bottom></div>
   </div>
</div>

and the coresponding (simplyfied) directives with a lot of debug output:

.directive('top', function() {
    return {
        controller : function($scope, $element, $attrs) {
            this.topMethod = function() {
                console.log('top method');
            }
        },
        compile : function($element, $attrs) {
            console.log('top compile');
            return {
                pre : function($scope, $element, $attrs) {
                    console.log('top pre');
                },
                post : function($scope, $element, $attrs) {
                    console.log('top post');
                }
            };
        }
    }
})

.directive('middle', function() {
    return {
        require : "^top",
        controller : function($scope, $element, $attrs) {
            this.middleMethod = function() {
                console.log('middle method');
                $scope.topController.topMethod();
            }
        },
        compile : function($element, $attrs) {
            console.log('middle compile');
            return {
                pre : function($scope, $element, $attrs, topController) {
                    console.log('middle pre');
                    $scope.topController = topController;
                },
                post : function($scope, $element, $attrs, topController) {
                    console.log('middle post');
                }
            };
        },
    }
})

.directive( 'bottom', function() {
    return {
        require : "^middle",
        compile : function($element, $attrs) {
            console.log('bottom compile');
            return {
                pre : function($scope, $element, $attrs, middleController) {
                    console.log('bottom pre');
                    middleController.middleMethod();
                },
                post : function($scope, $element, $attrs, middleController) {
                    console.log('bottom post');
                }
            };
        }
    }
})

we got the following output:

top compile
middle compile
bottom compile

top pre
middle pre
bottom pre

middle method
top method

bottom post
middle post
top post

As we can see first the compile function is called. Then the pre linking function is called and after that the post linking function is called. compile and pre are going from top to bottom and post goes from bottom to top. So we have to set the controller in the pre linking function.

7
votes

Taken from romario333's comment: The cleanest solution is to simply use

var topController = $element.controller('top') // pass directive name or controller name

From the docs:

controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').

$element can be injected into your directive controller.