14

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?

Dan
  • 59,490
  • 13
  • 101
  • 110
Michael Williamson
  • 11,308
  • 4
  • 37
  • 33

6 Answers6

11

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>'
    };
});
Michael Williamson
  • 11,308
  • 4
  • 37
  • 33
7

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/

cheziHoyzer
  • 4,803
  • 12
  • 54
  • 81
martinpaulucci
  • 2,322
  • 5
  • 24
  • 28
  • 1
    Thanks for the answer. Unfortunately, the second solution isn't possible since the actual data I'm using is deeply nested. The first case is possible but somewhat messy since there are many instances of the directive being used with constant values (they're generated server-side). – Michael Williamson Jun 05 '13 at 19:34
4

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>'
    };
});
Dan
  • 59,490
  • 13
  • 101
  • 110
2

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>
mirrormx
  • 4,049
  • 1
  • 19
  • 17
0

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);
        }
    };
});
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
0

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.

Peter Kovacs
  • 2,657
  • 21
  • 19