AngularJS完成加载时发送事件


114

想知道当所有指令都完成编译/链接时,检测页面加载/引导完成的最佳方法是什么。

已经有活动了吗?我应该重载引导程序功能吗?

Answers:


204

只是预感:为什么不看看ngCloak指令是如何执行的呢?显然,ngCloak伪指令设法在内容加载后显示内容。我敢打赌ngCloak会导致确切的答案...

1小时后编辑: 好的,我看了ngCloak,它真的很短。显然,这意味着编译函数只有在对{{template}}表达式求值(即,它加载的模板)之后才能执行,因此ngCloak指令具有很好的功能。

我的有根据的猜测是只用ngCloak一样简单的方式编写一条指令,然后在您的编译函数中执行您想做的任何事情。:)将指令放置在应用程序的根元素上。您可以像myOnload这样调用该指令,并将其用作my-onload属性。模板编译完成(评估表达式并加载子模板)后,将执行compile函数。

23小时后编辑:好,所以我做了一些研究,还问了自己一个问题。我提出的问题与该问题间接相关,但它恰巧使我找到了解决该问题的答案。

答案是,您可以创建一个简单的指令,然后将代码放入该指令的链接函数中,该链接函数(在大多数情况下,将在下面说明)将在元素准备就绪/加载时运行。根据Josh对执行编译和链接功能的顺序的描述

如果您有此标记:

<div directive1>
  <div directive2>
    <!-- ... -->
  </div>
</div>

然后,AngularJS将通过以一定顺序运行指令函数来创建指令:

directive1: compile
  directive2: compile
directive1: controller
directive1: pre-link
  directive2: controller
  directive2: pre-link
  directive2: post-link
directive1: post-link

默认情况下,直接的“链接”功能是后链接,因此,外部指令1的链接功能将在内部指令2的链接功能运行后才能运行。这就是为什么我们说在后链接中进行DOM操作是唯一安全的原因。因此,对于最初的问题,如上所述,尽管必须编译动态插入的内容,但从外部指令的链接函数访问子指令的内部html应该没有问题。

由此我们可以得出结论,当一切准备就绪/编译/链接/加载时,我们可以简单地创建一条指令来执行代码:

    app.directive('ngElementReady', [function() {
        return {
            priority: -1000, // a low number so this directive loads after all other directives have loaded. 
            restrict: "A", // attribute only
            link: function($scope, $element, $attributes) {
                console.log(" -- Element ready!");
                // do what you want here.
            }
        };
    }]);

现在您可以做的是将ngElementReady指令放到应用程序的根元素上,并且在console.log加载时将触发:

<body data-ng-app="MyApp" data-ng-element-ready="">
   ...
   ...
</body>

就这么简单!只需制定一个简单的指令并使用它即可。;)

您可以进一步对其进行自定义,使其可以通过添加以下表达式来执行表达式(即函数)$scope.$eval($attributes.ngElementReady);

    app.directive('ngElementReady', [function() {
        return {
            priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
            restrict: "A",
            link: function($scope, $element, $attributes) {
                $scope.$eval($attributes.ngElementReady); // execute the expression in the attribute.
            }
        };
    }]);

然后,您可以在任何元素上使用它:

<body data-ng-app="MyApp" data-ng-controller="BodyCtrl" data-ng-element-ready="bodyIsReady()">
    ...
    <div data-ng-element-ready="divIsReady()">...<div>
</body>

只要确保在元素所在的作用域(在控制器中)中定义了函数(例如bodyIsReady和divIsReady)即可。

注意事项:我说过,这在大多数情况下都适用。使用某些指令(例如ngRepeat和ngIf)时要小心。它们创建自己的范围,并且您的指令可能不会触发。例如,如果将新的ngElementReady指令放置在也具有ngIf的元素上,并且ngIf的条件计算为false,则不会加载ngElementReady指令。或者,例如,如果您将新的ngElementReady指令放在也具有ngInclude指令的元素上,则如果ngInclude的模板不存在,则不会加载我们的指令。通过确保嵌套指令而不是将指令全部放在同一元素上,可以解决其中的一些问题。例如,通过执行以下操作:

<div data-ng-element-ready="divIsReady()">
    <div data-ng-include="non-existent-template.html"></div>
<div>

代替这个:

