32

I want to write a directive with isolated scope but also want to make that scope available for the parent scope's controller. I found this solution:

<div ng-controller="Main">
  <popupbutton directive-scope="popup"></popupbutton>
</div>

app.directive('popupbutton', [function() {
  return {
    restrict:   "E",
    scope:      {    
      directiveScope: "="
    },
    link:       function(sc, el, attrs) {
      sc.directiveScope = sc;
      sc.testvalue = 'foo';
    }
  };  
}]);

app.controller('Main', function($scope) {
  alert($scope.popup.testvalue);  // Where did the property 'popup' come from?!?
});

See Plunker.

I find this a bit ugly because it involves writing an attribute in HTML and in controller's code you can't tell where a scope property came from. Is there a better way to do this?

Edit:

Besides, it seems that $scope.popup isn't even available when controller 'Main' is run. The directive's linking function isn't executed yet?

Esa Toivola
  • 1,538
  • 3
  • 16
  • 27
  • 1
    You shouldn't be accessing the directive's scope at all from your controller. There is probably a better way to do this - what's your use case? – Josh David Miller Jan 27 '13 at 00:11
  • The directive creates a button and a popup box that can be opened by clicking the button. I'd like the parent controller to see if the box is open or not. The directive's scope also contains the methods to open and close the box and the parent controller should have access to these also. – Esa Toivola Jan 27 '13 at 05:48

1 Answers1

47

To maintain proper separation of concerns, you should not mix scopes. Not to mention that it will be hard to synchronize. To summarize: your directive should not know anything about the parent scope (or its controller) and your controller should not know anything about a directive's internals. They are separate components in separate layers.

The proper way to communicate between a controller and a directive is through directive attributes. In the case of a popup, say, this can be done with a simple boolean value.

The controller and directive:

app.directive('popupbutton', [function() {
  return {
    restrict: "E",
    scope: { isOpen: "=" },
    template: '<a ng-click="isOpen = !isOpen">Toggle</a><div>Open? {{isOpen}}'
  };
}]);

app.controller('MainCtrl', function($scope) {
  $scope.isOpen = false;
});

And the markup:

<popupbutton is-open="isOpen"></popupbutton>

This method requires no logic, works out of the box, and maintains clean separation of concerns. Here's an updated plunker: http://plnkr.co/edit/otIaGCLmiNdGcYEgi60f?p=preview

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • Thanks! With your help I wrote this directive: http://plnkr.co/edit/vqkPBDkYT2zVVEGXBZVQ?p=preview. But if I omit the 'is-open' attribute in HTML, I get an error: "Error: Non-assignable model expression: undefined (directive: popupbutton)". How can I make the attribute 'is-open' optional? – Esa Toivola Jan 27 '13 at 15:47
  • Yes, for simplicity I kept it all the same, whereas in reality you would not do it this way. In the real world, the directive would have its own internal variable that represents the "true state" and a separate attribute with a $watch that will update the true state when it changes. I just collapsed them into one to make the functionality here clear. – Josh David Miller Jan 27 '13 at 17:43
  • I forked your plunker (see http://plnkr.co/edit/vh6yOigpHd7E5ijJ3QEa?p=preview) to explore a strange problem I encountered. If I plase a tag with ng-click attribute inside my directive tag (with 'transclude' set to true) it won't work as excepted. The lower left button does nothing while the lower right works. I can't understand the reason or the difference between these two buttons. – Esa Toivola Jan 28 '13 at 21:23
  • 2
    This will seem counter-intuitive, but the reason is scope inheritance. With transclusion, the contents are evaluated in a *child* scope; i.e. the `ng-click`s are in a scope that prototypically inherits from your controller's scope. So objects (including functions) are references to the values in the parent scope, but primitives (strings, nums, bools) are *copies*. That is, it's not the same `isOpen` anymore. [This article](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance) talks about this more and is a must-read. – Josh David Miller Jan 28 '13 at 21:51
  • 3
    @Esa, I just now updated a [SO answer related to transcluded scope](http://stackoverflow.com/a/14484903/215945) with some pictures. (The article Josh mentions references this answer, but it is kind of buried, so I thought it might be useful to point you right to it.) – Mark Rajcok Jan 29 '13 at 01:59
  • Wow, thanks. I had completely missed that ng-transclude creates a new scope. – Esa Toivola Jan 29 '13 at 05:21
  • What happens if I set `is-open="true"`? – Chris X Aug 02 '13 at 13:30
  • @ChrisX The `=` is for bi-directional data binding, so that will probably get/set the property `$scope["true"]`, though I haven't tested it. If you want to just pass in a value like `true`, you can use `&` or `@`, but you'll have to change the logic a little. If you tell me what you're wanting to do, I can be more specific. – Josh David Miller Aug 02 '13 at 20:34