kns
kns

Reputation: 23

It only works with .$apply but throws $digest already in progress

I'm trying to implement a sortable drag-n-drop. I tried ng-sortable and a few others but I have some corner cases where I didn'd manage to tweak them to my needs and decided to do it myself.

I have directives for draggable, droppable, and a main ui-router view/controller.

I'm passing a function ('handleDrop()') accepting some parameters from the controller to the droppable directive. I want the function to update the model in the main view/controller scope when called from the directive.

It works fine, but throws a $digest already in progress error.

It's because in the directive, I wrap the call to handleDrop() with $apply, and then inside the handleDrop, I wrap part of the function in another $apply. Clearly not correct but it's the only way I get it to work - because then it all works fine except the error.

If I remove the $apply in handleDrop() then the view only gets partly updated ( e.g. I get a li-element attached to my list but empty - without the corresponding data.

I tried replacing the $apply in handleDrop() with $timeout based on answers to similar questions and then it works as long as I don't inject $timeout but that obviously throws "$timeout is not defined" - but it also works.

If I inject $timeout into the controller I get no such error (as expected) but then the dom isn't updated properly again..

Below stripped versions of my directive and controller, what I think are the importan parts. drop() is specified in the html to be handleDrop().

Any help on what I'm doing wrong greatly appreciated

Directive:

app.directive('droppable', function() {
   return {
    scope: {
      drop: '&',
      bin: '='
    },
  link: function(scope, element) {
    var el = element[0];

  el.addEventListener(
    'drop',
    function(e) {
      if (e.stopPropagation) e.stopPropagation();

      var item = document.getElementById(e.dataTransfer.getData('text/plain'));
      // Putting additional data  into variables newStage etc

      // call the passed drop function
      scope.$apply(function(scope) {
        var fn = scope.drop();
        if ('undefined' !== typeof fn ) {            
          fn(item, newStage, prevStage, newIndex); 
            document.getElementById(someHtmlElementIdIHave).appendChild(item); 
            }   
        }
      });

      return false;
    },
    false
  );

And the controller:

  .controller('funnelCtrl', function ($scope, $state, $http, someObject) {
    $scope.handleDrop = function(item, newStage, prevStage, newIndex) {

      $http.put('/api/funnel/' + item.id, {some object}).
        success(function (data) {   
                    $timeout(function(){
                        $scope.someObject[newStage].splice(newIndex, 0, item.optyId) ;
                        $scope.someObect[prevStage].splice(prevIndex,1);                            
                    });                         
        }).
        error(function (data) {alert('Error Saving');}); 
    };

Upvotes: 2

Views: 577

Answers (1)

maurycy
maurycy

Reputation: 8465

Instead if $apply() use $timeout as apply triggers digest but timeout will schedule it for next digest process running, although I'm not so sure that you even need it with your code, you don't assign anything to the scope in your current $apply

$timeout(function(scope) {
  var fn = scope.drop();
  if ('undefined' !== typeof fn ) {            
    fn(item, newStage, prevStage, newIndex); 
    document.getElementById(someHtmlElementIdIHave).appendChild(item); 
  }   
})

Upvotes: 1

Related Questions