<div data-ng-element-ready="divIsReady()" data-ng-include="non-existent-template.html"></div>

ngElementReady指令将在后一个示例中进行编译,但不会执行其链接功能。注意:伪指令始终是编译的,但它们的链接功能并非总是根据上述某些情况执行。

几分钟后编辑:

哦,要完全回答这个问题,您现在可以从属性中执行的表达式或函数中提取事件$emit$broadcast事件ng-element-ready。:)例如:

<div data-ng-element-ready="$emit('someEvent')">
    ...
<div>

编辑,再过几分钟:

@satchmorun的答案也适用,但仅适用于初始加载。这是一个非常有用的SO问题,它描述了事物执行的顺序,包括链接函数app.run,和其他。因此,取决于您的用例,app.run可能会很好,但对于特定元素而言则不然,在这种情况下,链接功能会更好。

编辑,五个月后,太平洋标准时间10月17日晚上8:11:

这不适用于异步加载的局部。您需要在部分文件中添加簿记(例如,一种方法是让每个部分跟踪其内容何时完成加载,然后发出一个事件,以便父范围可以计算已加载了多少部分文件并最终执行所需的操作加载完所有的部分后执行)。

编辑,太平洋标准时间10月23日晚上10:52:

我做了一个简单的指令,用于在加载图像时触发一些代码:

/*
 * This img directive makes it so that if you put a loaded="" attribute on any
 * img element in your app, the expression of that attribute will be evaluated
 * after the images has finished loading. Use this to, for example, remove
 * loading animations after images have finished loading.
 */
  app.directive('img', function() {
    return {
      restrict: 'E',
      link: function($scope, $element, $attributes) {
        $element.bind('load', function() {
          if ($attributes.loaded) {
            $scope.$eval($attributes.loaded);
          }
        });
      }
    };
  });

编辑,太平洋标准时间10月24日上午12:48:

我改进了原始ngElementReady指令并将其重命名为whenReady

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. done loading all sub directives and DOM
 * content except for things that load asynchronously like partials and images).
 *
 * Execute multiple expressions by delimiting them with a semi-colon. If there
 * is more than one expression, and the last expression evaluates to true, then
 * all expressions prior will be evaluated after all text nodes in the element
 * have been interpolated (i.e. {{placeholders}} replaced with actual values). 
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length == 0) { return; }

      if (expressions.length > 1) {
        if ($scope.$eval(expressions.pop())) {
          waitForInterpolation = true;
        }
      }

      if (waitForInterpolation) {
        requestAnimationFrame(function checkIfInterpolated() {
          if ($element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            requestAnimationFrame(checkIfInterpolated);
          }
          else {
            evalExpressions(expressions);
          }
        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  }
}]);

例如,像这样使用它someFunction在元素加载{{placeholders}}但尚未替换时触发:

<div when-ready="someFunction()">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction在所有item.property占位符被替换之前将被调用。

根据需要评估尽可能多的表达式,并使最后一个表达式true等待{{placeholders}}被评估,如下所示:

<div when-ready="someFunction(); anotherFunction(); true">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction并在更换anotherFunction后将被解雇{{placeholders}}

这仅在第一次加载元素时有效,而在以后的更改中无效。如果$digest在最初替换占位符后继续发生错误,则可能无法按预期运行($ digest最多可能发生10次,直到数据停止更改为止)。它适用于绝大多数用例。

编辑,太平洋标准时间10月31日晚上7:26:

