Wait for content to be updated and rendered in AngularJS 1.x
Listening for the $viewContentLoaded or $onChanges events don't actually solve the problem. So here's a solution that does using our own custom directive.
I’ve run into this problem before on another project, and the other day I ran into it again - waiting for data on scope to be updated and rendered on the page before doing something.
Scenario 1 - MVVC Architecture
The application was being developed using an off-the-shelf theme implementing UI Bootstrap and Jasny Bootstrap, among others.
Ultimately, the issue was that the width of the navbar-header
and the profile-dropdown
elements were being calculated using JavaScript in an AngularJS directive after the DOM was loaded and whenever there was a page resize in the link
function, something like this.
// directive
link: function() {
angular.element(document).ready(function() {
$timeout(function() {
resetWidth();
}, TIMEOUT_GLOBAL_RENDERING);
});
angular.element($window).bind('resize', function() {
resetWidth();
});
}
A hack if ever I’d seen one. In fact, it simply did not work.
See angular.element(document).ready()
will execute as soon as the DOM is ready, setting the appropriate widths for the elements before the promises which fetched the data for them had been resolved. When said promises did resolve and populate the elements with content, that caused them to expand and overflow the width of the page.
You might say this was an easy problem to solve, but with the complexity of so many different libraries intertwined and being maintained outside of our application, we didn’t have the option of rewriting the existing functionality. We had to extend it.
The initial thought was to use $viewContentLoaded
.
The issue with this approach is that $viewContentLoaded
applies to the HTML, not to the content in its bindings. So, this would fire and redraw the page before the promises resolved and before the data in the bindings changes in the view.
Scenario 2 - Component-based Architecture
This application is being developed using an in-house developed theme using AngularStrap.
The pattern for page loads and refreshes when I joined the project was already well established, and used throughout the application.
// controller
vm.loading = true;
promise()
.then(
function(success) {
// Do something on success
},
function(error) {
// Do something on failure
}
)
.finally(
function() {
vm.loading = false;
}
);
It worked for the application at that point, but as my team and I began further development, this implementation did not scale well.
While the data had been retrieved, vm.loading
was being set to false
a long time before the data had chance to work its way down through the component tree and be propagated to the view.
This caused the loading spinner in the view to disappear while the old data was still visible, with a delay before the fresh data replaced it.
We investigated a number of solutions, including the obvious component lifecycle hooks $postLink
and $onChanges
.
Again, we had the same issue that both $postLink
and $onChanges
are fired before the data in the bindings actually changes in the view.
The Solution - A Custom Directive
The problem is obvious - we need to wait until the data in the view has been updated before we apply the successive logic.
The solution was equally obvious - essentially create our own ‘hook’ which could be applied to elements as a directive that would be fired after the data in the view has been updated.
- Step 1 - Add our process to the end of the message queue.
- Step 2 - Wait for the element containing the data to be ‘ready’.
- Step 3 - Apply any scope (data) changes to the element.
- Step 4 - Broadcast that this process has completed.
- Step 5 - Listen for this event and act upon it.
The result of this is the following directive and implementation.
// directive
(angular => {
'use strict';
const elementReady = ($timeout, $rootScope) => {
return {
restrict: 'A',
link(scope, element, attrs) {
$timeout(() => {
element.ready(() => {
scope.$apply(() => {
$rootScope.$broadcast(`${attrs.elementReady}:ready`);
});
});
});
}
};
};
elementReady.$inject = ['$timeout', '$rootScope'];
angular
.module('ElementReady', [])
.directive('elementReady', elementReady);
})(angular);
<!-- template -->
<div element-ready="search-results">
{{ vm.data }}
</div>
// controller
$scope.$on('search-results:ready', () => {
// Take action after the view has been populated with the updated data
});
If you’ve faced this problem and solved it in a different way I’d love to hear about it.
Hopefully this helps somebody out there. As always, the code is available to download on Github.
- Solving the essential JavaScript interview question
- Presenting the essential JavaScript interview question
- Can you give a realistic task estimate?
- Creating a maintainable gulpfile.js
- JavaScript is a compiled language?
- Integrating ESLint settings with WebStorm
- What are object prototypes? [Part 1]
- Why participate in #100DaysOfCode?
- Finding a decent static site generator
- Now