Search code examples
javascriptangularjsangularjs-directiveangularjs-components

Error: [$compile:multidir] for Component Directive with Attribute Directive


I need a 'sticky' directive that adds a css class to element when it is at the top of a page and also signals the changes in its state. For that reason I define a scope like { onStickyChange: '&' }. Now I'd like to use the directive in an angularjs component like:

<my-component sticky on-sticky-change="$ctrl.onStickyChange(sticky)">
</my-component>

I expected the directive to notify the parent controller when the my-component is sticked/unsticked. However I get the following error:

Error: [$compile:multidir] Multiple directives [myComponent, sticky] asking for new/isolated scope on: http://errors.angularjs.org/1.6.2/$compile/multidir?p0=myComponent&p1=&p2=s…icky%3D%22%22%20on-sticky-change%3D%22%24ctrl.onStickyChange(sticky)%22%3E at angular.js:68 at assertNoDuplicate (angular.js:10049) at applyDirectivesToNode (angular.js:9237) at compileNodes (angular.js:8826) at compileNodes (angular.js:8838) at compileNodes (angular.js:8838) at compile (angular.js:8707) at angular.js:1847 at Scope.$eval (angular.js:18017) at Scope.$apply (angular.js:18117)

app.component('myComponent', {
    template: '<div style="height: 6000px; width: 100%; background-color: #ccf></div>',
    controller: function () {
        this.is = 'nothing';
    }
});
app.directive('sticky', ['$window', function($window) {
    return {
        restrict: 'A',
        scope: { onStickyChange: '&' },
        link: link
    };
    function link(scope, element, attributes) {
        if (typeof scope.onStickyChange !== 'function' ) {
            throw Error('Sticky requires change handler');
        }

        let sticky = isSticky(element);

        angular.element($window).bind('scroll', _.throttle(onWindowScroll, 60));

        function onWindowScroll() {
            let isNowSticky = isSticky(element);

            if (sticky === isNowSticky) {
                return;
            }

            sticky = isNowSticky;

            if (sticky) {
                element.addClass('sticky');
            }
            else {
                element.removeClass('sticky');
            }

            scope.onStickyChange({ sticky: sticky });
        }

        function isSticky(element) {
            return window.scrollTop() > element.position().top;
        }
    }

}]);

How is it possible to solve the problem?

PS: there is a plunk.


Solution

  • The error occurs because both the component directive and the attribute directive are trying to create an isolate scope.

    From the Docs:

    Error: $compile:multidir

    Multiple Directive Resource Contention

    This error occurs when multiple directives are applied to the same DOM element, and processing them would result in a collision or an unsupported configuration.

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

    • Multiple directives requesting isolated scope.

    — AngularJS Error Reference - Error: $compile:multidir

    The solution is to re-write the attribute directive to work without creating an isolate scope:

    app.directive('sticky', function($window, $parse) {
        return {
            restrict: 'A',
            ̶s̶c̶o̶p̶e̶:̶ ̶{̶ ̶o̶n̶S̶t̶i̶c̶k̶y̶C̶h̶a̶n̶g̶e̶:̶ ̶'̶&̶'̶ ̶}̶,̶
            scope: false,
            link: postLink
        };
        function postLink(scope, elem, attrs) {
    
            //code ...
    
                ̶s̶c̶o̶p̶e̶.̶o̶n̶S̶t̶i̶c̶k̶y̶C̶h̶a̶n̶g̶e̶(̶{̶ ̶s̶t̶i̶c̶k̶y̶:̶ ̶s̶t̶i̶c̶k̶y̶ ̶}̶)̶;̶
                $parse(attrs.onStickyChange)(scope, { sticky: sticky });
    
            //code ...
        }
    });
    

    Use the $parse Service to evaluate the Angular Expression on the on-sticky-change attribute.