AngularJS:AngularJS 渲染模板后如何运行附加代码?

Posted

技术标签:

【中文标题】AngularJS:AngularJS 渲染模板后如何运行附加代码?【英文标题】:AngularJS: How to run additional code after AngularJS has rendered a template? 【发布时间】:2012-08-31 12:31:32 【问题描述】:

我在 DOM 中有一个 Angular 模板。当我的控制器从服务中获取新数据时,它会更新 $scope 中的模型,并重新渲染模板。到目前为止一切顺利。

问题是在模板被重新渲染并且在 DOM 中(在本例中为 jQuery 插件)之后,我还需要做一些额外的工作。

似乎应该有一个事件可以听,例如 AfterRender,但我找不到任何这样的东西。也许指令会是一条路,但它似乎也太早了。

这是一个概述我的问题的 jsFiddle:Fiddle-AngularIssue

== 更新 ==

基于有用的 cmets,我相应地切换到一个指令来处理 DOM 操作,并在指令内实现了一个模型 $watch。但是,我仍然有同样的基本问题; $watch 事件中的代码在模板被编译并插入 DOM 之前触发,因此,jquery 插件总是在评估一个空表。

有趣的是,如果我删除异步调用,整个事情都会正常工作,所以这是朝着正确方向迈出的一步。

这是我更新的 Fiddle 以反映这些变化:http://jsfiddle.net/uNREn/12/

【问题讨论】:

这似乎与我的一个问题相似。 ***.com/questions/11444494/…。也许那里的东西可以提供帮助。 【参考方案1】:

首先,处理渲染的正确位置是指令。我的建议是通过像这样的指令来包装 DOM 操作 jQuery 插件。

我遇到了同样的问题并想出了这个sn-p。它使用$watch$evalAsync 来确保您的代码在ng-repeat 之类的指令被解析并且 value 之类的模板被渲染后运行。

app.directive('name', function() 
    return 
        link: function($scope, element, attrs) 
            // Trigger when number of children changes,
            // including by directives like ng-repeat
            var watch = $scope.$watch(function() 
                return element.children().length;
            , function() 
                // Wait for templates to render
                $scope.$evalAsync(function() 
                    // Finally, directives are evaluated
                    // and templates are renderer here
                    var children = element.children();
                    console.log(children);
                );
            );
        ,
    ;
);

希望这可以帮助您避免一些麻烦。

【讨论】:

这可能是显而易见的,但如果这对您不起作用,请检查链接函数/控制器中的异步操作。我不认为 $evalAsync 可以考虑这些。 值得注意的是,您还可以将link 函数与templateUrl 结合使用。【参考方案2】:

这篇文章很旧,但我将您的代码更改为:

scope.$watch("assignments", function (value) //I change here
  var val = value || null;            
  if (val)
    element.dataTable("bDestroy": true);
  );

见jsfiddle。

希望对你有帮助

【讨论】:

谢谢,这很有帮助。这对我来说是最好的解决方案,因为它不需要控制器进行 dom 操作,并且当从真正的异步服务响应更新数据时仍然可以工作(而且我不必在控制器中使用 $evalAsync)。 这已经被弃用了吗?当我尝试这个时,它实际上只调用 before 渲染 DOM。是依赖shadow dom还是什么? 我对 Angular 比较陌生,我看不出如何用我的具体案例来实现这个答案。我一直在寻找各种答案和猜测,但这不起作用。我不确定“手表”功能应该看什么。我们的 Angular 应用程序有各种不同的 eng- 指令,它们会因页面而异,所以我怎么知道要“观看”什么?当页面完成渲染时,肯定有一种简单的方法可以触发一些代码?【参考方案3】:

按照 Misko 的建议,如果您想要异步操作,那么不要使用 $timeout()(这不起作用)

$timeout(function ()  $scope.assignmentsLoaded(data); , 1000);

使用 $evalAsync() (确实有效)

$scope.$evalAsync(function()  $scope.assignmentsLoaded(data);  );

Fiddle。我还添加了一个“删除数据行”链接,该链接将修改 $scope.assignments,模拟对数据/模型的更改——以显示更改数据有效。

概念概述页面的Runtime 部分解释说,当您需要在当前堆栈框架之外但在浏览器呈现之前发生某些事情时,应该使用 evalAsync。 (在这里猜测......“当前堆栈框架”可能包括 Angular DOM 更新。)如果您需要在浏览器呈现后发生某些事情,请使用 $timeout。

但是,正如您已经发现的那样,我认为这里不需要异步操作。

【讨论】:

$scope.$evalAsync($scope.assignmentsLoaded(data)); 在海事组织中没有任何意义。 $evalAsync() 的参数应该是一个函数。在您的示例中,您在应注册函数以供以后执行时调用该函数。 @hgoebl,$evalAsync 的参数可以是函数或表达式。在这种情况下,我使用了一个表达式。但你是对的,表达式会立即执行。我修改了答案以将其包装在一个函数中以供以后执行。感谢您了解这一点。【参考方案4】:

