Subscribe via RSS Feed

Working Through Angular.js With Transclude

June 24, 2014 1 Comment

Once you dive into Angular, creating custom directives is a daily chore and having good understanding of transclusion becomes imperative. To explain it in one sentence, transclusion consists of plucking out the content of a custom directive, processing it against right scope and then placing it at a marked position in the template of that directive. While the basic transclude using the built-in ngTransclude directive is easy enough,

Basic transclude:
Let’s start with the most basic transclude example. Let’s say, we are creating a buttonBar directive that a user can use to add a group of buttons to a page. The directive takes care of alignment of the buttons.

Transclude at multiple locations:
Lets say we would like to enhance our buttonBar directive to have two kinds of buttons – primary and secondary; with primary buttons right-aligned and secondary left-aligned. It would entail picking up the contents of the directive and adding them into two separate divs, one for primary buttons and the other for secondary buttons. Transclude into two different locations, if you will.

Let us see what javascript has to say about this,

var testapp = angular.module('testapp', []);

testapp.controller('parentController', ['$scope', '$window',function($scope, $window) {
    $scope.primary1Label = 'Prime1';
    $scope.onPrimary1Click = function() {
        $window.alert('Primary 1 clicked');
    }        
}]);

testapp.directive('primary', function() {
    return {
        restrict: 'C',
        link: function(scope, element, attrs) {
            element.addClass('btn btn-primary');
        }
    }
});

testapp.directive('secondary', function() {
    return {
        restrict: 'C',
        link: function(scope, element, attrs) {
            element.addClass('btn');
        }
    }
});    

testapp.directive('buttonBar', function() {
    return {
        restrict: 'EA',
        template: '<div class="span4 well clearfix"><div class="primary-block pull-right"></div><div class="secondary-block"></div><div class="transcluded" ng-transclude></div></div>',
        replace: true,
        transclude: true,
        link: function(scope, element, attrs) {
            var primaryBlock = element.find('div.primary-block');
            var secondaryBlock = element.find('div.secondary-block');
            var transcludedBlock = element.find('div.transcluded');
            var transcludedButtons = transcludedBlock.children().filter(':button');
            angular.forEach(transcludedButtons, function(elem) {
                if (angular.element(elem).hasClass('primary')) {
                    primaryBlock.append(elem);
                } else if (angular.element(elem).hasClass('secondary')) {
                    secondaryBlock.append(elem);
                }
            });
            transcludedBlock.remove();
        }
    };
});

So,in the above we can see that we have used (transclude:true). The question is when should we use it?
Transclude compiles the content of the element and make it available to the directive. Typically used with ngTransclude. The advantage of transclusion is that the linking function receives a transclusion function which is pre-bound to the correct scope. In a typical setup the widget creates an isolate scope, but the transclusion is not a child, but a sibling of the isolate scope. This makes it possible for the widget to have private state, and the transclusion to be bound to the parent scope.

true – transclude the content of the directive. As example after compiling we get

<div>
  <my-transclude-true>
    <!-- transcluded -->
  </my-transclude-true>
</div>

‘element’ – transclude the whole element including any directives defined at lower priority. As example after compiling we get

<div>
<!--transcluded-->
</div>

where the complete div with parent and child alike are transcluded.

Transclude argument to compile function in a directive

function compile(tElement, tAttrs, transclude) { ... }

And here is what it says about the third argument – A transclude linking function:

function(scope, cloneLinkingFn).

Let us view an example of the linking function

var testapp = angular.module('testapp', []);

testapp.controller('parentController', ['$scope', '$window', function($scope, $window) {
    $scope.primary1Label = 'Prime1';

    $scope.onPrimary1Click = function() {
        $window.alert('Primary 1 clicked');                
    }
}]);

testapp.directive('primary', function() {
    return {
        restrict: 'C',
        link: function(scope, element, attrs) {
            element.addClass('btn btn-primary');
        }
    }
});

