AngularJS指令在作用域变量更改时不会更新

71
我尝试编写一个小指令,用另一个模板文件包装它的内容。
这段代码:
<layout name="Default">My cool content</layout>

应该有这个输出:

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

因为布局“默认”有这段代码:

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

这里是指令的代码:

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

});

我的问题:

当我在模板中使用作用域变量(在布局模板或布局标记内部),例如 {{whatever}},它一开始就可以工作。但如果我更新了whatever变量,则该指令将不再更新。整个链接函数只会被触发一次。

我认为 AngularJS 不知道这个指令使用了作用域变量,因此它不会被更新。但是我不知道如何修复这种行为。


2
目前的答案似乎都没有解答为什么(使用 $compile 时)观察器不会自动设置的问题。正如你所说,它最初就被绑定了... - Davin Tryon
我找到了另一种解决方案,使用模板和ng-transclude。这个方法总是有效的。唯一的问题是,我不知道如何使布局模板本身可配置。如果我使用ng-include和一个作用域函数来获取模板路径,我会得到一个ngTransclude:orphan错误。 - Armin
1
好的,我已经找到了动态更改templateUrl的解决方案。请看下面我的回答。 - Armin
9个回答

87

您应该创建一个绑定作用域变量并监视其更改:

return {
   restrict: 'E',
   scope: {
     name: '='
   },
   link: function(scope) {
     scope.$watch('name', function() {
        // all the code here...
     });
   }
};

7
样式提示:正确的变量名称应该是 scope 而不是 $scope。在 link 函数内,scope 是一个普通变量。 - adelriosantiago

42

我也需要解决这个问题,我使用这个帖子中的回答得出了以下解决方法:

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

在你的HTML中包含:

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

该指令用于根据从服务器检索到的数据集动态加载报告模板。

它在 scope.data 属性上设置了一个监视器,每当该属性更新时(当用户请求从服务器获取新数据集时),它就会加载相应的指令以显示数据。


$watch() 函数永远是救命稻草!! - JayKandari

17

你需要告诉Angular指令使用了一个作用域变量:

你需要将作用域的某个属性绑定到你的指令上:

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

然后$watch它:

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

请参考Angular指令文档获取更多信息。


8

我找到了一个更好的解决方案:

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

我目前唯一看到的缺点是,转义模板有它们自己的作用域。它们从它们的父级获取值,但是与其在父级中更改该值,该值会存储在一个新的子作用域中。为了避免这种情况,我现在使用$parent.whatever而不是whatever

例子:

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

2

您需要密切关注您的范围。

以下是您可以执行此操作的方法:

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

您的指令应该看起来像这样

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)

同样地,您可以在指令中使用模型,因此如果模型自动更新,则您的监视方法将更新您的指令。

关键是在作用域变量上使用“=”,并将$watch的第三个参数设置为true。(注意,在作用域变量上使用“@”是行不通的) - Shih-Min Lee

2

我知道这是一个老话题,但如果有人像我一样找到了这个问题:

当我需要指令在“父级作用域”更新时更新值时,我使用了以下代码。请务必纠正我,因为我还在学习Angular,但这确实做到了我所需的;

指令:

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

0

我们可以试试这个

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

http://jsfiddle.net/Etb9d/


2
虽然这段代码片段可能解决了问题,但包括解释真的有助于提高您的帖子质量。请记住,您正在为未来的读者回答问题,而这些人可能不知道您的代码建议的原因。 - Rosário Pereira Fernandes

0
一个简单的解决方案是将作用域变量设置为对象。然后使用{{ whatever-object.whatever-property }}访问内容。变量不会更新,因为JavaScript通过传递原始类型。而对象是通过引用传递的,这解决了问题。

0

我不确定为什么还没有人建议使用bindToController,它可以消除所有这些丑陋的scopes和$watches。如果您正在使用Angular 1.4

以下是一个示例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>

以下是相关的 controller 代码:

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);

这是一个用来试验的Fiddle,我们在controller中更改范围值,自动更新directive随着范围的更改而更新。 http://jsfiddle.net/spechackers/1ywL3fnq/


1
你的示例中甚至没有使用bindToController。 - Suamere

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接