30

I might be thinking about this completely backwards, but I'm trying to make three nested directives, lets call them: screen, component and widget. I want widget to be able to trigger some behavior in component, which in turn triggers some behavior in screen. So:

.directive('screen', function() {
    return {
        scope: true,
        controller: function() {
            this.doSomethingScreeny = function() {
                alert("screeny!");
            }
        }
    }
})

.directive('component', function() {
    return {
        scope: true,
        controller: function() {
            this.componentFunction = function() {
                WHAT.doSomethingScreeny();
            }
        }
    }
})

.directive('widget', function() {
    return {
        scope: true,
        require: "^component",
        link: function(scope, element, attrs, componentCtrl) {
            scope.widgetIt = function() {
                componentCtrl.componentFunction();
            };
        }
    }
})

<div screen>
    <div component>
        <div widget>
            <button ng-click="widgetIt()">Woo Hoo</button>
        </div>
    </div>
</div>

I can require parent components in a widget's link fn using require: "^component", but how do I further give components controller access to its containing screen?

What I need is the WHAT in component so when you click the widget's button it alerts "screeny!".

Thanks.

nicholas
  • 14,184
  • 22
  • 82
  • 138

4 Answers4

36

Here are two ways you could solve your problem:

  1. Since you are using scope: true, all scopes prototypically inherit. So if you define your methods on $scope instead of on this in the screen controller, then both component and widget will have access to function doSomethingScreeny.
    Fiddle.
  2. Define a link function on component and require: '^screen'. In the link function, save the screenCtrl to a scope property, then you can access it in the directive's controller (inject $scope).
    Fiddle.
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Thanks. I've ended up going with your opt 1. But either way it seems you have to drop everything onto the scope, so I'm not sure I'm fully understanding the point of the controller. Or rather what it would be used for. Granted, I've only spent a couple of hours with AngularJS. Any useful tutorials you might point me towards that explain the relationship between services, controllers and directives? Thanks. – nicholas Mar 27 '13 at 03:36
  • 1
    @nicholas, whatever you want to show in a view has to be on the associated $scope. So, the controller is sort of the glue between your models (which are often in services) and your view. Controllers also add behavior to the view via functions. See also http://stackoverflow.com/a/13482008/215945 and http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background – Mark Rajcok Mar 27 '13 at 03:57
  • @MarkRajcok, I've updated the [fiddle](http://jsfiddle.net/StXFK/255/) that demonstrates your second option to more closely match the [fiddle](http://jsfiddle.net/mrajcok/bVWbN/) for your first option. I did this by including both the screen and the component directives' controllers into the widget's `link` function. This change demonstrates how a directive can `require` more than one controller by using an array of strings. – chuck w Sep 28 '14 at 22:22
  • 2
    Controllers allow your directives to interact (require only gets you a reference to the controller) and controllers are ALWAYS easier to test than directives because they don't require you to mock the DOM or template when testing. – Josh G Oct 17 '14 at 20:20
5

Most of this stuffs fails when you want to directly access properties or methods from the parent controller on controller creation. I found another solution by using dependency injection and using the $controller service.

.directive('screen', function ($controller) {
    return {
       require: '^parent',
       scope: {},
       link: function (scope, element, attr, controller) {
           $controller('MyCtrl', {
                $scope: scope,
                $element: element,
                $attr, attr, 
                controller: controller
           });
       }
    }
})

.controller('MyCtrl, function ($scope, $element, $attr, controller) {});

This method is better testable and does not pollute your scope with unwanted controllers.

mlegenhausen
  • 317
  • 2
  • 4
0

return { scope: true } or return { scope: false } is not affect to $scope variable in controller: function($scope) {} in each directive, but directive tag have to putted into the ng-controller or ng-app tag.

JSFiddle

JSFiddle

0

var myApp = angular.module('myApp', [])

.directive('screen', function() {
  return {
    scope: true,
    controller: function() {
      this.doSomethingScreeny = function() {
        alert("screeny!");
      }
    }
  }
})

.directive('component', function() {
  return {
    scope: true,
    controller: function($element) {
      this.componentFunction = function() {
        $element.controller('screen').doSomethingScreeny();
      }
    }
  }
})

.directive('widget', function() {
  return {
    scope: true,
    controller: function($scope, $element) {
      $scope.widgetFunction = function() {
        $element.controller('component').componentFunction();
      }
    }
  }
})

.controller('MyCtrl', function($scope) {
  $scope.name = 'Superhero';
})
<body ng-app="myApp">

  <div ng-controller="MyCtrl">
    <div screen>
      <div component>
        <div widget>
          <button ng-click="widgetFunction()">Woo Hoo</button>
        </div>
      </div>
    </div>
  </div>

</body>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>

If you want to access a function defined in screen directive controller from component directive controller (not a link function), you can use $element.controller('screen').doSomethingScreeny() (from component directive).

JSFiddle

Angular documentation:

  • 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').
Felix
  • 3,999
  • 3
  • 42
  • 66