Angular JS – Transclude and Require

Transclude allows us to define a directive’s content, or part of it, directly from the content we put inside it. This is a very flexible way to compose an interface by directly putting directives one inside the other. The place in the template where to place the inner content can be defined with the ng-transclude directive.

However the Transclude’s default behavior doesn’t always work as expected, so in this example we’ll add some magic in order to make it work in the right way.

<div ng-app="myApp">
    <my-container current-color="red">
        <!--I can have bound HTML-->
        <p>{{description}}</p>
 
        <!--I can have bound directives-->
        <my-content color="red" current-color="currentColor" next-color="blue"></my-content>
        <my-content color="blue" current-color="currentColor" next-color="green"></my-content>
        <my-content color="green" current-color="currentColor" next-color=""></my-content>
    </my-container>
</div>

angular.module("myApp.directives", []);
angular.module("myApp", [
    "myApp.directives"
]);
 
angular.module("myApp.directives").directive("myContainer", function () {
    return {
        restrict: "E",
        replace: true,
        transclude: true,
        scope: {
            currentColor: "@"
        },
        template:
            "<div>" +
                "<h3>{{title}}</h3>" +
                "<div id='transclude'></div>" +
                "<h3>{{footer}}</h3>" +
            "</div>",
        controller: ["$scope", function ($scope) {
            $scope.title = "This is the title";
            $scope.description = "Click on the colors!";
            $scope.footer = "This is the footer";
        }],
        link: function ($scope, elem, attrs, ctrl, transclude) {
            /*
            http://angular-tips.com/blog/2014/03/transclusion-and-scopes
            By default, the transcluded content is not bound to the directive's
            isolate scope, as one could think, but is instead bound to the outer
            controller's one.
 
            ---------------------------        -------------
            | Outer controller        | =====> | Directive |
            |                         |        -------------
            | (the main in this case) |        -----------------------
            |                         | -----> | Transcluded content |
            ---------------------------        -----------------------
 
            =====> = Isolated scope
            -----> = Non-isolated scope
 
            Because of this both $scope.title and $scope.footer would be rendered,
            since they are included in the template, while $scope.description
            wouldn't, because is referred in the transcluded content.
 
            The transclude() function allows us to change this behavior by setting
            what scope the transcluded content should to be compiled with. After
            the content has been compiled we can manually insert it where we want.
 
            IMPORTANT
            Since we override the default behavior in this way we don't have to use
            the ng-transclude directive to specify where the transcluded content
            should be placed. Since that directive automatically inject the
            content as it's generated in the default way, if we do so we'll end
            up having it rendered twice.
            */
            transclude($scope, function (clone) {
                elem.find("#transclude").append(clone);
            });
        }
    };
});
 
angular.module("myApp.directives").directive("myContent", function () {
    return {
        restrict: "E",
        replace: true,
        require: "^myContainer", // <-- Must be insert within a myContainer controller
        scope: {
            color: "@",
            currentColor: "=",
            nextColor: "@"
        },
        template:
            "<div style='background-color:{{color}}'>" +
                "<h3>{{title}}</h3>" +
                "<div ng-if='currentColor == color'>" +
                    "<button ng-click='onNext()'>Next</button>" +
                "</div>" +
            "</div>",
        controller: ["$scope", function ($scope) {
            $scope.title = "This is the content " + $scope.color;
            $scope.onNext = function () {
                $scope.currentColor = $scope.nextColor;
 
                //We can access to the parent's controller
                $scope.$parent.title = $scope.color;
            };
        }]
    };
});


Referring the $parent from the view

As we saw before we can user $scope.$parent to refer the parent’s scope (this is more safe if we use the “require” option to enforce our component to be used with the right parent). However there is some consideration to do when accessing the $parent from the view. The $parent keyword is managed by angular and isn’t always referring the scope we suppose. This is because several angular directives, such as ng-if and ng-repeat create their own child scope, so if we refer to the $parent from inside one of them we’ll just get their outer scope and not the scope of our component’s parent. And easy way to be sure about the parent we are referring to is to map a reference in our scope:

$scope.parent = $scope.$parent;
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s