Transclusion: $transcludeFn

For some time I was wondering what’s that $transcludeFn passed as last argument to AngularJS directive linking function. The documentation did not allow me to fully understand it. So I’ve taken a deep dive into implementation of ng-if, ng-include and ng-repeat directives (ng module built-in functionality). And it seems to be as simple as shooting one right down.


This article is continuation of Transclusion: field-strip

Good understanding of AngularJS Directives mechanics is desirable to fully comprehend this article. Possible knowledge sources:


Introducing “$transclude” function

When DOM is ready, templates included, classes added, rifle scopes mounted and control established it’s time to use the ultimate tool of synergistic directives – the $transcludeFn. It is passed as fifth argument to linking functions of transcluding directives. Once upon a time it was passed into compile function but as compile is called before scope is ready, transclude was quite useless there. The Transclude Function creates clone of transcluded element and then it can handle several scenarios:

function link(scope, element, attrs, ctrl, $transcludeFn) {
  • Compile clone in child scope of directive’s scope and pass clone after calling all it’s linking functions
        var linkedClone = $transcludeFn();
        // do something with clone compiled and linked
        // in child scope of directive's scope:
        element.parent().append(linkedClone);
         
    
  • Compile clone in child scope of directive’s scope and pass clone before calling all its linking functions
        var linkedClone = $transcludeFn(function (notLinkedClone, cloneScope) {
            // do something with clone compiled in child scope
            // of directive's scope (but not linked):
            element.parent().append(notLinkedClone.addClass('hidden'));
            // second arg here is scope in which the
            // clone was compiled (and will be linked),
            // a child of the directive's scope
            console.assert(cloneScope.$parent === scope);
            console.assert(cloneScope === notLinkedClone.scope());
        });
        // do something with clone compiled and linked
        // in child scope of directive's scope (still the same element):
        linkedClone.removeClass('hidden');
         
    
  • Compile clone in any given scope and pass clone before calling all its linking functions
        var customScope = $rootScope.$new();
        var linkedClone = $transcludeFn(
            customScope,
            function (notLinkedClone, cloneScope) {
                // do something with clone compiled in a custom
                // chosen scope (but not linked):
                element.parent().append(notLinkedClone.addClass('hidden'));
                // second arg here is scope in which the
                // clone was compiled (and will be linked),
                // a passed custom chosen scope
                console.assert(cloneScope === customScope);
                console.assert(cloneScope === notLinkedClone.scope());
            }
        );
        // do something with clone compiled and linked
        // in child scope of directive's scope (still the same element):
        linkedClone.removeClass('hidden');
        console.assert(customScope === linkedClone.scope());
         
    
}

So if our directive should just duplicate some element several times it could look like this:

angular.module('myAppDirectives')
.directive('simpleRepeat', function () {
    return {
        link: function (scope, element, attrs, ctrl, $transcludeFn) {
            var count = Math.abs(parseInt(attrs.simpleRepeat, 10) || 0);
            while (count--) {
                $transcludeFn(scope, function (clone) {
                    // be sure elements are inserted
                    // into html before linking
                    element.after(clone);
                });
            }
        },
        priority: 600,
        transclude: 'element'
    };
});

…and be used like this:

<ul>
    <li simple-repeat="5">Some list item to be repeated 5 times</li>
</ul>

After compilation and linking it will look like this:

<ul>
    <!-- simpleRepeat: 5 -->
    <li class="ng-scope" simple-repeat="5">Some list item to be repeated 5 times</li>
    <li class="ng-scope" simple-repeat="5">Some list item to be repeated 5 times</li>
    <li class="ng-scope" simple-repeat="5">Some list item to be repeated 5 times</li>
    <li class="ng-scope" simple-repeat="5">Some list item to be repeated 5 times</li>
    <li class="ng-scope" simple-repeat="5">Some list item to be repeated 5 times</li>
</ul>

Although this simple example looks like using a sledgehammer to crack a nut, real life scenarios need scope binding and asynchronous DOM manipulation where all features of described mechanism are obligate not obsolete.

And the demo fiddle: jsfiddle.net/ulfryk/kds6k25k

Purpose

So why shouldn’t we just $element.clone()?

Beacuse AngularJS is all about data in application. Data is bound to view, and the glue is the $scope. Without transclusion mechanism we would have to handle cloning and compilation of HTML elments by ourselves. It may look like this:

app.directive('cloneAndHide', [$compile, function ($compile) {
    return {
        compile: function (tElement, tAttrs) {
            var cloneHtml, element, insertCopy;
            element = angular.element('<!-- cloneAndHide: ' + tAttrs.cloneAndHide + ' -->');
            tElement.remove();
            cloneHtml = angular.element('<div/>').append(tElement).html();
            pseudoTranscludeFn = function (scope, cloneAttachFn) {
                return $compile(cloneHtml)(scope, cloneAttachFn);
            };
            return function (scope, _e, attrs, ctrl) {
                pseudoTranscludeFn(scope, function (clone) {
                    clone.after(element);
                });
                pseudoTranscludeFn(scope, function (clone) {
                    clone.after(element); // so it's 2 copies right now
                }); 
            }
        }
    };
}]);

And still it would not behave properly used with other directives. It’s much better to use built in transclusion:

app.directive('cloneAndHide', function () {
    return {
        link: function (scope, element, attrs, ctrl, $transcludeFn) {
                $transcludeFn(scope, function (clone) {
                    clone.after(element);
                });
                $transcludeFn(scope, function (clone) {
                    clone.after(element); // so it's 2 copies right now
                });
        },
        transclude: 'element'
    };
});

Examples

Further Reading