6

I've got a directive that has a model bound 2-ways, when calling a save() method via ng-click the parent scope isn't updated unless I call $scope.$apply() which then throws the $apply already in progress error.

I'm using ngResource, and the event has a listener calling $scope.model.$save();

Is there a work-around for this? Or am I doing something completely wrong?

.directive('editable', function(){
    return {
        restrict: 'AE',
        templateUrl: '/assets/partials/editable.html',
        scope: {
            value: '=editable',
            field: '@fieldType'
        },
        controller: function($scope){

           ...

            $scope.save = function(){
                $scope.value = $scope.editor.value;
                $scope.$emit('saved');
                $scope.toggleEditor();
            };

        }
    };
})

UPDATE

It looks like it is updating the parent after all but that the emit is being fired before the digest has finished completing. I can force it to the end of the stack using $timeout but it feels a bit hacky. Is there a better way?

$scope.$on('saved', function(){
    $timeout(function(){
        $scope.contact.$update();
    }, 0);
});
8
  • Can you please show this in a jsfiddle or plunkr? Commented Mar 16, 2014 at 18:28
  • It looks like it's working here but that's just the value in the isolate scope updating. jsfiddle.net/Rw46r/1 If I look in Batarang I can see that the parent isn't being updated. Commented Mar 16, 2014 at 18:52
  • I'm not sure how that would help force the parent scope to update? As I said, it seems to work when I force a $scope.$apply(); but that's not ideal. Commented Mar 16, 2014 at 18:56
  • If you pass through an object then update the property on the object it should work. The problem is Strings are immutable in javascript so it really just replaces the local reference instead of updating the value. Here's an updated fiddle see if this works how you expect jsfiddle.net/Rw46r/4 Commented Mar 16, 2014 at 19:00
  • Unfortunately that doesn't work with what the directive is trying to achieve. Having a bit of a play it seems that the parent is in fact being updated, just slower then the even is being emitted. Commented Mar 16, 2014 at 19:02

1 Answer 1

1

How are you calling $scope.save ? If you use one of the angular directives, like ng-click, angular will automatically run a digest cycle for you and therefore you don't need to call $scope.$apply(). Internally, angular checks if a digest is already in progress before starting another cycle, so it will handle the issue of digest already in progress for you if you use one of the built-in directives. For example, put this in your directive's template...

<button ng-click="save()">Save</button>

If you need to call save from the controller or link function, you can do a little hack to prevent the 'digest already in progress' error. Use the $timeout service to defer the call to the end of the call stack...

$scope.save();
$timeout(function() {
    $scope.$apply();
}, 0);

We are setting the timeout to 0, so there is no real delay, but this is still enough to push the $apply call to the end of the current call stack, which allows the digest that is in progress to finish first and prevents the error. This is not ideal and could imply a larger issue with your design, but sometimes you just have to make it work

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

4 Comments

Yeah, we're using ng-click which is why I thought it strange the $scope.$apply() would be necessary — turns out it isn't. The issue seems to be that our event is being emitted before the digest has finished updating and we're therefore sending old data to the server. I've got it working with a $timeout() in the $scope.$on() handler but it feels really hacky. $scope.$watch() might be a better solution?
This is what I'm now doing and it feels a little dirty gist.github.com/steve228uk/f1f5923d4ed89f4356d1
Yeah I just read the comments. Should have probably done that before answering :) I would prefer $scope.$watch over an event. It works great for this sort of thing if I'm understanding your design properly. If not, another option is to just pass the new value in the event, like.. $scope.$emit('saved', $scope.value); and catch it like.. $scope.$on('saved', event, newValue)
That sounds like it might be a more elegant solution. Gonna swap the event out for a watch. UPDATE: watch actually won't work here, so I think I might pass that value through instead.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.