Sending event when AngularJS finished loading

2019-01-02 17:06发布

Wondered what's the best way to detect the finish of page loading/bootstrapping, when all directives done compiling/linking.

Any event already there? Should I overload the bootstrap function?

12条回答
低头抚发
2楼-- · 2019-01-02 17:15

In the docs for angular.Module, there's an entry describing the run function:

Use this method to register work which should be performed when the injector is done loading all modules.

So if you have some module that is your app:

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

You can run stuff after the modules have loaded with:

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

EDIT: Manual Initialization to the rescue

So it's been pointed out that the run doesn't get called when the DOM is ready and linked up. It gets called when the $injector for the module referenced by ng-app has loaded all its dependencies, which is separate from the DOM compilation step.

I took another look at manual initialization, and it seems that this should do the trick.

I've made a fiddle to illustrate.

The HTML is simple:

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

Note the lack of an ng-app. And I have a directive that will do some DOM manipulation, so we can make sure of the order and timing of things.

As usual, a module is created:

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

And here's the directive:

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

We're going to replace the test-directive tag with a div of class test-directive, and wrap its contents in an h1.

I've added a compile function that returns both pre and post link functions so we can see when these things run.

Here's the rest of the code:

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

Before we've done anything, there should be no elements with a class of test-directive in the DOM, and after we're done there should be 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'));
});

It's pretty straightforward. When the document is ready, call angular.bootstrap with the root element of your app and an array of module names.

In fact, if you attach a run function to the app module, you'll see it gets run before any of the compiling takes place.

If you run the fiddle and watch the console, you'll see the following:

before (should be 0): 0 
Compiling test-directive 
Prelink
Postlink
after (should be 1): 1 <--- success!
查看更多
千与千寻千般痛.
3楼-- · 2019-01-02 17:17

These are all great solutions, However, if you are currently using Routing then I found this solution to be the easiest and least amount of code needed. Using the 'resolve' property to wait for a promise to complete before triggering the route. e.g.

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

})

Click here for the full docs - Credit to K. Scott Allen

查看更多
ら面具成の殇う
4楼-- · 2019-01-02 17:19

I have come up with a solution that is relatively accurate at evaluating when the angular initialisation is complete.

The directive is:

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

That can then just be added as an attribute to the body element and then listened for using $scope.$on('initialised', fn)

It works by assuming that the application is initialised when there are no more $digest cycles. $watch is called every digest cycle and so a timer is started (setTimeout not $timeout so a new digest cycle is not triggered). If a digest cycle does not occur within the timeout then the application is assumed to have initialised.

It is obviously not as accurate as satchmoruns solution (as it is possible a digest cycle takes longer than the timeout) but my solution doesn't need you to keep track of the modules which makes it that much easier to manage (particularly for larger projects). Anyway, seems to be accurate enough for my requirements. Hope it helps.

查看更多
像晚风撩人
5楼-- · 2019-01-02 17:21

i observe DOM manipulation of angular with JQuery and i did set a finish for my app (some sort of predefined and satisfactory situation that i need for my app-abstract) for example i expect my ng-repeater to produce 7 result and there for i will set an observation function with the help of setInterval for this purpose .

$(document).ready(function(){

  var interval = setInterval(function(){

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

  },50);

});
查看更多
临风纵饮
6楼-- · 2019-01-02 17:22

Just a hunch: why not look at how the ngCloak directive does it? Clearly the ngCloak directive manages to show content after things have loaded. I bet looking at ngCloak will lead to the exact answer...

EDIT 1 hour later: Ok, well, I looked at ngCloak and it's really short. What this obviously implies is that the compile function won't get executed until {{template}} expressions have been evaluated (i.e. the template it loaded), thus the nice functionality of the ngCloak directive.

My educated guess would be to just make a directive with the same simplicity of ngCloak, then in your compile function do whatever you want to do. :) Place the directive on the root element of your app. You can call the directive something like myOnload and use it as an attribute my-onload. The compile function will execute once the template has been compiled (expressions evaluated and sub-templates loaded).

EDIT, 23 hours later: Ok, so I did some research, and I also asked my own question. The question I asked was indirectly related to this question, but it coincidentally lead me to the answer that solves this question.

