95

I cannot seem to find a way to call a function on the parent scope from within a directive without using isolated scope. I know that if I use isolated scope I can just use "&" in the isolated to access the function on the parent scope, but using isolated scope when it isn't necessary has consequences. Consider the following HTML:

<button ng-hide="hideButton()" confirm="Are you sure?" confirm-action="doIt()">Do It</button>

In this simple example, I want to show a JavaScript confirm dialog and only call doIt() if they click "OK" in the confirmation dialog. This is simple using an isolated scope. The directive would look like this:

.directive('confirm', function () {
    return {
        restrict: 'A',
        scope: {
            confirm: '@',
            confirmAction: '&'
        },
        link: function (scope, element, attrs) {
            element.bind('click', function (e) {
                if (confirm(scope.confirm)) {
                    scope.confirmAction();
                }
            });
        }
    };
})

But the problem is, because I'm using isolated scope, ng-hide in the example above no longer executes against the parent scope, but rather in the isolated scope (since using an isolated scope on any directive causes all directives on that element to use the isolated scope). Here is a jsFiddle of the above example where ng-hide is not working. (Note that in this fiddle, the button should hide when you type "yes" in the input box.)

The alternative would be to NOT use an isolated scope, which actually is what I really want here since there is no need for this directive's scope to be isolated. The only problem I have is, how do I call a method on the parent scope if I don't pass it in on on isolated scope?

Here is a jsfiddle where I am NOT using isolated scope and the ng-hide is working fine, but, of course, the call to confirmAction() doesn't work, and I don't know how to make it work.

Please note, the answer I am really looking for is how to call functions on the outer scope WITHOUT using an isolated scope. And I am not interested in making this confirm dialog work in another way, because the point of this question is to figure out how to make calls to the outer scope and still be able to have other directives work against the parent scope.

Alternatively, I would be interested to hear of solutions that use an isolated scope if other directives will still work against the parent scope, but I don't think this is possible.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jim Cooper
  • 5,113
  • 5
  • 30
  • 35
  • For those passing by, Dan Wahlin has written a very good article explaining isolate scope and function parameters: http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters – tanguy_k Mar 24 '15 at 01:47

4 Answers4

118

Since the directive is only calling a function (and not trying to set a value on a property), you can use $eval instead of $parse (with a non-isolated scope):

scope.$apply(function() {
    scope.$eval(attrs.confirmAction);
});

Or better, simply just use $apply, which will $eval()uate its argument against the scope:

scope.$apply(attrs.confirmAction);