好吧,这可能是我的最后也是最后一次更新。这可能适用于99.999个用例:

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. when it's done loading all sub directives and DOM
 * content). See: /programming/14968690/sending-event-when-angular-js-finished-loading
 *
 * Execute multiple expressions in the when-ready attribute by delimiting them
 * with a semi-colon. when-ready="doThis(); doThat()"
 *
 * Optional: If the value of a wait-for-interpolation attribute on the
 * element evaluates to true, then the expressions in when-ready will be
 * evaluated after all text nodes in the element have been interpolated (i.e.
 * {{placeholders}} have been replaced with actual values).
 *
 * Optional: Use a ready-check attribute to write an expression that
 * specifies what condition is true at any given moment in time when the
 * element is ready. The expression will be evaluated repeatedly until the
 * condition is finally true. The expression is executed with
 * requestAnimationFrame so that it fires at a moment when it is least likely
 * to block rendering of the page.
 *
 * If wait-for-interpolation and ready-check are both supplied, then the
 * when-ready expressions will fire after interpolation is done *and* after
 * the ready-check condition evaluates to true.
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;
      var hasReadyCheckExpression = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length === 0) { return; }

    if ($attributes.waitForInterpolation && $scope.$eval($attributes.waitForInterpolation)) {
        waitForInterpolation = true;
    }

      if ($attributes.readyCheck) {
        hasReadyCheckExpression = true;
      }

      if (waitForInterpolation || hasReadyCheckExpression) {
        requestAnimationFrame(function checkIfReady() {
          var isInterpolated = false;
          var isReadyCheckTrue = false;

          if (waitForInterpolation && $element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            isInterpolated = false;
          }
          else {
            isInterpolated = true;
          }

          if (hasReadyCheckExpression && !$scope.$eval($attributes.readyCheck)) { // if the ready check expression returns false
            isReadyCheckTrue = false;
          }
          else {
            isReadyCheckTrue = true;
          }

          if (isInterpolated && isReadyCheckTrue) { evalExpressions(expressions); }
          else { requestAnimationFrame(checkIfReady); }

        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  };
}]);

这样使用

<div when-ready="isReady()" ready-check="checkIfReady()" wait-for-interpolation="true">
   isReady will fire when this {{placeholder}} has been evaluated
   and when checkIfReady finally returns true. checkIfReady might
   contain code like `$('.some-element').length`.
</div>

当然,它可能可以进行优化,但是我只保留它。requestAnimationFrame很好。


3
真的很讨厌所有这些“数据-”前缀。我很高兴自己不使用它们。
stolsvik 2014年

1
@stolsvik heh,是的,在最现代的浏览器中,不需要它们。
trusktr 2014年

49
应为此投票投入大量时间和精力。干得好!
GordyD 2014年

8
好的答案,但是请考虑删除所有的“编辑”行并稍微调整一下答案。编辑历史记录可通过答案底部的“已编辑...”链接获得,并且在阅读时会分散注意力。
user247702'1

2
源代码将非常有帮助。如果您可以在npm上将其公开,那就太完美了。很好的答案,很好的解释,为此付出了很多努力。
tfrascaroli

38

在的文档中angular.Module,有一个条目描述了该run功能:

使用此方法来记录注入器完成所有模块的加载后应执行的工作。

因此,如果您有一些应用程序模块:

var app = angular.module('app', [/* module dependencies */]);

您可以在模块加载后运行东西:

app.run(function() {
  // Do post-load initialization stuff here
});

编辑:手动初始化以营救

因此,已经指出,run在DOM准备就绪并链接起来时,不会调用。当所$injector引用的模块的ng-app加载了其所有依赖项时(与DOM编译步骤分开),它将被调用。

我又看了一下手动初始化,看来应该可以解决问题。

我做了个小提琴来说明

HTML很简单:

<html>
    <body>
        <test-directive>This is a test</test-directive>
    </body>
</html>

请注意缺少ng-app。而且我有一条指令将执行一些DOM操作,因此我们可以确保事物的顺序和时机。

与往常一样,将创建一个模块:

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

这是指令:

app.directive('testDirective', function() {
    return {
        restrict: 'E',
        template: '<div class="test-directive"><h1><div ng-transclude></div></h1></div>',
        replace: true,
        transclude: true,
        compile: function() {
            console.log("Compiling test-directive");
            return {
                pre: function() { console.log("Prelink"); },
                post: function() { console.log("Postlink"); }
            };
        }
    };
});

我们将test-directivedivclass 的标签替换标记test-directive,并将其内容包装在h1

我添加了一个编译函数,该函数同时返回链接前和链接后的功能,因此我们可以看到这些东西何时运行。

这是其余的代码:

// The bootstrapping process

var body = document.getElementsByTagName('body')[0];

// Check that our directive hasn't been compiled

function howmany(classname) {
    return document.getElementsByClassName(classname).length;
}

在完成任何操作之前,test-directiveDOM中不应该包含具有类的元素,而在完成之后,应该包含1。

console.log('before (should be 0):', howmany('test-directive'));

