Skip to content

Feature request: a way to detect if Angular finished rendering of HTML DOM #734

Closed
kstep opened this Issue Jan 25, 2012 · 41 comments
@kstep
kstep commented Jan 25, 2012

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?

@IgorMinar
Angular member

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?

@mhevery
Angular member
@kstep
kstep commented Jan 25, 2012

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

@kstep
kstep commented Jan 25, 2012

Quick example: we load some partial with <ng:view/>, then this partial is rendered (e.g. a lot of ng:repeat's and other ng:bind stuff is evaluated), and as the result of this (potentially long) process DOM becomes stable (I guess after the end of digest cycle), so it's safe to apply some special effects to it, like drag-n-drop handling, fading in/out, custom event handlers binding, applying some jQuery plugins effects like Masonry, LazyLoad (to load images in a lazy fashion), or run Facebook handlers (it's a real problem for us, as Facebook XFBML parser requires already existing fb:* DOM nodes, which is difficult to guarantee with asynchronous DOM rendering).

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.

@mhevery
Angular member
@kstep
kstep commented Jan 26, 2012

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.

@mhevery
Angular member
@IgorMinar
Angular member

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.

@IgorMinar IgorMinar closed this Mar 20, 2012
@eprouver

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.

http://jsfiddle.net/eprouver/WpZZE/4/

@paramburu

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.

@kstep
kstep commented Aug 1, 2012

Have you tried using $timeout() to schedule your code execution in $viewContentLoaded event handler? Or evalAsync()?

@eprouver
eprouver commented Aug 1, 2012

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/

@kstep
kstep commented Aug 1, 2012

@eprouver
How about creating my-draggable directive instead?
http://jsfiddle.net/gXSLb/1/
I consider this is most clean and angularish way of doing what you want.

@eprouver
eprouver commented Aug 1, 2012

Ah, yes, perfect... this is exactly the mindset that I was missing. Thanks!

@abdulazeezsk

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,

http://stackoverflow.com/questions/12748392/expressions-are-not-evaluated-in-watch-of-custom-directive-of-angularjs

Didn't get any reply on google group and stack overflow?

Can anyone help me with this?

Thanks,
Abdul

@todiadiyatmo

@kstep
@eprouver

Hi,
I'am quite new to angular. The solution for this problem for me is to detect the dom element change on the parrent element where the ng-repeat attribute apply.

So for @eprouver fiddle, i attach a listener to the

@kstep
kstep commented Nov 15, 2012

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

@erkulas
erkulas commented Mar 20, 2013

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.

@mokesmokes

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.

@wuxiaoying

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.

@eprouver
eprouver commented Aug 6, 2013

@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:
http://jsfiddle.net/eprouver/Xnacm/

@wuxiaoying

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

@eprouver
eprouver commented Aug 6, 2013

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

@wuxiaoying

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 :).

@LukeStone

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.

@shadowii

I ran into a similar problem and found a solution similar to eprouver's. I have a popup-style directive (very similar to angular-ui's) but it supports ajax-driven data and templates. The popup is properly positioned relative to its parent based on its size, which, based on the data and template, can vary.

Unfortunately, I haven't seen a way to get a notification from angular that the popup's HTML has been updated, so I could never properly position the popup relative to the parent after data load.

Anyway, I noticed that angular's $animate service has functions with a callback that meets my needs. I'm guessing it's doing something similar to eprouver's solution but behind the scenes. I used the $animate.addClass function to add a class to my directive element after my data has been loaded. Worked like a charm. Would still like to see something a little less hacky, though...

@Kaidanov
Kaidanov commented Apr 2, 2014

Is there some kind of feature that listens to the whole page dynamically rendered and the whole scopes are digested ?
I have a dunamically built page with ng-repeats inside each other with multiple templates on one view ..How should I catch the rendered and scope is ready event ?
Used $browser notifyWhenNoOutstandingRequests event but work only first time on loading the page.Second time ,it doesn't fill the context of my scope.

@subdigit

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

@zlf341
zlf341 commented Aug 21, 2014

@subdigit thank you, I tried $timeout, it works on me,

@dcleao
dcleao commented Sep 4, 2014

What I think is being request here, is what angular uses internally as scope.$$postDigest(callback).
This is what I would need to implement a "cloak" directive, that must run after all watches.
If it exists, it is because there are good needs for it... So why not find some time to make a thing like this public?

@chandermi

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?

@NecoleYu

$timeout + 1

@gercheq
gercheq commented Feb 23, 2015

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

<!-- banner -->
<div hero-section></div>

<!-- recommendations -->
<div recommendations></div>

style.css

.hero-section { 
  background-image: url('hero-image.jpg');
}

.recommendations {
  background-image: url('recommendations-image.jpg')
}

If you want hero-image.jpg to load before the other one, good luck. I was able achieve this only by using $timeout inside the recommendations directive.

It'd be great to have a way of prioritizing which directives should be rendered first.

@sebastianconcept

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

$viewContentFinished or $viewContentRendered event required.

@phaggood phaggood referenced this issue in zachfitz/Ionic-Material May 19, 2015
Closed

Can't get ng-repeat to display on modified friends page #65

@kaafloy
kaafloy commented May 30, 2015

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:

 Services - $stateChangeStart
 >>> GET app/views/services.html
 <<< GET app/views/services.html
 Services - $stateChangeSuccess
 Services - $viewContentLoaded
 >>> GET <backendservice>/services
 Services - $locationChangeStart 
 Services - $locationChangeSuccess
 <<< GET <backendservice>/services

So I'm missing an event after the content of the services view is presented and bound on the client.

Any suggestions?

/Kenneth

@katlimruiz

+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:
1. $viewContentLoaded + $timeout
2. if you use your own directive, link + $timeout
3. if the directive is 3rd party, use a transparent gif as a directive, and link + $timeout

I will try all of these.

hope this gets enabled somehow

@fenduru
fenduru commented Nov 13, 2015

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 $$postDigest is insufficient because of the way ngAnimate works (it might add a $$postDigest handler after yours, and then also does logic in animation frames).

The main area I run into is trying to position a dropdown when it opens. If there are any ngIfs inside the dropdown content, the measurements made to do the positioning are invalidated when new content is transcluded.

@basvd
basvd commented Feb 25, 2016

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.

@hexinatgithub

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.