Multiple directives [myPopup, myDraggable] asking for new/isolated scope

84,328

Solution 1

From docs:

Example scenarios of multiple incompatible directives applied to the same element include:

Multiple directives requesting isolated scope.

Multiple directives publishing a controller under the same name.

Multiple directives declared with the transclusion option.

Multiple directives attempting to define a template or templateURL.

Try removing isolate scope on myDraggable's directive:

app.directive('myDraggable', ['$document',
    function ($document) {
    return {
        restrict: 'A',
        replace: false,
        scope: { enabled: '=myDraggable' }, //remove this line

Replace scope.enabled with attrs.enabled:

if (attrs.enabled == "true") {

And modify your template to bind the enable attribute:

<div my-draggable="draggable" enabled="{{draggable}}"

DEMO

Solution 2

A DOM element is creating a collision with your attempted isolate scopes. Therefore, you should always ask yourself if an isolate scope is needed.

Consider removing the isolate scope on myDraggable, interpolating the myDraggable value (like you did with isDraggable), and accessing the attribute in the link function.

<div class="draggable" my-draggable="{{isDraggable}}">I am draggable {{isDraggable}}</div>
...

replace: false,

link: function (scope, elm, attrs) {
  var startX, startY, initialMouseX, initialMouseY,
      enabled = attrs.myDraggable === 'true';
  if (enabled === true) {

...

See your updated Plunker here and notice the change in the myPopup template.

If you want to see the myDraggable attribute changes then implement something like:

attrs.$observe('myDraggable', function(iVal) {
  enabled = iVal === 'true';
  // AND/OR
  if (iVal === 'true') doSomething();
});

See Angular Attribute Docs $observe function

Solution 3

I ran into a similar situation. If it doesn't mess up your layout and you definitely need to have an isolate scope on both directives, my suggestion would be to remove the property replace: true from the myPopup directive definition.

Solution 4

There is a way to work around it.

You will not isolate scope of the directive, instead of it, we will create a new isolated scope using $new method . This method creates a new child scope, if you use true at 1st parameter it will create an isolated scope:

If true, then the scope does not prototypically inherit from the parent scope. The scope is isolated, as it can not see parent >scope properties. When creating widgets, it is useful for the widget to not accidentally read parent state.

But it isn't a problem because we have access to private scope by the directive link function, so is possible to work parallel with "parent" and isolated scope into a very close behavior of a directive with an isolated scope.

Se the example bellow:

app.directive('myDraggable', ['$document',
    function ($document) {
    return {
        restrict: 'A',
        replace: false,
        scope: false,
        //scope: { enabled: '=myDraggable', oneWayAttr: "@" }, //Just for reference I introduced a new 
        link: function(parentScope, elem, attr) {
        var scope = parentScope.$new(true); //Simulated isolation.
            scope.oneWayAttr = attr.oneWayAttr; //one-way binding @
            scope.myDraggable = parentScope.$parent.$eval(attr.myDraggable);

            scope.watchmyDraggable = function () {
                    return scope.myDraggable = parentScope.$parent.$eval(attr.myDraggable); //parent -> isolatedscope
            };          
            scope.$watch(scope.watchmyDraggable, function(newValue, oldValue) {
             //(...)
            });

            parentScope.innerScope = scope; //If you need view access, you must create a kind of symbolic link to it.

        //(...)
        }

I developed this work around to a validation directive, that's works very well.

Solution 5

I have included my directive js file twice when I compressed my app. This caused the error.

Share:
84,328
Martin
Author by

Martin

Updated on July 09, 2022

Comments

  • Martin
    Martin almost 2 years

    I wrote a directive for dialogs (myPopup) and another one for dragging this dialog (myDraggable), but I allways get the error:

    Multiple directives [myPopup, myDraggable] asking for new/isolated scope

    Here is a Plunker: http://plnkr.co/edit/kMQ0hK5RnVw5xOBdDq5P?p=preview

    What can I do?

    JS code:

    var app = angular.module('myApp', []);
    
    function myController($scope) {
        $scope.isDraggable = true;
    }
    
    
    app.directive('myPopup', [
        function () {
            "use strict";
    
            return {
                restrict: 'E',
                replace: true,
                transclude: true,
                template: '<div my-draggable="draggable"class="dialog"><div class="title">{{title}}</div><div class="content" ng-transclude></div></div>',
                scope: {
                    title: '@?dialogTitle',
                    draggable: '@?isDraggable',
                    width: '@?width',
                    height: '@?height',
                },
                controller: function ($scope) {
                    // Some code
                },
                link: function (scope, element, attr) {
                    if (scope.width) {
                        element.css('width', scope.width);
                    }
    
                    if (scope.height) {
                        element.css('height', scope.height);
                    }                    
                }
            };
        }
    ]);
    
    app.directive('myDraggable', ['$document',
        function ($document) {
        return {
            restrict: 'A',
            replace: false,
            scope: { enabled: '=myDraggable' },
    
            link: function (scope, elm, attrs) {
                var startX, startY, initialMouseX, initialMouseY;
    
                if (scope.enabled === true) {
                    elm.bind('mousedown', function ($event) {
                        startX = elm.prop('offsetLeft');
                        startY = elm.prop('offsetTop');
                        initialMouseX = $event.clientX;
                        initialMouseY = $event.clientY;
                        $document.bind('mousemove', mousemove);
                        $document.bind('mouseup', mouseup);
                        $event.preventDefault();
                    });
                }
    
                function getMaxPos() {
                    var computetStyle = getComputedStyle(elm[0], null);
                    var tx, ty;
                    var transformOrigin =
                        computetStyle.transformOrigin ||
                        computetStyle.webkitTransformOrigin ||
                        computetStyle.MozTransformOrigin ||
                        computetStyle.msTransformOrigin ||
                        computetStyle.OTransformOrigin;
                    tx = Math.ceil(parseFloat(transformOrigin));
                    ty = Math.ceil(parseFloat(transformOrigin.split(" ")[1]));
                    return {
                        max: {
                            x: tx + window.innerWidth - elm.prop('offsetWidth'),
                            y: ty + window.innerHeight - elm.prop('offsetHeight')
                        },
                        min: {
                            x: tx,
                            y: ty
                        }
                    };
                }
    
                function mousemove($event) {
                    var x = startX + $event.clientX - initialMouseX;
                    var y = startY + $event.clientY - initialMouseY;
                    var limit = getMaxPos();
                    x = (x < limit.max.x) ? ((x > limit.min.x) ? x : limit.min.x) : limit.max.x;
                    y = (y < limit.max.y) ? ((y > limit.min.y) ? y : limit.min.y) : limit.max.y;
                    elm.css({
                        top: y + 'px',
                        left: x + 'px'
                    });
                    $event.preventDefault();
                }
    
                function mouseup() {
                    $document.unbind('mousemove', mousemove);
                    $document.unbind('mouseup', mouseup);
                }
            }
        };
    }]);
    
  • Martin
    Martin over 10 years
    Sorry, this does not work because of this part if (scope.enabled === true) { elm.bind('mousedown', function ($event) { @Matthias
  • Martin
    Martin over 10 years
    Sorry, the DEMO doen´t work, becouse attrs.enabled is in the link-part and so it never will be updated. @Khanh TO
  • Khanh TO
    Khanh TO over 10 years
    @Martin: I don't see you bind it anywhere. In case you need to get notified when attrs.enabled is updated, you could try scope.$watch(function(){ return attrs.enabled },function (value){});
  • Martin
    Martin over 10 years
    Okay, now I have solved it. Here is my Plunker
  • Stephen J Barker
    Stephen J Barker over 10 years
    Awesome, @Martin! If this is solved will you mark an accepted answer and vote for the relevant answers? Thanks!
  • beNerd
    beNerd over 6 years
    anyway we can have a two-way data binding with this?
  • LeonanCarvalho
    LeonanCarvalho over 6 years
    Yes, you can. But it's emulated with an $watch
  • Christiaan Westerbeek
    Christiaan Westerbeek about 6 years
    After reading bennadel.com/blog/… I scrolled down to find out you answered it already. This needs an upvote since the rest is just a distraction when by accident you defined the component twice by mistake. Like this for example: angular.module('myapp', []).component('thing', Thing).component('thing', Thing). The mistake will cause this error once rendered: > Multiple directives [thing, thing] asking for new/isolated scope on: <thing>