[javascript] AngularJS directive does not update on scope variable changes

I've tried to write a small directive, to wrap its contents with another template file.

This code:

<layout name="Default">My cool content</layout>

Should have this output:

<div class="layoutDefault">My cool content</div>

Because the layout "Default" has this code:

<div class="layoutDefault">{{content}}</div>

Here the code of the directive:

app.directive('layout', function($http, $compile){
return {
    restrict: 'E',
    link: function(scope, element, attributes) {
        var layoutName = (angular.isDefined(attributes.name)) ? attributes.name : 'Default';
        $http.get(scope.constants.pathLayouts + layoutName + '.html')
            .success(function(layout){
                var regexp = /^([\s\S]*?){{content}}([\s\S]*)$/g;
                var result = regexp.exec(layout);

                var templateWithLayout = result[1] + element.html() + result[2];
                element.html($compile(templateWithLayout)(scope));
            });
    }
}

});

My problem:

When I'm using scope variables in template (in layout template or inside of layout tag), eg. {{whatever}} it just work initially. If I update the whatever variable, the directive is not updated anymore. The whole link function will just get triggered once.

I think, that AngularJS does not know, that this directive uses scope variables and therefore it will not be updated. But I have no clue how to fix this behavior.

The answer is


We can try this

$scope.$apply(function() {
    $scope.step1 = true;
    //scope.list2.length = 0;
});

http://jsfiddle.net/Etb9d/


You should keep a watch on your scope.

Here is how you can do it:

<layout layoutId="myScope"></layout>

Your directive should look like

