Sign up ×
Stack Overflow is a community of 4.7 million programmers, just like you, helping each other. Join them, it only takes a minute:

I am creating reusable UI components with AngularJS directives. I would like to have a controller that contains my business logic with the nested components (directives). I want the directives to be able to manipulate a single property on the controller scope. The directives need to have an isolate scope because I might use the same directive more than once, and each instance needs to be bound to a particular controller scope property.

So far, the only way I can apply changes back to the controller's scope is to call scope.$apply() from the directive. But this breaks when I'm inside of an ng-click callback because of rootScope:inprog (scope operation in progress) errors.

So my question: What is the best way to make my controller aware when a child directive has updated a value on the controller's scope?

I've considered having a function on the controller that the directive could call to make an update, but that seems heavy to me.

Here is my code that breaks on an ng-click callback. Keep in mind that I don't just want to solve the ng-click issue. I want the best overall solution to apply reusable directives to modify a parent scope/model.

html

<div ng-controller="myCtrl">
    <my-directive value="val1"></my-directive>
</div>

controller

...
.controller('myCtrl', ['$scope', function ($scope) {
    $scope.val1 = 'something';
}});

directive

...
.directive('myDirective', [function () {

return {
    link: function(scope) {
        scope.buttonClick = function () {
            var val = 'new value';
            scope.value = val;
            scope.$apply(); 
        };
    },
    scope: {
        value: '='
    },
    template: '<button ng-click="buttonClick()"></button>'
};
}]);
share|improve this question
    
You could emit events on $rootScope. – camden_kid Feb 27 at 17:08
    
@camden_kid I was hoping for better encapsulation. At the very least I'll use the controller method I've considered. I don't think I want to add more chatter on the root scope though. – Brett Feb 27 at 17:13
1  
You don't need to call $apply(), since the buttonClick function is called by the ng-click directive, and is thus not executed outside of angular's event handling. If you want to modify an attribute, then what you have is fine (except you shouldn't use $apply()). If you want to call a callback function, then pass a callable function using '&' instead of '='. – JB Nizet Feb 27 at 17:15
    
You could inject a service. One thing I've noticed is that you have a button within the directive element markup. It won't work like that. You need a template (or templateURL) in your directive. – camden_kid Feb 27 at 17:17
    
@camden_kid you're right about the template. I posted it this way for brevity, but I'll make an edit. – Brett Feb 27 at 17:19

2 Answers 2

up vote 2 down vote accepted

The purpose of two-way data binding in directives is exactly what you're asking -- to "[allow] directives to modify a parent scope/model."

Ensure that you are using two-way data binding on the directive attribute which exposes the variable you want to share, and in the controller you can use $watch to detect updates if you need to do something when the value changes. I suppose if it saves a lot of code or improves performance you could put the directive in charge of triggering the change event and provide an on-change function binding, e.g.

<div ng-controller="myCtrl">
    <my-directive value="val1" on-val-change="myFunc"> <!-- Added on-change binding -->
        <button ng-click="buttonClick()"></button>
    </my-directive>
</div>
share|improve this answer
    
you pointed me down the right path. My issue was tangled up with two-way bindings and primitives. – Brett Feb 27 at 17:42

I think your question about $scope.apply is a red herring. I'm not sure what problem it was solving for you as you evolved this demo and question, but that's not what it's for, and FWIW your example works for me without it.

You're not supposed to have to worry about this issue ("make controller aware ... that [something] modified a value on a scope"); Angular's data binding takes care of that automatically.

It is a little complicated here because with the directive, there are multiple scopes to worry about. The outer scope belongs to the <div ng-controller=myCtrl>, and that scope has a .val property, and there's an inner scope created by the <my-directive> which also has a .val property, and the buttonClick handler inside myDirective modifies the inner one. But you declared myDirective's scope with value: '=' which sets up bidirectional syncing of that property value between the inner and outer scope.

So it should work automatically, and in the plunker I created from your question code, it does work automatically.

So where does scope.$apply come in? It's explicitly for triggering a digest cycle when Angular doesn't know it needs to. (And if you use it when Angular did know it needed a digest cycle already, you get a nested digest cycle and the "inprog" error you noticed.) Here's the doc link, from which I quote "$apply() is used to execute an expression in angular from outside of the angular framework". You need to use it, for example, when responding to an event handler set up with non-Angular methods -- direct DOM event bindings, jQuery, socket.io, etc. If you're using these mechanisms in an Angular app it's often best to wrap them in a directive or service that handles the Angular-to-non-Angular interface so the rest of your app doesn't have to worry about it.

(scope.$apply is actually a wrapper around scope.$digest that also manages exception handling. This isn't very clear from the docs. I find it easier to understand the name/behavior of $digest, and then consider $apply to be "the friendlier version of $digest that I'm actually supposed to use".)

One final note on $apply; it takes a function callback argument and you're supposed to do the work inside this callback. If you do some work and then call $apply with no arguments afterwards, it works, but at that point it's the same as $digest. So if you did need to use $apply here, it should look more like:

scope.buttonClick = function() { scope.$apply(function() { scope.value = newValue; }); });

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.