我发现最简单(便宜又愉快)的解决方案就是在最后一个渲染元素的末尾添加一个带有 ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing()" 的空跨度。当检查是否应显示 span 元素时,将运行此函数。执行此函数中的任何其他代码。

我意识到这不是最优雅的做事方式,但是,它对我有用...

我也遇到过类似的情况,虽然在动画开始时我需要移除加载指示器的情况略有相反,但在移动设备上,angular 的初始化速度比要显示的动画快得多,并且使用 ng-cloak 是不够的,因为在显示任何真实数据之前,加载指示器已被删除。在这种情况下,我只是将 my return 0 函数添加到第一个渲染元素,并在该函数中翻转了隐藏加载指示器的 var。 (当然我在这个函数触发的加载指示器中添加了一个 ng-hide。

【讨论】:

这确实是一个 hack,但它正是我所需要的。强制我的代码在渲染后准确运行(或非常接近准确)。在一个理想的世界里,我不会这样做,但我们在这里...... 我需要在 DOM 渲染后调用内置的 $anchorScroll 服务,但似乎没有什么能解决这个问题。黑客但有效。 ng-init 是更好的选择 ng-init 可能并不总是有效。例如,我有一个 ng-repeat 向 dom 添加许多图像。如果我想在 ng-repeat 中添加每个图像后调用一个函数,我必须使用 ng-show。 哇!好点子。对自己使用 Angular。这是唯一有效的方法,所有涉及异步操作的解决方案根本不起作用,因为视图在更改之前是可见的,这会导致闪烁。 Angular 简直太棒了,但是这些琐碎的事情(例如,简单地直接聚焦一个元素)很难在其中实现并且需要 hack 【参考方案5】:

我认为您正在寻找 $evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync

【讨论】:

感谢您的评论。也许我对 Angular 太陌生,但我没有看到如何将它应用到我的代码中。你有什么例子可以指点我吗?【参考方案6】:

最后我找到了解决方案,我正在使用 REST 服务来更新我的收藏。为了转换数据表jquery是下面的代码:

$scope.$watchCollection( 'conferences', function( old, nuew ) 
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () 
                $( '#dataTablex' ).dataTable();
        );
    );

【讨论】:

【参考方案7】:

我不得不经常这样做。我有一个指令,需要在模型内容完全加载到 DOM 后执行一些 jquery 内容。所以我把我的逻辑放在链接中:指令的函数并将代码包装在 setTimeout(function() ..... , 1); setTimout 将在 DOM 加载后触发,1 毫秒是 DOM 加载后代码执行前的最短时间。这似乎对我有用,但我确实希望在模板加载完成后 angular 引发一个事件,以便该模板使用的指令可以执行 jquery 内容并访问 DOM 元素。希望这会有所帮助。

【讨论】:

【参考方案8】:

您还可以创建一个指令,在链接函数中运行您的代码。

见that *** reply。

【讨论】:

【参考方案9】:

$scope.$evalAsync() 或 $timeout(fn, 0) 对我来说都不可靠。

我不得不将两者结合起来。我制定了一个指令,并且将优先级设置为高于默认值以获得良好的衡量标准。这是它的指令(注意我使用 ngInject 来注入依赖项):

app.directive('postrenderAction', postrenderAction);

/* @ngInject */
function postrenderAction($timeout) 
    // ### Directive Interface
    // Defines base properties for the directive.
    var directive = 
        restrict: 'A',
        priority: 101,
        link: link
    ;
    return directive;

    // ### Link Function
    // Provides functionality for the directive during the DOM building/data binding stage.
    function link(scope, element, attrs) 
        $timeout(function() 
            scope.$evalAsync(attrs.postrenderAction);
        , 0);
    

要调用指令,您可以这样做:

<div postrender-action="functionToRun()"></div>

如果您想在 ng-repeat 运行完毕后调用它,我在 ng-repeat 和 ng-if="$last" 中添加了一个空跨度:

<li ng-repeat="item in list">
    <!-- Do stuff with list -->
    ...

    <!-- Fire function after the last element is rendered -->
    <span ng-if="$last" postrender-action="$ctrl.postRender()"></span>
</li>

【讨论】:

【参考方案10】:

在某些情况下,您更新服务并重定向到新视图(页面),然后在更新服务之前加载您的指令,那么如果您的 $watch 或 $timeout 失败,您可以使用 $rootScope.$broadcast

查看

<service-history log="log" data-ng-repeat="log in requiedData"></service-history>

控制器

app.controller("MyController",['$scope','$rootScope', function($scope, $rootScope) 

   $scope.$on('$viewContentLoaded', function () 
       SomeSerive.getHistory().then(function(data) 
           $scope.requiedData = data;
           $rootScope.$broadcast("history-updation");
       );
  );

]);