Fiddle

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Did not know that, thanks for that. I've always thought the $parse method was a bit too wordy. – Clark Pan Jul 12 '13 at 00:26
  • 2
    how would you set parameters on that function? – CMCDragonkai Sep 04 '13 at 09:50
  • 2
    @CMCDragonkai, if the function has arguments, you could specify them in the HTML, `confirm-action="doIt(arg1)"`, and then set `scope.arg1` before calling $eval. However, it would probably be cleaner to use $parse: http://stackoverflow.com/a/16200618/215945 – Mark Rajcok Sep 04 '13 at 14:40
  • Is it not possible to do scope.$eval(attrs.doIt({arg1: 'blah'});? – CMCDragonkai Sep 05 '13 at 02:36
  • 3
    @CMCDragonkai, no, `scope.$eval(attrs.confirmAction({arg1: 'blah'})` won't work. You would need to use $parse with that syntax. – Mark Rajcok Sep 05 '13 at 14:03
  • Thanks, one more question, I actually need access to the function as a reference, not to execute the function. How would I do that in this context? The apply, eval and parse methods seem to execute the function. But I need to pass this function as data to another function. – CMCDragonkai Sep 09 '13 at 15:04
  • @CMCDragonkai, see if this works for you: `` (note `doIt`, not `doIt()`) and then in the link function: `console.log(scope[attrs.confirmAction])`. – Mark Rajcok Sep 09 '13 at 19:22
  • Thanks, how would you incorporate safe apply into such a method?: https://coderwall.com/p/ngisma Also I seem to get digest errors when trying the run the function inside callbacks inside Facebook's JS SDK. – CMCDragonkai Sep 17 '13 at 02:49
  • @CMCDragonkai, I suggest you create a new SO question for that. – Mark Rajcok Sep 18 '13 at 13:47
17

You want to make use of the $parse service in AngularJS.

So for your example:

.directive('confirm', function ($parse) {
    return {
        restrict: 'A',

        // Child scope, not isolated
        scope : true,
        link: function (scope, element, attrs) {
            element.bind('click', function (e) {
                if (confirm(attrs.confirm)) {
                    scope.$apply(function(){

                        // $parse returns a getter function to be 
                        // executed against an object
                        var fn = $parse(attrs.confirmAction);

                        // In our case, we want to execute the statement in 
                        // confirmAction i.e. 'doIt()' against the scope 
                        // which this directive is bound to, because the 
                        // scope is a child scope, not an isolated scope. 
                        // The prototypical inheritance of scopes will mean 
                        // that it will eventually find a 'doIt' function 
                        // bound against a parent's scope.
                        fn(scope, {$event : e});
                    });
                }
            });
        }
    };
});

There is a gotcha to do with setting a property using $parse, but I'll let you cross that bridge when you come to it.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Clark Pan
  • 6,027
  • 1
  • 22
  • 18
  • Thanks, Clark, this is exactly what I was looking for. I had tried using $parse, but failed to pass the scope in so it wasn't working for me. This is perfect. – Jim Cooper Jul 11 '13 at 06:14
  • I was surprised to find that this solution also works without scope:true. My understanding was that scope:true is what makes the directive's scope prototypically inherit from the parent scope. Any idea why this still works without it? – Jim Cooper Jul 11 '13 at 06:21
  • 2
    no child scope (deleting `scope:true`) works because the directive will not create a new scope at all, and thus the `scope` property you reference in the `link` function is the exact same scope as the 'parent' scope previously. – Clark Pan Jul 11 '13 at 07:34
  • $apply is sufficient for this case (see my answer). – Mark Rajcok Jul 11 '13 at 17:44
  • 1
    What's the $event for? – CMCDragonkai Sep 05 '13 at 02:37
  • So the responding function has access to the event object – Clark Pan Sep 09 '13 at 02:08
12

Explicitly call hideButton on the parent scope.

Here's the fiddle: http://jsfiddle.net/pXej2/5/

And here is the updated HTML:

<div ng-app="myModule" ng-controller="myController">
    <input ng-model="showIt"></input>
    <button ng-hide="$parent.hideButton()" confirm="Are you sure?" confirm-action="doIt()">Do It</button>
</div>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jason
  • 15,915
  • 3
  • 48
  • 72
  • +1 for a working solution. I had actually tried using $parent and when it didn't work gave up. After seeing your solution, I took a closer look and it turned out I was failing to add $parent to the parameters I was passing in (not shown in my original fiddle). For example, if I wanted to pass showIt in to the hideButton() function I would need to use $parent.hideButton($parent.showIt) and I was missing the second $parent. I am going to accept Clark's answer, however, since that solution doesn't require the user of my directive to know they need to add $parent. Thanks for the insight! – Jim Cooper Jul 11 '13 at 06:12
1

The only problem I have is, how do I call a method on the parent scope if I don't pass it in on on isolated scope?

Here is a jsfiddle where I am NOT using isolated scope and the ng-hide is working fine, but, of course, the call to confirmAction() doesn't work and I don't know how to make it work.

First a small point: In this case the outer controller's scope is not the parent scope of the directive's scope; the outer controller's scope is the directive's scope. In other words, variable names used in the directive will be looked up directly in the controller's scope.

Next, writing attrs.confirmAction() doesn't work because attrs.confirmAction for this button,

<button ... confirm-action="doIt()">Do It</button>

is the string "doIt()", and you can't call a string, e.g. "doIt()"().

Your question really is:

How do I call a function when I have its name as a string?

In JavaScript, you mostly call functions using dot notation, like this:

some_obj.greet()

But you can also use array notation:

some_obj['greet']

Note how the index value is a string. So, in your directive you can simply do this:

  link: function (scope, element, attrs) {

    element.bind('click', function (e) {

      if (confirm(attrs.confirm)) {
        var func_str = attrs.confirmAction;
        var func_name = func_str.substring(0, func_str.indexOf('(')); // Or func_str.match(/[^(]+/)[0];
        func_name = func_name.trim();

        console.log(func_name); // => doIt
        scope[func_name]();
      }
    });
  }
Community
  • 1
  • 1
7stud
  • 46,922
  • 14
  • 101
  • 127