app.directive('layout', function($http, $compile){
    return {
        restrict: 'E',
        scope: {
            layoutId: "=layoutId"
        },
        link: function(scope, element, attributes) {
            var layoutName = (angular.isDefined(attributes.name)) ? attributes.name : 'Default';
            $http.get(scope.constants.pathLayouts + layoutName + '.html')
                .success(function(layout){
                    var regexp = /^([\s\S]*?){{content}}([\s\S]*)$/g;
                    var result = regexp.exec(layout);

                    var templateWithLayout = result[1] + element.html() + result[2];
                    element.html($compile(templateWithLayout)(scope));
        });
    }
}

$scope.$watch('myScope',function(){
        //Do Whatever you want
    },true)

Similarly you can models in your directive, so if model updates automatically your watch method will update your directive.


A simple solution is to make the scope variable object. Then access the content with {{ whatever-object.whatever-property }}. The variable is not updating because JavaScript pass Primitive type by value. Whereas Object are passed by reference which solves the problem.


I know this an old subject but in case any finds this like myself:

I used the following code when i needed my directive to update values when the "parent scope" updated. Please by all means correct me if am doing something wrong as i am still learning angular, but this did what i needed;

directive:

directive('dateRangePrint', function(){
    return {
        restrict: 'E',
        scope:{
        //still using the single dir binding
            From: '@rangeFrom',
            To: '@rangeTo',
            format: '@format'
        },
        controller: function($scope, $element){

            $scope.viewFrom = function(){
                    return formatDate($scope.From, $scope.format);
                }

            $scope.viewTo = function(){
                    return formatDate($scope.To, $scope.format);
                }

            function formatDate(date, format){
                format = format || 'DD-MM-YYYY';

                //do stuff to date...

                return date.format(format);
            }

        },
        replace: true,
        // note the parenthesis after scope var
        template: '<span>{{ viewFrom() }} - {{ viewTo() }}</span>'
    }
})

You need to tell Angular that your directive uses a scope variable:

You need to bind some property of the scope to your directive:

return {
    restrict: 'E',
    scope: {
      whatever: '='
    },
   ...
}

and then $watch it:

  $scope.$watch('whatever', function(value) {
    // do something with the new value
  });

Refer to the Angular documentation on directives for more information.


I've found a much better solution:

app.directive('layout', function(){
    var settings = {
        restrict: 'E',
        transclude: true,
        templateUrl: function(element, attributes){
            var layoutName = (angular.isDefined(attributes.name)) ? attributes.name : 'Default';
            return constants.pathLayouts + layoutName + '.html';
        }
    }
    return settings;
});

The only disadvantage I see currently, is the fact that transcluded templates got their own scope. They get the values from their parents, but instead of change the value in the parent, the value get stored in an own, new child-scope. To avoid this, I am now using $parent.whatever instead of whatever.

Example:

<layout name="Default">
    <layout name="AnotherNestedLayout">
        <label>Whatever:</label>
        <input type="text" ng-model="$parent.whatever">
    </layout>
</layout>

I needed a solution for this issue as well and I used the answers in this thread to come up with the following:

.directive('tpReport', ['$parse', '$http', '$compile', '$templateCache', function($parse, $http, $compile, $templateCache)
    {
        var getTemplateUrl = function(type)
        {
            var templateUrl = '';

            switch (type)
            {
                case 1: // Table
                    templateUrl = 'modules/tpReport/directives/table-report.tpl.html';
                    break;
                case 0:
                    templateUrl = 'modules/tpReport/directives/default.tpl.html';
                    break;
                default:
                    templateUrl = '';
                    console.log("Type not defined for tpReport");
                    break;
            }

            return templateUrl;
        };

        var linker = function (scope, element, attrs)
        {

            scope.$watch('data', function(){
                var templateUrl = getTemplateUrl(scope.data[0].typeID);
                var data = $templateCache.get(templateUrl);
                element.html(data);
                $compile(element.contents())(scope);

            });



        };

        return {
            controller: 'tpReportCtrl',
            template: '<div>{{data}}</div>',
            // Remove all existing content of the directive.
            transclude: true,
            restrict: "E",
            scope: {
                data: '='
            },
            link: linker
        };
    }])
    ;

Include in your html:

<tp-report data='data'></tp-report>

This directive is used for dynamically loading report templates based on the dataset retrieved from the server.

It sets a watch on the scope.data property and whenever this gets updated (when the users requests a new dataset from the server) it loads the corresponding directive to show the data.


I am not sure why no one has yet suggested bindToController which removes all these ugly scopes and $watches. If You are using Angular 1.4

Below is a sample DOM:

<div ng-app="app">
    <div ng-controller="MainCtrl as vm">
        {{ vm.name }}
        <foo-directive name="vm.name"></foo-directive>
        <button ng-click="vm.changeScopeValue()">
        changeScopeValue
        </button>
    </div>
</div>

Follows the controller code:

angular.module('app', []);

// main.js
function MainCtrl() {
    this.name = 'Vinoth Initial';
    this.changeScopeValue = function(){
        this.name = "Vinoth has Changed"
    }
}

angular
    .module('app')
    .controller('MainCtrl', MainCtrl);

// foo.js
function FooDirCtrl() {
}

function fooDirective() {
    return {
        restrict: 'E',
        scope: {
            name: '='
        },
        controller: 'FooDirCtrl',
        controllerAs: 'vm',
        template:'<div><input ng-model="name"></div>',
        bindToController: true
    };
}

angular
    .module('app')
    .directive('fooDirective', fooDirective)
    .controller('FooDirCtrl', FooDirCtrl);

A Fiddle to play around, here we are changing the scope value in the controller and automatically the directive updates on scope change. http://jsfiddle.net/spechackers/1ywL3fnq/


Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to angularjs

AngularJs directive not updating another directive's scope ERROR in Cannot find module 'node-sass' CORS: credentials mode is 'include' CORS error :Request header field Authorization is not allowed by Access-Control-Allow-Headers in preflight response WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 400 Print Html template in Angular 2 (ng-print in Angular 2) $http.get(...).success is not a function Angular 1.6.0: "Possibly unhandled rejection" error Find object by its property in array of objects with AngularJS way Error: Cannot invoke an expression whose type lacks a call signature

Examples related to angularjs-directive

Angular2 - Input Field To Accept Only Numbers Use of symbols '@', '&', '=' and '>' in custom directive's scope binding: AngularJS ng-change not working on a text input Controller not a function, got undefined, while defining controllers globally Find child element in AngularJS directive Angular directives - when and how to use compile, controller, pre-link and post-link How to validate email id in angularJs using ng-pattern Enable/Disable Anchor Tags using AngularJS get original element from ng-click How to detect browser using angularjs?

Examples related to angularjs-scope

Use of symbols '@', '&', '=' and '>' in custom directive's scope binding: AngularJS How to call a function from another controller in angularjs? Detect if checkbox is checked or unchecked in Angular.js ng-change event $rootScope.$broadcast vs. $scope.$emit How to clear or stop timeInterval in angularjs? How do I inject a controller into another controller in AngularJS Controller not a function, got undefined, while defining controllers globally AngularJS - get element attributes values ng-if, not equal to? How to set the id attribute of a HTML element dynamically with angularjs (1.x)?