-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Feature request: a way to detect if Angular finished rendering of HTML DOM #734
Comments
Why isn't creating directives sufficient to bind these events? The thing is that the compilation is a continuous process in angular, if you don't use directives, you are bypassing the compiler and then you run the issue of not knowing when things are ready. If I'm missing something (which I may be!), can you create a simple jsfiddle demonstrating your use case? |
I think you are looking for this: 2012/1/25 Konstantin Stepanov <
|
$evalAsync looks interesting, but not exactly what we want. According to docs it evaluates expression before any DOM rendering is done and after a digest cycle is finished. What we want is to run some code after both digest cycle is finished and DOM rendering is done. Either I or my colleague will provide you an example tomorrow, I'm too tired for today and my colleague is not available at the moment either. |
Quick example: we load some partial with For now we use $browser.defer() here and there, but it's a very bad thing, we'd prefer more robust and correct way to do it. |
The asyncEval is after the DOM construction but before the browser renders. 2012/1/25 Konstantin Stepanov <
|
Hmm... I looked through code and what I found out, $evalAsync just queues code for execution, and then it's executed in $digest, but there in $digest first execution queue (filled with $evalAsync) is run, and then watchers are evaluated. And all widgets/directives use $watch to update DOM nodes. So if I get it correctly, my $evalAsync()ed code run before DOM manipulations, and thus I have no guarantee my DOM tree won't be modified just after my $evalAsync()ed function executed. Am I missing something? I feel like I need something like additional execution queue to run after the main $digest loop is done it's work. Of cause I will have to guarantee my code in the queue won't change DOM lest $digest must be run again. |
if you enqueue from controller then it will be before, but if you enqueue The issue is that most frameworks have a string template which then gets With angular that is not the case. Since angular has concept of directives, So in order for us to help you, what is the specific problem which you are -- Misko 2012/1/26 Konstantin Stepanov <
|
we now have $viewContentLoaded and $includeContentLoaded events that are emitted in ng-view and ng-include respectively. I think this is as close as one can get to knowing when we are done with the compilation. |
I think I have the same problem. When using a directive that contains another directive (ng-repeat) the higher level doesn't seem to wait until the entire DOM is created, and the async eval is evaluated before all of the DOM is present. Could you suggest a good time to bind jQuery events? In the example I'm using jQuery UI draggable. |
I'm also having problems to detect when the ngView has ended rendering. The task need to get the ngView "container"s height but when the event $viewContentLoaded triggers, height is still 0. What should I do? I'll appreciate a little help. |
Have you tried using $timeout() to schedule your code execution in $viewContentLoaded event handler? Or evalAsync()? |
@eprouver try this out http://jsfiddle.net/WpZZE/5/ |
Thanks for responding: With local data $timeout seemed to work. I believe I started running into this problem when loading outside content. Here is a simple example that fails similarly: http://jsfiddle.net/eprouver/WpZZE/6/ |
@eprouver |
Ah, yes, perfect... this is exactly the mindset that I was missing. Thanks! |
Hi, I need similar functionality in my code. directive template's expressions are not evaluated by the time listener in watch gets called. Here is the code, Didn't get any reply on google group and stack overflow? Can anyone help me with this? Thanks, |
Hi, So for @eprouver fiddle, i attach a listener to the
Here is my fiddle http://jsfiddle.net/KJBQg/ In my personal opinion i think my code is bit cleaner Cheers, |
Your code is nice and works fine, but it doesn't involve AngularJS (except for controller), so it doesn't know about a thing about AngularJS DOM rebuilding and scopes life-cycle. It works by detecting side-effect event of AngularJS DOM rendering (you detect resize event of ul container) instead of direct information from AngularJS. That is, the problem is you code can stop working if AngularJS rendering produces DOM with sizes identical to previous iteration (e.g. the same number of pictures are loaded, so DOM elements can change w/o changing container's size → your code won't put draggable thing on these new elements). Just try to click "Add" button in your fiddle second time and you will see what I mean. Besides your code lacks access to AngularJS's scope, rendering it unable to take/pass data from/to controllers (which is not used in your fiddle anyway, but is a drawback of your approach nevertheless). |
This is a side-effect of the all async nature of the AngularJS setup. There are other side-effects as well. For me total async behaviour (all elements on page doing their own thing) is not the best solution - there has to be some balance. Anyway. We solved this by placing a 0-size transparent gif in the template and putting a onload event that runs when the image is rendered in DOM. Made a directive out of this. I could make a JSFiddle if anybody is interested. This solution also has it's drawbacks due to the way AngularJS operates. For example we put the image in the template that had a ng-repeat loop and we wanted to access the looped elements after they are in DOM. It only started working when we put the image actually inside the loop element(s) because DOM is the Angular templating system and the other parts ended up in DOM before the looped elements did. So. this method works but with quirks that you have to account for. |
Seems that in most cases here people want to trigger jQuery plugins - usually angular-ui with the jQuery passthrough directive does the job. Using it successfully for Lazyload with Ajax-loaded images. Note: I did have a small issue with this but not because of Angular - but rather because Lazyload is not designed for dynamically added images. |
Here's a use case: I have a directive that draws a canvas on top of div elements. The canvas needs to know the div positions in order to draw the right diagram. This means that it has to update when the div finishes rendering (the div's size depends on text that the directive also sets) so that it knows its top, left, height, and width properties. I've used the timeout hack to get it to render after but its still not reliable. |
@wuxiaoying - I've used $attrs.$observe for a similar case and put the size-defining text in a data-text attribute. It worked for me... here is an example: |
@eprouver hmm that's an interesting solution; thanks for sharing! However, isn't that also a hack? it's basically setting an attribute to an interpolated value, observing the interpolation, and assuming that since that value was interpolated, the value inside the div is also interpolated. |
@wuxiaoying - Yeah... it's a hack. I think, generally, it's a good idea to make a specific directive for your divs that generate canvas charts. Similar to creating directives for jQuery ui widgets. That $attrs hack just proved more reliable for me than setTimeout. |
My directive is specific :) it basically draws diagrams around divs, so the canvas drawing itself is related to div positioning. I would use the $attr hack, but I'm not sure it work in my case since the canvas positioning doesn't only depend on text, but also child divs and and other elements. I guess the timeout hack will work for now; I realized that the few unreliable cases were when the page went invisible (due to a tab switch) before the timeouts occurred and swwtching back to that tab resulted in a very messed up looking canvas :). |
There's no event for "finished rendering HTML DOM" because things may be mutating the DOM at any time. IMHO the best way to handle this is to come up with a heuristic for whether the DOM has changed in a way that's interesting to you, set up a watch on this, then trigger whatever behavior you need. |
Is there some kind of feature that listens to the whole page dynamically rendered and the whole scopes are digested ? |
I got what seems like a working solution from here: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering As I understand it, it adds the post render function to the timeout queue which places it in a list of things to execute after the rendering engine has completed (which should already be in the queue busy executing away). (Pretty new to angular so trying to wrap my head around things still so I may be way off base...) |
@subdigit thank you, I tried $timeout, it works on me, |
What I think is being request here, is what angular uses internally as scope.$$postDigest(callback). |
I want to render directive on drag of a control but it is not working it just adding the tag but not replacing html.Is there any solution for this? |
$timeout + 1 |
I need a similar feature as well. My goal is to be able to control the order of resources that get downloaded on page load. Since angularjs handles the directive rendering internally, there's no way to specify which directive gets higher priority in a controller. Let me explain with an example: index.html
style.css
If you want It'd be great to have a way of prioritizing which directives should be rendered first. |
This is a really common need and pretty basic and intuitive. I don't get why the Angular team won't just emit an event. Anyways... here is what I did: define(['angular'], function (angular) {
'use strict';
return angular.module('app.common.after-render', [])
.directive('afterRender', [ '$timeout', function($timeout) {
var def = {
restrict : 'A',
terminal : true,
transclude : false,
link : function(scope, element, attrs) {
if (attrs) { scope.$eval(attrs.afterRender) }
scope.$emit('onAfterRender')
}
};
return def;
}]);
}); then you can do: <div after-render></div> or with any useful expression like: <div after-render="$emit='onAfterThisThingRendered'"></div> |
$viewContentFinished or $viewContentRendered event required. |
I think this is needed as well. I'm currently trying to measure the actual page load time and report this to application insights, and the events I've found so far does not cover everything. I'm using http://angular-ui.github.com for the UI routing. I've added logging to every event I could think of. The >>> and <<< is logging entries from a http interceptor. The other logging statements should be self explanatory. For a page "Services" the results is:
So I'm missing an event after the content of the services view is presented and bound on the client. Any suggestions? /Kenneth |
+1 I have also this problem, since I want to use masonry I can't because I dont have a place where the plugins injects into the DOM. The summary of options I see:
I will try all of these. hope this gets enabled somehow |
It's clear that this will never be addressed in angular 1.x, but I'll express my desire for this feature as well. It is impossible to do anything that requires measuring DOM elements in Angular. Even The main area I run into is trying to position a dropdown when it opens. If there are any |
Another solution that was not mentioned here has been found by SO-user joeforker. It uses the same logic that Protactor needs to test an element after rendering. |
Yes, this is a problem trouble me too.When i want to do something after angular controller finish add something to $scope and after compiled, I have no ideal how to do it. |
My solution for that kind of problems is angular.afterBindings: |
Hello, There are lot of examples for displaying charts on single page. What about if I need to display the chart on the next page. Its like clicking the button on template 1 and showing the chart on template 2.? |
I think what you want here is a way to asynchronously add a function with custom code to the end of Ionic's (or Angular's) processing queue. I used the Rxjs Observable timer to do this. .ts file .html My use case was to show/hide a Google map in response to a user button click, so I used toggleMap above as the button's click handler function. I toggle a member variable which the *ngIf directive watches to create the HTML element for the map. Then, when the timeout completes, my loadMap function can access that element. If I were to try to call loadMap directly from the toggleMap function without the timeout, I would get an error that the map element is undefined. Also, these need to be imported in the .ts file: I hope this helps someone. |
@JMCollins My gut feeling is that Angular uses this exact same "micro scheduling" scheme for almost anything these days and that you are just so lucky because your particular Angular rendering is simple (enough). If the rendering relies on nested
So the "end" of this queue is really a moving target because the Angular devs know about your trick and are using it for almost everything. Again, I have no idea if this is really the root of the problem, I get nervous when I study the code, but it can in theory explain the strange "quantum effects" you will observe when you attempt to compose Angular with other frameworks. If it is true, it would suit the developers to perform this stunt via a managed queue and then dispatch an event whenever nothing new gets pushed into it; preferably one that can be picked up outside of the Angular code, so like a custom DOM event or something. |
@wiredearp All of what you said makes sense. I may well be really lucky to have a simple *ngIf condition and a fast-running custom function. However, I think the issue you bring up in a more complex scenario would require an event to fire when HTML DOM is done rendering, corresponding to item 5 in your post and not coincidentally the title of this thread. Then, one could perhaps re-schedule an asynchronous update to run custom code (again), item 2 in your post. My case uses the user's button click which occurs after the UI has rendered. Wouldn't a lifecycle hook like ngOnInit or ionViewDidEnter signal that rendering is done? It is also possible that the custom code could make dirty something that Angular is watching, resulting in a circular dependency(?) which would be infinite. Forgive me if I am over-simplifying, just food for thought. |
I know this thread is quite old, but for those who still have to work with old angular version, like myself I have a solution with no extra directives. At least it works fine for me. var digestObserverValue = 0, digestObserverHandledValue = -1;
var onBeforePaint = function() {
// Called before painting
digestObserverValue++; // Prepare for the next digest
$(document.body).trigger("paint.ng");
setTimeout(function() {
// Called almost right after painting
$(document.body).trigger("painted.ng");
}, 0);
};
scope.$watch(function() {
// Called on every digest.
if( digestObserverHandledValue !== digestObserverValue ) {
// Executed only once per multiple sequential digest cycles (until $evalAsync and later
// timeout/animation handlers are called).
digestObserverHandledValue = digestObserverValue;
scope.$evalAsync(function() {
// Called after at least single digest cycle is finished, but before rendering.
if( "requestAnimationFrame" in window ) {
// Get onBeforePaint() called right before DOM styles are calculated and page is
// painted.
requestAnimationFrame(onBeforePaint);
}
else {
// For older browser compatibility: get onBeforePaint() called before painting
// when javascript engine finishes executing the current thread code and starts
// handling expired timers (0ms timeout).
setTimeout(onBeforePaint, 0);
}
});
}
return digestObserverValue;
}, function() {}); I add this code to the application controller. It uses jQuery to trigger |
@destrofer can you explain a bit more how that would work? Also, where would you put code that should run exactly after everything was loaded? |
@hanoii, I can't really explain much more than is written in comments. The main trick is in putting digest detection code into watchExpression function of As for usage: I put this code into controller initialization, but it should also work anywhere where you have access to scopes, like in directive linking function. I would recommend using this code in application controller because it is initialized only once. myApp.controller('MainController', ['$scope', function(scope) {
var digestObserverValue = 0, digestObserverHandledValue = -1;
var onBeforePaint = function() {
// Called before painting
digestObserverValue++; // Prepare for the next digest
$(document.body).trigger("paint.ng");
setTimeout(function() {
// Called almost right after painting
$(document.body).trigger("painted.ng");
}, 0);
};
scope.$watch(function() {
// Called on every digest.
if( digestObserverHandledValue !== digestObserverValue ) {
// Executed only once per multiple sequential digest cycles (until $evalAsync and later
// timeout/animation handlers are called).
digestObserverHandledValue = digestObserverValue;
scope.$evalAsync(function() {
// Called after at least single digest cycle is finished, but before rendering.
if( "requestAnimationFrame" in window ) {
// Get onBeforePaint() called right before DOM styles are calculated and page is
// painted.
requestAnimationFrame(onBeforePaint);
}
else {
// For older browser compatibility: get onBeforePaint() called before painting
// when javascript engine finishes executing the current thread code and starts
// handling expired timers (0ms timeout).
setTimeout(onBeforePaint, 0);
}
});
}
return digestObserverValue;
}, function() {});
}]); To catch this event you can simply handle DOM events <script type="text/javascript">
$(document.body).on("painted.ng", function() {
$.material.init();
});
</script>
<div ng-controller="MainController">
<table>
<tr ng-repeat="item in items">
<td>{{item.name}}</td>
<td><input type="checkbox" ng-model="item.hidden" /></td>
</tr>
</table>
</div> This was a simplified example of usage in javascript outside angular app, but you can use same code in your angular app too. Just remember that I can't think of anything that would require to change scope only after element was rendered, but here is the example that would do so: myApp.directive('greeting', function() {
return {
restrict: 'E',
template: '{{message}}',
link: function(scope, element, attrs, controllers) {
scope.message = "Not rendered yet";
var messageChanged = false;
$(document.body).on("painted.ng", function() {
if( !messageChanged ) {
messageChanged = true; // Prevent infinite loop
scope.message = "Hello world!";
scope.$apply();
}
});
}
};
}); <div ng-controller="MainController">
<greeting></greeting>
</div> |
@destrofer, thanks for the write up - truly appreciate. Unfortunately this didn't work for me. I am working on a complex angular app with a lot of different async events that can affect the DOM and I needed a way to reliable detect the last possible moment in the cycle of the page loading, but I haven't yet found a way to do it. On your logic, the paint event is called many times, and then indefinitely as each time evalSync finishes it called onbefore pain which changes the variable which is watched. I guess in your use case it was good enough to detect it the first time, but I really need it to be once and exactly at the last possible time. On your example, it seems |
I also tried a similar approach of detecting the digest cicle, but it's no point, as evalAsync always add a digest cycle to it. Further more, a watch works of the interval of digest cycles. |
@hanoii, Yes, The only exception I can think of, why it would not work as expected would be if javascript engine is fully asynchronous. That is when timeout/animation frame event handlers are called in a separate thread while angular digest cycles are still running. I checked it on Edge, FireFox, Opera and Chrome, and they did wait till digest cycles were stopped.
I guess it is not possible by using my suggested algorithm since angular may re-render HTML DOM on multiple occasions. But at least I suggested something that is actually related to the topic of this issue :) |
@destrofer - THANK YOU. This is the only method that reliably worked on our really really complicated nested page of ng-repeats. I simply modified your code to turn off our "loading progress" when "painted" and all is well. No other modifications were needed. |
We often need to do some operations on DOM nodes, e.g. bind custom events to them (like "mouseover", "click" or "dblclick"), but Angular changes DOM eventually at the end of digest cycle.
What we need is a way to bind some handler upon DOM modification by Angular is finished and it's safe to handle DOM elements (so they won't disappear next moment). From what I understand ng:init runs only once per node on first document rendering and doesn't guarantee any surrounding nodes are rendered.
Is there a way to set a hook to run on end of digest cycle after all DOM nodes are rendered, so it's safe to go on with DOM operations?
The text was updated successfully, but these errors were encountered: