AngularJS: как привязать постоянный объект к директиве

Я создал директиву с привязкой, используя «область действия». В некоторых случаях я хочу привязать постоянный объект. Например, с HTML:

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

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

Хотя это работает, это также вызывает ошибку JavaScript:

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

(скрипт демонстрирует проблему)

Как правильно связать постоянный объект, не вызывая ошибки?


person Michael Williamson    schedule 05.06.2013    source источник


Ответы (6)


Вот решение, которое я придумал, основываясь на ответе @sh0ber:

Реализуйте пользовательскую функцию link. Если атрибут является действительным JSON, то это постоянное значение, поэтому мы оцениваем его только один раз. В противном случае наблюдайте и обновляйте значение как обычно (другими словами, попытайтесь вести себя как привязка =). scope необходимо установить на true, чтобы убедиться, что назначенное значение влияет только на этот экземпляр директивы.

(пример на 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>'
    };
});
person Michael Williamson    schedule 06.06.2013

Вы получаете эту ошибку, потому что Angular каждый раз оценивает выражение. '=' для имен переменных.

Вот два альтернативных способа добиться того же результата без ошибок.

Первое решение:

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>

Второе решение:

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/

person martinpaulucci    schedule 05.06.2013
comment
Спасибо за ответ. К сожалению, второе решение невозможно, поскольку фактические данные, которые я использую, глубоко вложены друг в друга. Первый случай возможен, но несколько запутан, так как есть много экземпляров директивы, используемой с постоянными значениями (они генерируются на стороне сервера). - person Michael Williamson; 05.06.2013

Другой вариант:

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>'
    };
});
person Dan    schedule 05.06.2013

Это связано с тем, что если вы используете ссылку поля области видимости типа =, значение атрибута отслеживается на предмет изменений, но проверяется на равенство ссылок (с !==), а не глубоко проверяется на равенство. Указание литерала объекта в строке заставит angular создавать новый объект всякий раз, когда к атрибуту обращаются для получения его значения — таким образом, когда angular выполняет грязную проверку, сравнение старого значения с текущим всегда сигнализирует об изменении.

Один из способов преодолеть это - изменить исходный код angular, как описано здесь:

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

В противном случае вы можете создать свой объект в контроллере и ссылаться на него по имени в атрибуте элемента:

HTML

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

JS

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

Еще один способ — создать объект в директиве ng-init родительского элемента, а затем сослаться на него по имени (но этот вариант менее читаем):

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

Мне не особенно нравится использовать eval(), но если вы действительно хотите, чтобы это работало с предоставленным вами HTML:

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);
        }
    };
});
person Mark Rajcok    schedule 05.06.2013

У меня была такая же проблема, я решил ее, разобрав json на этапе компиляции:

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

Единственным недостатком является то, что $scope изначально не заполняется при первом запуске контроллера.

Вот JSFiddle с этим кодом.

person Peter Kovacs    schedule 18.02.2014