How to send a DOM function to a directive in AngularJS?

3k views Asked by At

I was following a great tutorial about implementing Drag & Drop in Angular which I found originally in this StackOverflow answer which links to the writer's blog and GitHub code.

When I implemented it and tested it in my project, I got the error:

[$parse:isecdom] Referencing DOM nodes in Angular expressions is disallowed! Expression: dropped(dragEl, dropEl)

According to Angular Docs, the problem is that I am referencing a function that takes DOM objects as parameters inside a DOM attributes - it's happening in this line:

<div x-lvl-drop-target='true' x-on-drop='dropped(dragEl, dropEl)'>drop zone</div>

As you can see, the on-drop attribute is passing this dropped function (which is defined in my controller, to the directive lvlDropTarget (posted below) which calls it to take action of the drag-drop action the user makes. I like this design, as it makes the directive reusable for a number of different drag-drop possiblities in the same app. Essentially, I just need to define a different function in my controller and pass it to the directive through the on-drop attribute.

However, this unfortunately seems to have been shot down by Angular. Does anyone have any other as to how to achieve this same design and functionality but in a way Angular is more OK with?

Here's the lvlDropTarget directive

module.directive('lvlDropTarget', ['$rootScope', 'uuid',
  function ($rootScope, uuid) {
    return {
      restrict: 'A',
      scope: {
        onDrop: '&'
      },
      link: function (scope, el, attrs, controller) {
        var id = angular.element(el).attr("id");
        if (!id) {
          id = uuid.new()
          angular.element(el).attr("id", id);
        }

        el.bind("dragover", function (e) {
          if (e.preventDefault) {
            e.preventDefault(); // Necessary. Allows us to drop.
          }

          if (e.stopPropagation) {
            e.stopPropagation();
          }

          e.dataTransfer.dropEffect = 'move';
          return false;
        });

        el.bind("dragenter", function (e) {
          angular.element(e.target).addClass('lvl-over');
        });

        el.bind("dragleave", function (e) {
          angular.element(e.target).removeClass('lvl-over'); // this / e.target is previous target element.
        });

        el.bind("drop", function (e) {
          if (e.preventDefault) {
            e.preventDefault(); // Necessary. Allows us to drop.
          }

          if (e.stopPropogation) {
            e.stopPropogation(); // Necessary. Allows us to drop.
          }

          var data = e.dataTransfer.getData("text");
          var dest = document.getElementById(id);
          var src = document.getElementById(data);

          scope.onDrop({
            dragEl: src,
            dropEl: dest
          });
        });

        $rootScope.$on("LVL-DRAG-START", function () {
          var el = document.getElementById(id);
          angular.element(el).addClass("lvl-target");
        });

        $rootScope.$on("LVL-DRAG-END", function () {
          var el = document.getElementById(id);
          angular.element(el).removeClass("lvl-target");
          angular.element(el).removeClass("lvl-over");
        });
      }
    }
  }
]);
2

There are 2 answers

0
j.wittwer On BEST ANSWER

An issue was logged on github for this on Oct 14:

AngularJS is now giving this error: "Referencing DOM nodes in Angular expressions is disallowed!", because of this: on-drop="controller.dropped(dragEl, dropEl)".

The following solution has been proposed:

Returning the DOM elements themselves causing an $parse:isecdom error in newer versions of AngularJS. Simply enough, let the dropped(...) callback handle fetching the elements.

suggested code change on github

So, if this change is accepted, the directive will be modified to return the dragged and dropped element ids. Then it is up to your callback to do document.getElementById:

$scope.dropped = function(dragId, dropId) {
  var dragEl = document.getElementById(dragId);
  var dropEl = document.getElementById(dropId);

  console.log(dragEl, dropEl);
}

Having said that, Angular added isecdom validation to their expression sandboxing with good reason:

AngularJS restricts access to DOM nodes from within expressions since it's a known way to execute arbitrary Javascript code.

This check is only performed on object index and function calls in Angular expressions. These are places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not perform this check - it's up to the developer to not expose such sensitive and powerful objects directly on the scope chain.

To resolve this error, avoid access to DOM nodes.

Rather than attempt to circumvent this protection, I prefer your choice to use ngDraggable - which encourages use of declarative views and doesn't require DOM manipulation in your controller. Good call.

1
artur grzesiak On

I think, the solution to your problem is rather simple -- use intermediate object to wrap dragEl and dropEl, like so:

in link function:

scope.onDrop({ 
        event : {
          dragEl: src,
          dropEl: dest
        }
      });

in html:

<div x-lvl-drop-target='true' x-on-drop='dropped(event)'>drop zone</div>

in outside:

$scope.dropped = function(event){
   console.log('dropped', event);
}

And working DEMO.


Alternatively you can keep info about the drag event in a value, like so:

app.value('dropEvent', {dropEl:null, dragEl:null});

app.directive('lvlDropTarget',
  function ($rootScope, dropEvent) {
    return {

 //... 

      link: function (scope, el, attrs, controller) {

 //...

        el.bind("dragover", function (e) {

 //... 

          dropEvent.dragEl = src;
          dropEvent.dropEl = dest;

          scope.onDrop();
        });
 //...

    }
  }
);

Then to use it in a controller / outside-context:

app.controller('MainCtrl', function($scope, dropEvent) {
  $scope.dropped = function(){
    console.log('dropped', dropEvent);
  }
});

DEMO