71

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.

3
  • 2
    None of the current answers seem to address the question of why (when using $compile) the watch is not automatically set up. As you say, it is bound in initially... Commented Nov 19, 2013 at 11:17
  • I've found another solution, to use template and ng-transclude. This works well - always. The only problem is, that I don't know how to make the layout-template itself configurable. If I use ng-include with a scope function to get the template path, I get a ngTransclude:orphan error. Commented Nov 19, 2013 at 15:45
  • 1
    Okay, I've found a solution to change templateUrl dynamically. See my own answer below. Commented Nov 19, 2013 at 16:25

9 Answers 9

87

You should create a bound scope variable and watch its changes:

return {
   restrict: 'E',
   scope: {
     name: '='
   },
   link: function(scope) {
     scope.$watch('name', function() {
        // all the code here...
     });
   }
};
Sign up to request clarification or add additional context in comments.

1 Comment

Style tip: The correct variable name should be scope and not $scope. Inside the link function the scope is a plain variable.
42

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.

1 Comment

The $watch() function !! always to resuce. !!
17

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.

Comments

8

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>

Comments

2

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.

1 Comment

key is to use = on the scope variable and set thrid parameter of $watch as true. (note @ on the scope variable won't work)
2

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>'
    }
})

Comments

0

We can try this

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

http://jsfiddle.net/Etb9d/

1 Comment

While this code snippet may solve the question, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion.
0

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/

1 Comment

You aren't even using bindToController in your example.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.