Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

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

Closed
kstep opened this Issue · 35 comments
@kstep

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
Owner

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
Owner
@kstep

$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

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
Owner
@kstep

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
Owner
@IgorMinar
Owner

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

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

@eprouver

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

@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

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

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

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

@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

@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

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

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

@dcleao

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

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