Search code examples
javascriptangularjsangularjs-directiveangularjs-ng-transclude

Illegal use of ngTransclude directive in the template! when doing transclusion manually


I preciously asked on SO if it was possible to transclude the inner contents of a directive twice in the directive template (clone it and insert it in two places in the template).

A very helpful person helped me put this plunkr together.

http://plnkr.co/edit/k2UB1o4CTHtZ1voS0OKN?p=preview

It seems to work at first. The problem comes when I use any child element which uses transclusion itself. The error I get is...

[ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:

For example I have a button directive with the following definition.

angular.module('s4p.directives').directive('s4pButton', function () {

    return {
        restrict: 'E',
        scope: {
            icon: '@'
        },
        transclude: true,
        replace: true,
        template: getTemplate
    };

    function getTemplate(element, attr) {

        var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type;

        return  '<button s4p-button type="' + btnType + '">'+
                    '<s4p-button-content ng-transclude></s4p-button-content>'+
                    '<s4p-button-icon ng-if="icon">'+
                        '<s4p-icon href="{{icon}}"></s4p-icon>'+
                    '</s4p-button-icon>'+
                '</button>';
    }

});

as soon as I put one of my buttons inside the tool bar and it tries to clone it I get the above error.

EDIT:

New PLUNKR with full example

http://plnkr.co/edit/uK8r4EA2IPRnYKfjWNVG?p=preview

Any help would be greatly appreciated.


Solution

  • Problem Code

    The directive was trying to do the double transclusion in one invocation of the transclude function.

    //PROBLEM Code
    link: function(scope, element, attrs, controller, transclude) {
        transclude(function(clone, scope) {
            element.find('[transclude-main]').replaceWith(clone);
            element.find('[transclude-overflow]').replaceWith($compile(clone.clone())(scope));
        });
    }
    

    Correct Code

    To transclude the directive contents into two places in the template, invoke the transclusion function twice.

    app.directive('toolbar', function($compile) {
        return {
            restrict: 'E',
            scope: {},
            transclude: {
    
            },
            template: 
                '<toolbar-main><div transclude-main></div></toolbar-main>' +
                '<toolbar-overflow><div transclude-overflow></div></toolbar-overflow>',
            //CORRECTED code
            link: function(scope, element, attrs, controller, transclude) {
                transclude(scope, function(clone) {
                    element.find('[transclude-main]').replaceWith(clone);
                });
                transclude(scope, function(clone) {
                    element.find('[transclude-overflow]').replaceWith(clone);
                });
            }
        };
    });
    

    If you need new scopes for the transclusion, you can create them with scope.$new().

    var newScope = scope.$new();
    transclude(newScope, function(clone) {
        element.find('[transclude-main]').replaceWith(clone);
    });
    

    For more information on creating new scopes, see AngularJS $rootScope.scope API Reference -- $new.


    Using AngularJS jqLite

    AngularJS jqLite is a tiny, API-compatible subset of jQuery that allows Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most commonly needed functionality with the goal of having a very small footprint.1

    The find method of jqLite does not support attribute selectors. To make the above example compatible with jqLite, use custom tags for the transclusion targets.

    app.directive('toolbar', function($compile) {
        return {
            restrict: 'E',
            scope: {},
            transclude: {},
            template: 
                '<toolbar-main><my-main></my-main></toolbar-main>' +
                '<toolbar-overflow><my-overflow></my-overflow></toolbar-overflow>',
            //CORRECTED code
            link: function(scope, element, attrs, controller, transclude) {
                transclude(scope, function(clone) {
                    element.find('my-main').replaceWith(clone);
                });
                transclude(scope, function(clone) {
                    element.find('my-overflow').replaceWith(clone);
                });
            }
        };
    });
    

    This way there is no need to add jQuery to the app as a dependency.