angularjs provides an $includeContentLoaded event that is fired by every instance of the ngInclude directive whenever it's content is (re)loaded.
In situations where you have multiple nested ngIncludes, the event is of limited use since it is fiddly to detect which include has loaded, and there is no out of the box way to detect when ALL of them have loaded.
Check out this jsFiddle showing how to use the $includeContentLoaded event to detect when specific includes have loaded based on their scopes.
But what if you want to know when ALL the includes beneath a given scope have been loaded?
On a recent project I needed to use $anchorScroll on a long form built out of a lot of nested includes. If I called it too early, the page would get longer and the scroll position wouldn't be correct. Obviously I didn't want to just listen for $includeContentLoaded on the main page scope and call $anchorScroll every time it was fired... Not only would this be wasteful, it might also make the page jump and jitter visibly.
Luckily, it is possible to extend on the built in angularjs directives, including ngInclude.
So, I wrote a simple extension that registers instances of ngInclude up the scope chain, and $emits a custom event at every level when all child $includeContentLoaded events have been fired.
This means I can listen at any level in the application (e.g. a form, a menu wrapper etc.) and know when angularjs has finished laying out all the nested visual elements. Of course this does not guarantee that any asynchronous calls to webservices have been completed, only that the DOM rendering is ready.
Here is a working example of the directive in action with a load of debug info so you can see what's going on: jsFiddle / gist
Here is a cleaner version with minimal code and only the allContentLoaded events being printed: jsFiddle / gist
And if you just want the code for the directive, here it is:
/** * Extends the built-in angularjs IncludeDirective */ myApp.directive('ngInclude', function() { function recursivelyRegister(scopeToRegister, scopeToRegisterWith) { if(!scopeToRegisterWith.hasOwnProperty('includesLoading')) { scopeToRegisterWith.includesLoading = []; } if(scopeToRegisterWith.includesLoading.indexOf(scopeToRegister.$id) === -1) { scopeToRegisterWith.includesLoading.push(scopeToRegister.$id); if (scopeToRegister.hasOwnProperty('name') && scopeToRegisterWith.hasOwnProperty('name')) { scopeToRegister.logToConsole(scopeToRegister.name + ' was linked under ' + scopeToRegisterWith.name); } } if (scopeToRegisterWith.$parent) { recursivelyRegister(scopeToRegister, scopeToRegisterWith.$parent); } } function recursivelyDeRegisterAndNotify(scopeToDeRegister, scopeToDeRegisterFrom) { var i = scopeToDeRegisterFrom.includesLoading.indexOf(scopeToDeRegister.$id); if (i !== -1) { scopeToDeRegisterFrom.includesLoading.splice(i, 1); if (scopeToDeRegisterFrom.includesLoading.length === 0) { if (scopeToDeRegisterFrom.hasOwnProperty('name')) { scopeToDeRegister.logToConsole('all includes were loaded under ' + scopeToDeRegisterFrom.name); } scopeToDeRegisterFrom.$emit('allIncludesLoaded' + scopeToDeRegisterFrom.$id); } } if (scopeToDeRegisterFrom.$parent) { recursivelyDeRegisterAndNotify(scopeToDeRegister, scopeToDeRegisterFrom.$parent); } } function recursivelyReset(scope) { scope.includesLoading.length = 0; if (scope.$parent) { recursivelyReset(scope.$parent); } } return { restrict: 'A', link: function(scope, element, attr, ngModel) { recursivelyRegister(scope, scope.$parent); scope.$on('$routeChangeSuccess', function() { // empty the tracking arrays on route changes recursivelyReset(scope); }); scope.$on('$includeContentLoaded', function(event) { if (scope.hasOwnProperty('name') && scope.$parent.hasOwnProperty('name')) { scope.logToConsole(scope.name + ' was loaded under ' + scope.$parent.name); } recursivelyDeRegisterAndNotify(scope, scope.$parent); }); } }; });












