Sending event when AngularJS finished loading

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.

Leave a Comment