Angular directives and scope

When creating directives with angular, you can choose to use the parent’s scope, create your own isolated scope for the directive or have an inherited one. Let’s see what types of scope we have and how to work with them:

Let’s start by considering this main controller and template:

        var app = angular.module("testingDirectives", []);

        app.controller("mainController", function($scope, $timeout) {
            $scope.MainValue = "main";
            $scope.ValueToOverride = "notOverridden";
            $scope.ValueToOverrideOn6 = "notOverridden";

            $timeout(function() {
                $scope.MainValue = "main updated";
            }, 1000);
        });
    <div ng-app="testingDirectives" ng-controller="mainController">
        {{MainValue}}
        
        <directive-one></directive-one>
        <directive-two></directive-two>
        <directive-three></directive-three>
        <directive-four custom-att="attribute"></directive-four>
        <directive-five custom-att="attribute"></directive-five>
        <directive-six custom-att="attribute" custom-att2="overrided"></directive-six>
        <directive-six custom-att="attribute" custom-att2="overrided 2"></directive-six>
        <directive-seven custom-att="attribute" custom-att2="overrided"></directive-seven>
        <directive-seven custom-att="attribute" custom-att2="overrided 2"></directive-seven>
        <directive-eight from-parent="{{MainValue}}"></directive-eight>
        <directive-nine from-parent="{{MainValue}}"></directive-nine>
    </div>

Using the parent scope

In this case, the directive doesn’t have a scope of its own, it just uses the parent’s one. Useful to simplify and allow access to main scope:

        app.directive("directiveOne", function () {
            // no scope set.
            // result: writes directive + main
            return {
                restrict: 'E',
                replace: 'true',
                controller: function ($scope) {
                    $scope.fromDirective = "directive";
                },
                template: "<p><strong>Directive1</strong>: [{{fromDirective}}] - [{{MainValue}}]</p>"
            };
        });

Even though it doesn’t have its own scope, you can still add new properties to the scope, it will make it a bit messy and it certainly becomes dangerous to modify the parent scope without knowing who else may be doing the same and colliding with your directive, but hey, it is technically possible. The directive will then paint the value for both scope elements, the one set in the directive and the one set by the main controller.

Isolated scope

As said, using the parent’s scope may not be a good idea, so we have the option to create our own isolated scope independent from the parent that allows us to avoid collisions:

        app.directive("directiveTwo", function () {
            //scope set
            //result: writes only directive
            return {
                restrict: 'E',
                replace: 'true',
                scope: {},
                controller: function ($scope) {
                    $scope.fromDirective = "directive";
                },
                template: "<p><strong>Directive2</strong>: [{{fromDirective}}] - [{{MainValue}}]</p>"
            };
        });

Just to test, I’ve created an empty scope, there’s no need to add anything to it, just setting an empty one will generate an isolated scope for the directive. Later on I’m adding something to it though just to test our directive accessibility: this directive will paint the value of the property set inside the directive but, on not having access to the parent scope won’t paint the value of “MainValue”.

Getting value through an attribute

We may need to get some value from the parent and also an isolated scope, in that case, a good approach could be to set the value on the directives tag through an attribute:

        app.directive("directiveFour", function () {
            //scope set with attribute using @
            //result: writes directive + attribute
            return {
                restrict: 'E',
                replace: 'true',
                scope: {
                    customAtt: "@"
                },
                controller: function ($scope) {
                    $scope.fromDirective = "directive";
                },
                template: "<p><strong>Directive4</strong>: [{{fromDirective}}] - [{{MainValue}}] - [{{customAtt}}]</p>"
            };
        });

This directive here will not have access to the parent’s scope and so it won’t paint “MainValue”, but will be able to paint it’s own one (fromDirective) and the one set on the attribute, you have the template at the start of the post but just to remind you:

<directive-four custom-att="attribute"></directive-four>

Also, do notice that I am setting the value of that attribute to the scope using a key-character: “@”. Using the “at” you are telling the directive to fetch such value from the attributes and add it to the directive’s scope. So easy!

Getting values through attribute and sharing scope with parent

Ok, for whatever the reason, you may want to still have full access to the parent’s scope, and also give the directive a value to work with (like a selected user’s id or whatever). In this case you won’t be able to use the “@” but you still have access to the directive attributes:

        app.directive("directiveFive", function () {
            //no scope set, custom att retreived on link and set to local scope (overrides main).
            //result: writes directive + main + attribute !!!
            return {
                restrict: 'E',
                replace: 'true',
                controller: function ($scope, $element) {
                    $scope.fromDirective = "directive";
                },
                link: function (scope, element, attrs, controllers) {
                    scope.customAtt = attrs.customAtt;
                },
                template: "<p><strong>Directive5</strong>: [{{fromDirective}}] - [{{MainValue}}] - [{{customAtt}}]</p>"
            };
        });