testapp.directive('secondary', function() {
    return {
        restrict: 'C',
        link: function(scope, element, attrs) {
            element.addClass('btn');
        }
    }
});

testapp.directive('buttonBar', function() {
    return {
        restrict: 'EA',
        template: '<div class="span4 well clearfix"><div class="primary-block pull-right"></div><div class="secondary-block"></div></div>',
        replace: true,
        transclude: true,
        compile: function(elem, attrs, transcludeFn) {
            return function (scope, element, attrs) {
                transcludeFn(scope, function(clone) {
                    var primaryBlock = elem.find('div.primary-block');
                    var secondaryBlock = elem.find('div.secondary-block');
                    var transcludedButtons = clone.filter(':button'); 
                    angular.forEach(transcludedButtons, function(e) {
                        if (angular.element(e).hasClass('primary')) {
                            primaryBlock.append(e);
                        } else if (angular.element(e).hasClass('secondary')) {
                            secondaryBlock.append(e);
                        }
                    });
                });
            };
        }
    };
});

There is no scope available in compile function and you’ll have to pass the element that compile function received as the first argument to the transclude function. You might want to use this approach if you are already using the compile function and don’t need to work with scope.

Injecting $transclude in a Controller:

If you are searching for a different approach than the above i think below can help you a lot.

$transclude – A transclude linking function pre-bound to the correct transclusion scope: function(cloneLinkingFn).

Transclude and scope
A directive isolated scope and transclude scope are siblings. At previous example, you’ll notice that parentController creates a scope, the buttonBar directive declares an isolated scope under it and as mentioned in the Angular documentation, transclude creates yet another scope. I have added log statements at appropriate places in the directive to illustrate the relationship between these three scopes.

So, we have seen many examples and discussed many perspective of transclude. Let’s join them and see what happens? In the below example is a very basic example but on careful watch you can see that it is working through transclude.

Html file:

<div ng-controller="MyCtrl">
    <h2>Parent Scope</h2>
    <input ng-model="foo">     
    <br><br>

    <hr>
    <my-component attribute-foo="{{foo}}" binding-foo="foo" 
        local-expression-foo="showFoo(newFoo)" >
            <span>{{foo}}</span>
    </my-component>
        <my-component attribute-foo="{{foo}}" binding-foo="foo" 
        local-expression-foo="showFoo(newFoo)" >
            <span>Body: {{foo}}</span>
    </my-component>
    <my-component attribute-foo="{{foo}}" binding-foo="foo" 
        local-expression-foo="showFoo(newFoo)" >
            <span>Footer: {{foo}}</span>
    </my-component>
</div>

javascript file:

var myModule = angular.module('myModule', [])
    .directive('myComponent', function () {
        return {
            restrict:'E',
            template: '<div><h1>Hi {{ foo}}</h1><span ng-transclude></span></div>',
            transclude: true,
            scope:{

                localAttributeFoo:'@attributeFoo',
                localBindingFoo:'=bindingFoo',
                localExpressionFoo:'&'
            },
            controller:function($scope){$scope.foo = 'user';}            

        };
    })
    .controller('MyCtrl', ['$scope', function ($scope) {
        $scope.foo = 'Hello!';
        $scope.expressionFoo = 'Hello Expressions!';
        $scope.showFoo = function (newFoo) {
            $scope.expressionFoo = newFoo.split('').reverse().join('');
        }
    }]);

transclude

Related Links:
1>Directive in Angular.js – Part 1
2>Directive in Angular.js – Part 2
3>Working with Modules in Angular.js
4>Angular Router and UI-Router
5>Angularjs and Services
6>Angular.js Promise
7>Angular.js Two Way Data Binding
8>How To Use Filter In Angular.js
9>Angular.js Event Handling
10>Using Controllers in Angular.Js
11>Angularjs with Server Side Interaction
12>Angular Templates

If you find this article helpful, you can connect us in Google+ and Twitter.

Sign Up to read the rest of the content

Email will be used only for updates of our site

No Thanks

Filed in: Angular.js