指令

app.directive("serviceHistory", function() 
    return 
        restrict: 'E',
        replace: true,
        scope: 
           log: '='
        ,
        link: function($scope, element, attrs) 
            function updateHistory() 
               if(log) 
                   //do something
               
            
            $rootScope.$on("history-updation", updateHistory);
        
   ;
);

【讨论】:

【参考方案11】:

我提供了一个非常简单的解决方案。我不确定这是否是正确的方法,但它在实际意义上有效。让我们直接看我们想要渲染什么。例如,在包含一些 ng-repeats 的指令中,我会注意段落或整个 html 的文本长度(您可能还有其他内容!)。该指令将是这样的:

.directive('myDirective', [function () 
    'use strict';
    return 

        link: function (scope, element, attrs) 
            scope.$watch(function()
               var whole_p_length = 0;
               var ps = element.find('p');
                for (var i=0;i<ps.length;i++)
                    if (ps[i].innerHTML == undefined)
                        continue
                    
                    whole_p_length+= ps[i].innerHTML.length;
                
                //it could be this too:  whole_p_length = element[0].innerHTML.length; but my test showed that the above method is a bit faster
                console.log(whole_p_length);
                return whole_p_length;
            , function (value)    
                //Code you want to be run after rendering changes
            );
        
]);

注意代码实际上是在渲染更改而不是完整渲染之后运行的。但我想在大多数情况下,只要发生渲染更改,您就可以处理这些情况。如果您只想在渲染完成后一次运行代码,您还可以考虑将此ps 长度(或任何其他度量)与您的模型进行比较。 感谢您对此的任何想法/cmets。

【讨论】:

【参考方案12】:

您可以使用angular-ui utils 的“jQuery Passthrough”模块​​。我成功地将一个 jQuery touch carousel 插件绑定到一些我从 Web 服务异步检索并使用 ng-repeat 呈现它们的图像。

【讨论】:

【参考方案13】:

在我的解决方案中,我有一些需要首先加载的自定义指令,因为它们包含其兄弟指令调用的函数的定义。例如:

<div id="container">
    <custom-directive1></custom-directive1>
    <custom-directive2></custom-directive2>
    <custom-directive3></custom-directive3>
</div>

不幸的是,这里没有一个解决方案对我有用,因为它们只在渲染指令后才有效,而不是背后的指令代码。

所以当我实现上述任何解决方案时,为了执行一些加载函数,即使指令被渲染,作用域也不知道这些指令中的函数是什么。

所以我在控制器的任意位置创建了一个 observable:

//Call every time a directive is loaded
$scope.$watch('directiveLoaded', function (value) 
            debugger;
    if (value == document.querySelector('#container').children.length) 
        //Its ok to use childHead as we have only one child scope
        $scope.$$childHead.function1_Of_Directive1();
        $scope.$$childHead.function1_Of_Directive2();
    
);

然后我有这两个指令,我放置的地方

scope.$parent.directiveLoaded += 1;

在每个指令的底部。因为在控制器中我定义了可观察对象,所以每次更新变量directiveLoaded 时,它都会执行可观察函数。是的,我知道这是一个 hack,但要保证所有指令在执行最终函数之前完成渲染以及它们背后的代码,这是一个很小的代价。

为了完成这里的演示,有两个指令定义了以后需要调用的函数。

指令1

(function () 
    app.directive('customDirective1', function () 
        return 
            restrict: 'E',
            templateUrl: '/directive1.html',
            link: function (scope) 

                scope.function1_Of_Directive1 = function() 
                    scope.function2_Of_Directive2();
                    console.log("F1_D1")
                
     
                //AT BOTTOM OF EVERY DIRECTIVE
                scope.$parent.directiveLoaded += 1;
            
        
    );
)();

指令2

(function () 
    app.directive('customDirective2', function () 
        return 
            restrict: 'E',
            templateUrl: '/directive1.html',
            link: function (scope) 
                
                scope.function1_Of_Directive2 = function() 
                    console.log("F1_D2")
                
                scope.function2_Of_Directive2 = function() 
                    console.log("F2_D2")
                

                //AT BOTTOM OF EVERY DIRECTIVE
                scope.$parent.directiveLoaded += 1;
            
        
    );
)();

【讨论】:

以上是关于AngularJS:AngularJS 渲染模板后如何运行附加代码?的主要内容,如果未能解决你的问题,请参考以下文章

以及ajax以及angularjs 动态模板加载并进行渲染

AngularJS 具有相同模板的多个路由不应重新渲染

AngularJS 使用 ng-view 在 ng-repeat 中渲染不同的模板

AngularJS - 指令渲染完成后如何查询 DOM

AngularJS地址栏变化为啥不跳转到页面

AngularJS 与 Django - 模板标签冲突