We have access to the element both in the controller and on the link functions, but the controller one happens before painting the values on the DOM which will make us get an “undefined” if we try to read the attribute’s value. Instead, link happens after the values have been set so we’ll get the value from the parent properly.

This directive starts becoming quite interesting, it does share the scope with the parent and has full access to it so it paints “MainValue”, plus it’s own one plus the one in the attribute. If it weren’t for how messy things can end up with this approach and the fact the directive can’t have “a value of it’s own” this would be quite nice.

WARNING: if your intentions are read-only it may be a good approach to use a shared scope, instead, for writing purposes doing this may result in catastrophic endings, please be aware that this is just an example and don’t copy it without considering the risks.

Overriding a parent’s value

This is just to show what may happen in case we were to follow previous approach unwisely. I added a property to the main scope, ValueToOverrideOn6, and now I’m going to set a new value for it inside the directive:

        app.directive("directiveSix", function () {
            //adding an extra value from main scope to override.
            //result: it does override but also overrided on other instances of the directive 🙁
            return {
                restrict: 'E',
                replace: 'true',
                controller: function ($scope) {
                    $scope.fromDirective = "directive";
                },
                link: function (scope, element, attrs, controllers) {
                    scope.customAtt = attrs.customAtt;
                    scope.ValueToOverrideOn6 = attrs.customAtt2;
                },
                template: "<p><strong>Directive6</strong>: [{{fromDirective}}] - [{{MainValue}}] - [{{customAtt}}] - [{{ValueToOverrideOn6}}]</p>"
            };
        });
        <directive-six custom-att="attribute" custom-att2="overrided"></directive-six>
        <directive-six custom-att="attribute" custom-att2="overrided 2"></directive-six>

Now, if you check the html again you will see that I added two instances of “directive-six”, this was just to show you something, even though the directive gets the value on the attribute, so it allows us to get a custom value, on sharing the scope with the parent we are also sharing it with the rest of the instances of this directive, so our custom value gets overrided by the last instance executing it’s link function. Not the desired result probably.

Inheriting the parent scope

Now, this is quite useful. We can set scope to true which will allow us to get access to the main scope and have our own one at the same time:

        app.directive("directiveSeven", function () {
            //setting scope:true to create own scope but inherit parent
            //result: directive + main + attribute + overrided (without overriding others)
            return {
                restrict: 'E',
                replace: 'true',
                scope: true,
                transclude: true,
                controller: function ($scope) {
                    $scope.fromDirective = "directive";
                },
                link: function (scope, element, attrs, controllers) {
                    scope.customAtt = attrs.customAtt;
                    scope.ValueToOverride = attrs.customAtt2;
                },
                template: "<p><strong>Directive7</strong>: [{{fromDirective}}] - [{{MainValue}}] - [{{customAtt}}] - [{{ValueToOverride}}]</p>"
            };
        });
        <directive-seven custom-att="attribute" custom-att2="overrided"></directive-seven>
        <directive-seven custom-att="attribute" custom-att2="overrided 2"></directive-seven>

I’ve also added two instances of this directive to the HTML, but this time, each of them keeps its own value at the time they are able to access the parent scope and display the “MainValue”. So these directives are able to read the parent and to generate their own isolated scope properties. Note that if you try to override the parent scope, on being an inheritance, you will be creating your own isolated value for that attribute instead, which means that you can read the parent but you can’t write on it, if you try to do that a new property with the same name will be generated on your isolated scope instead and you will lose access to read the parent one.

Refreshing value from parent

There is a low side on getting a parent’s value through an attribute, if the parent scope already has the value when we get to the link function (which in these examples it does as it’s hard-coded) things will work plain and easy, but if the parent doesn’t (maybe becaue you need to make a request to fetch it) or the value changes at some point, the directive won’t refresh it, as it only executes the link function once. To solve that problem we can do this:

        app.directive("directiveNine", function () {
            //making it update the value when parent updates it using $observe
            //works!!
            return {
                restrict: 'E',
                scope: true,
                controller: function($scope, $element) {
                    $scope.fromDirective = "directive";
                },
                link: function (scope, element, attrs, controllers) {
                    attrs.$observe('fromParent', function (data) {
                        scope.customAtt = data;
                    }, true);
                },
                template: "<p><strong>Directive9</strong>: [{{fromDirective}}] - [{{MainValue}}] - [{{customAtt}}]</p>"
            };
        });

The $observe directive is quite similar to the $watch directive you may already know, but this one will pay attention to the DOM instead than the scope. So, by setting an observe we make angular glue that function to that DOM element attribute. Each time it changes our function will execute and the directive scope will be refreshed with it. Problem solved!

Leave a Reply

Your email address will not be published. Required fields are marked *