15

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 Answers 3

13

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.

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

4 Comments

I think you may have linked to the wrong jsFiddle. It's just a hello world example.
It seems that there is even easier way how to achieve this: var parentForm = $element.controller('form')
I've added a demo for this and romario333's solution here: work.karlhorky.com/frontend-tricks/…
I prefer this solution, as I rarely use the link method, rather than requiring the parent directive as a property, I can conditionally inherit the data from a directive on the controller instance.
11

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.

4 Comments

I had the same idea, but the controller is executed first so the topController is null in the first moment. In a case there the topcontroller must know all middleControllers, so they have to call for example topController.addChild(this) I get a NullpointerException. I also don't know which other sideeffects could appear
@mklemenz what do you mean with "the controller is executed first"?
First the controller is executed and perhaps tries to use the topController which is undefined in this moment. Then the link-method would be executed and would set the topController to the scope, if I would not get an exception in the controller first.
Well, this is actually working. The problem I have with this solution is that this code is hard to understand in the first place without comments about the when and why. I still hope there is an easier solution than this.
7

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.