The answer is that you can create a simple directive and put your code in the directive's link function, which (for most use cases, explained below) will run when your element is ready/loaded. Based on Josh's description of the order in which compile and link functions are executed,

if you have this markup:

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

Then AngularJS will create the directives by running directive functions in a certain order:

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

By default a straight "link" function is a post-link, so your outer directive1's link function will not run until after the inner directive2's link function has ran. That's why we say that it's only safe to do DOM manipulation in the post-link. So toward the original question, there should be no issue accessing the child directive's inner html from the outer directive's link function, though dynamically inserted contents must be compiled, as said above.

From this we can conclude that we can simply make a directive to execute our code when everything is ready/compiled/linked/loaded:

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

Now what you can do is put the ngElementReady directive onto the root element of the app, and the console.log will fire when it's loaded:

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

It's that simple! Just make a simple directive and use it. ;)

You can further customize it so it can execute an expression (i.e. a function) by adding $scope.$eval($attributes.ngElementReady); to it:

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

Then you can use it on any element:

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

Just make sure you have your functions (e.g. bodyIsReady and divIsReady) defined in the scope (in the controller) that your element lives under.

Caveats: I said this will work for most cases. Be careful when using certain directives like ngRepeat and ngIf. They create their own scope, and your directive may not fire. For example if you put our new ngElementReady directive on an element that also has ngIf, and the condition of the ngIf evaluates to false, then our ngElementReady directive won't get loaded. Or, for example, if you put our new ngElementReady directive on an element that also has a ngInclude directive, our directive won't be loaded if the template for the ngInclude does not exist. You can get around some of these problems by making sure you nest the directives instead of putting them all on the same element. For example, by doing this:

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

instead of this:

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

The ngElementReady directive will be compiled in the latter example, but it's link function will not be executed. Note: directives are always compiled, but their link functions are not always executed depending on certain scenarios like the above.

EDIT, a few minutes later:

Oh, and to fully answer the question, you can now $emit or $broadcast your event from the expression or function that is executed in the ng-element-ready attribute. :) E.g.:

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

EDIT, even more few minutes later:

@satchmorun's answer works too, but only for the initial load. Here's a very useful SO question that describes the order things are executed including link functions, app.run, and others. So, depending on your use case, app.run might be good, but not for specific elements, in which case link functions are better.

EDIT, five months later, Oct 17 at 8:11 PST:

This doesn't work with partials that are loaded asynchronously. You'll need to add bookkeeping into your partials (e.g. one way is to make each partial keep track of when its content is done loading then emit an event so the parent scope can count how many partials have loaded and finally do what it needs to do after all partials are loaded).

EDIT, Oct 23 at 10:52pm PST:

I made a simple directive for firing some code when an image is loaded:

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

EDIT, Oct 24 at 12:48am PST:

I improved my original ngElementReady directive and renamed it to 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);
      }
    }
  }
}]);

For example, use it like this to fire someFunction when an element is loaded and {{placeholders}} not yet replaced:

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

someFunction will be called before all the item.property placeholders are replaced.

Evaluate as many expressions as you want, and make the last expression true to wait for {{placeholders}} to be evaluated like this:

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

someFunction and anotherFunction will be fired after {{placeholders}} have been replaced.

This only works the first time an element is loaded, not on future changes. It may not work as desired if a $digest keeps happening after placeholders have initially been replaced (a $digest can happen up to 10 times until data stops changing). It'll be suitable for a vast majority of use cases.

EDIT, Oct 31 at 7:26pm PST:

Alright, this is probably my last and final update. This will probably work for 99.999 of the use cases out there:

/*
 * 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: https://stackoverflow.com/questions/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);
      }
    }
  };
}]);

Use it like this

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

Of course, it can probably be optimized, but I'll just leave it at that. requestAnimationFrame is nice.

查看更多
墨雨无痕
7楼-- · 2019-01-02 17:23

Angular hasn't provided a way to signal when a page finished loading, maybe because "finished" depends on your application. For example, if you have hierarchical tree of partials, one loading the others. "Finish" would mean that all of them have been loaded. Any framework would have a hard time analyzing your code and understanding that everything is done, or still waited upon. For that, you would have to provide application-specific logic to check and determine that.

查看更多
登录 后发表回答