angular.element(document).ready(function() {
    // Bootstrap the body, which loades the specified modules
    // and compiled the DOM.
    angular.bootstrap(body, ['app']);

    // Our app is loaded and the DOM is compiled
    console.log('after (should be 1):', howmany('test-directive'));
});

这很简单。准备好文档后,angular.bootstrap使用应用程序的根元素和模块名称数组进行调用。

实际上,如果将run功能附加到app模块,您将看到它在任何编译发生之前就已运行。

如果您运行小提琴并观看控制台,则会看到以下内容:

before (should be 0): 0 
Compiling test-directive 
Prelink
Postlink
after (should be 1): 1 <--- success!

2
谢谢@satchmorun!但是run()在链接部分结束之前执行-只需使用一些console.logs进行验证。
Lior

我自己很好奇...我有一条指令可以执行一些jQuery DOM插件,run在该指令执行之前可以触发,而在运行指令执行时,html并不存在
charlietfl 2013年

@charlietfl-我花了一些时间来进行手动引导,这实际上是一种很简单的方法来获取所要查找的问题。我在原始答案中添加了相当长的编辑内容。
satchmorun

5
我发现$timeout( initMyPlugins,0)在指令中使用作品,我需要的所有html都在那里
charlietfl 2013年

@satchmorun,请参阅以下跟进:stackoverflow.com/questions/14989161/…–
Lior

16

Angular没有提供一种在页面加载完成时发出信号的方法,这可能是因为“完成”取决于您的应用程序。例如,如果您具有层次结构的局部树,则其中一个会加载其他树。“完成”将表示它们都已加载。任何框架都将很难分析您的代码并理解一切都已完成或仍在等待。为此,您必须提供特定于应用程序的逻辑以进行检查和确定。


14

我提出了一个解决方案,该解决方案在评估角度初始化完成时相对准确。

该指令是:

.directive('initialisation',['$rootScope',function($rootScope) {
            return {
                restrict: 'A',
                link: function($scope) {
                    var to;
                    var listener = $scope.$watch(function() {
                        clearTimeout(to);
                        to = setTimeout(function () {
                            console.log('initialised');
                            listener();
                            $rootScope.$broadcast('initialised');
                        }, 50);
                    });
                }
            };
        }]);

然后可以将其作为属性添加到body元素,然后使用$scope.$on('initialised', fn)

它通过假设没有更多的$ digest循环初始化应用程序而起作用。$ watch在每个摘要周期被调用,因此将启动一个计时器(setTimeout不是$ timeout,因此不会触发新的摘要周期)。如果在超时时间内未发生摘要周期,则认为该应用程序已初始化。

显然,它不如satchmoruns解决方案那么精确(因为摘要周期可能要花费比超时更长的时间),但是我的解决方案不需要您跟踪模块,这使其更容易管理(特别是对于大型项目) )。无论如何,似乎足以满足我的要求。希望能帮助到你。


优秀的解决方案。对于项目,当一个或两个压缩文件中的所有代码都能很好地工作时。
merqlove 2014年

1
这是一个很棒的解决方案。如果您在jquery中有很多代码,并且试图将代码逐步转换为有角度的代码,那么这是很有意义的。
Mangesh Pimpalkar 2014年

11

如果您正在使用Angular UI Router,则可以侦听$viewContentLoaded事件。

“ $ viewContentLoaded- 呈现DOM后,加载视图触发。视图的'$ scope'发出事件。- 链接

$scope.$on('$viewContentLoaded', 
function(event){ ... });

