Question

I've created a directive with a binding using "scope". In some cases, I want to bind a constant object. For instance, with HTML:

<div ng-controller="Ctrl">
    <greeting person="{firstName: 'Bob', lastName: 'Jones'}"></greeting>
</div>

and JavaScript:

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

app.controller("Ctrl", function($scope) {

});

app.directive("greeting", function () {
    return {
        restrict: "E",
        replace: true,
        scope: {
            person: "="
        },
        template:
        '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
    };
});

Although this works, it also causes a JavaScript error:

Error: 10 $digest() iterations reached. Aborting!

(Fiddle demonstrating the problem)

What's the correct way to bind a constant object without causing the error?

Was it helpful?

Solution

Here's the solution I came up with, based on @sh0ber's answer:

Implement a custom link function. If the attribute is valid JSON, then it's a constant value, so we only evaluate it once. Otherwise, watch and update the value as normal (in other words, try to behave as a = binding). scope needs to be set to true to make sure that the assigned value only affects this instance of the directive.

(Example on jsFiddle)

HTML:

<div ng-controller="Ctrl">
    <greeting person='{"firstName": "Bob", "lastName": "Jones"}'></greeting>
    <greeting person="jim"></greeting>
</div>

JavaScript:

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

app.controller("Ctrl", function($scope) {
    $scope.jim = {firstName: 'Jim', lastName: "Bloggs"};
});

app.directive("greeting", function () {
    return {
        restrict: "E",
        replace: true,
        scope: true,
        link: function(scope, elements, attrs) {
            try {
                scope.person = JSON.parse(attrs.person);
            } catch (e) {
                scope.$watch(function() {
                    return scope.$parent.$eval(attrs.person);
                }, function(newValue, oldValue) {
                    scope.person = newValue;
                });
            }   
        },
        template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
    };
});

OTHER TIPS

You are getting that error because Angular is evaluating the expression every time. '=' is for variable names.

Here are two alternative ways to achieve the same think without the error.

First Solution:

app.controller("Ctrl", function($scope) {
    $scope.person = {firstName: 'Bob', lastName: 'Jones'};
});

app.directive("greeting", function () {
    return {
        restrict: "E",
        replace: true,
        scope: {
            person: "="
        },
        template:
        '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
    };
});

<greeting person="person"></greeting>

Second Solution:

app.directive("greeting2", function () {
    return {
        restrict: "E",
        replace: true,
        scope: {
            firstName: "@",
            lastName: "@"
        },
        template:
        '<p>Hello {{firstName}} {{lastName}}</p>'
    };
});

<greeting2 first-name="Bob" last-Name="Jones"></greeting2>

http://jsfiddle.net/7bNAd/82/

Another option:

app.directive("greeting", function () {
    return {
        restrict: "E",
        link: function(scope,element,attrs){
            scope.person = scope.$eval(attrs.person);
        },
        template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
    };
});

This is because if you use the = type of scope field link, the attribute value is being observed for changes, but tested for reference equality (with !==) rather than tested deeply for equality. Specifying object literal in-line will cause angular to create the new object whenever the atribute is accessed for getting its value — thus when angular does dirty-checking, comparing the old value to the current one always signals the change.

One way to overcome that would be to modify angular's source as described here:

https://github.com/mgonto/angular.js/commit/09d19353a2ba0de8edcf625aa7a21464be830f02.

Otherwise, you could create your object in the controller and reference it by name in the element's attribute:

HTML

<div ng-controller="Ctrl">
    <greeting person="personObj"></greeting>
</div>

JS

app.controller("Ctrl", function($scope)
{
    $scope.personObj = { firstName : 'Bob', lastName : 'Jones' };
});

Yet another way is to create the object in the parent element's ng-init directive and later reference it by name (but this one is less readable):

<div ng-controller="Ctrl" ng-init="personObj = { firstName : 'Bob', lastName : 'Jones' }">
    <greeting person="personObj"></greeting>
</div>

I don't particularly like using eval(), but if you really want to get this to work with the HTML you provided:

app.directive("greeting", function() {
    return {
        restrict: "E",
        compile: function(element, attrs) {
            eval("var person = " + attrs.person);
            var htmlText = '<p>Hello ' + person.firstName + ' ' + person.lastName + '</p>';
            element.replaceWith(htmlText);
        }
    };
});

I had the same problem, I solved it by parsing the json in the compile step:

angular.module('foo', []).
directive('myDirective', function () {
    return {
        scope: {
            myData: '@'
        },
        controller: function ($scope, $timeout) {
            $timeout(function () {
                console.log($scope.myData);
            });
        },
        template: "{{myData | json}} a is  {{myData.a}} b is {{myData.b}}",
        compile: function (element, attrs) {
            attrs['myData'] = angular.fromJson(attrs['myData']);
        }
    };
});

The one drawback is that the $scope isn't initially populated when the controller first runs.

Here's a JSFiddle with this code.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top