3
$ scope。$ watch('$ viewContentLoaded',function()对我来说很成功
路易十四

2
为“你应该成为”而投票。如果我说“如果您使用React而不是Angular(应该使用)...”怎么办?在这个生态系统恕我直言不是很态度。
Valentin Waeselynck '16

@ValentinWaeselynck,你是绝对正确的。我编辑了答案以消除偏见。
Jordan Skole

1
为我工作!谢谢。实际上,我将其添加到运行函数中,然后将$ scope更改为$ rootScope。
JDavies '16

2
正如Angular University在一个不同的答案中指出的那样,$ viewContentLoaded最初可能不存在,但现在它以完全相同的方式在内置ngRoute提供程序中工作。考虑到这一点,我认为这是许多(大多数?)未来读者所希望的快速,简单,易读的答案。
凯文·克鲁姆利

3

我用JQuery观察了对Ang的DOM操作,并且确实为我的应用设置了结束(某种我需要我的应用抽象的预定义和令人满意的情况),例如,我希望ng-repeater可以产生7个结果,为此,我们将借助setInterval设置观察功能。

$(document).ready(function(){

  var interval = setInterval(function(){

  if($("article").size() == 7){
     myFunction();
     clearInterval(interval);
  }

  },50);

});

3
我不会这样做。使用时间间隔检查是否发生了某些事情并不是一种好习惯,并且无法扩展,并且还有其他方法可以使事情发生。计时器用于执行在一定时间段后需要执行的具体任务,而不是在准备好内容或结果时“猜测”。
dudewad 2014年

更不用说在angular平台上使用jquery计时器会适得其反-angular具有超时类,您应该使用该类,否则您将跨越两个框架,并且变得非常混乱。
dudewad 2014年

3

如果您不使用ngRoute模块,即您没有$ viewContentLoaded事件。

您可以使用另一种指令方法:

    angular.module('someModule')
        .directive('someDirective', someDirective);

    someDirective.$inject = ['$rootScope', '$timeout']; //Inject services

    function someDirective($rootScope, $timeout){
        return {
            restrict: "A",
            priority: Number.MIN_SAFE_INTEGER, //Lowest priority
            link    : function(scope, element, attr){
                $timeout(
                    function(){
                        $rootScope.$emit("Some:event");
                    }
                );
            }
        };
    }

根据trusktr的回答,它的优先级最低。再加上$ timeout将使Angular在执行回调之前贯穿整个事件循环。

使用$ rootScope,因为它允许将指令放置在应用程序的任何范围内,并仅通知必要的侦听器。

$ rootScope。$ emit将为所有$ rootScope。$仅在侦听器上触发一个事件。有趣的是,$ rootScope。$广播会通知所有$ rootScope。$上以及$范围。$上监听


2

根据Angular团队和这个Github问题

我们现在有分别在ng-view和ng-include中发出的$ viewContentLoaded和$ includeContentLoaded事件。我认为这与我们完成编译的时间差不多。

基于此,目前似乎不可能以可靠的方式进行此操作,否则Angular会立即提供该事件。

引导应用程序意味着在根作用域上运行摘要循环,并且也没有摘要循环完成事件。

根据Angular 2 设计文档

由于有多个摘要,因此无法确定并通知组件该模型是稳定的。这是因为通知可以进一步更改数据,从而可以重新启动绑定过程。

据此,不可能做到这一点的事实是做出决定重写Angular 2的原因之一。


2

我有一个片段,该片段在通过路由进入的主要部分之后/被装载。

在加载该子部分后,我需要运行一个函数,并且我不想编写新的指令,因此发现可以使用厚脸皮 ngIf

父部分的控制器:

$scope.subIsLoaded = function() { /*do stuff*/; return true; };

子部分的HTML

<element ng-if="subIsLoaded()"><!-- more html --></element>

1

如果要使用服务器端数据(JSP,PHP)生成JS,则可以将逻辑添加到服务中,该逻辑将在加载控制器时自动加载。

此外,如果要在所有指令完成编译/链接后做出反应,则可以在初始化逻辑中添加上面建议的适当解决方案。

module.factory('YourControllerInitService', function() {

    // add your initialization logic here

    // return empty service, because it will not be used
    return {};
});


module.controller('YourController', function (YourControllerInitService) {
});

0

这些都是不错的解决方案,但是,如果您当前正在使用“路由”,那么我发现此解决方案是所需的最简单且最少的代码。在触发路由之前,使用'resolve'属性等待诺言完成。例如

$routeProvider
.when("/news", {
    templateUrl: "newsView.html",
    controller: "newsController",
    resolve: {
        message: function(messageService){
            return messageService.getMessage();
    }
}

})

单击此处,查看完整的文档-感谢K. Scott Allen


0

也许我可以通过这个例子为您提供帮助

在自定义花式框中,我显示了带有插值的内容。

在服务中,在“开放” fancybox方法中,我会

open: function(html, $compile) {
        var el = angular.element(html);
     var compiledEl = $compile(el);
        $.fancybox.open(el); 
      }

$ compile返回编译后的数据。你可以检查编译后的数据

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.