ChatGPT解决这个技术问题 Extra ChatGPT

How can I run a directive after the dom has finished rendering?

I've got a seemingly simple problem with no apparent (by reading the Angular JS docs) solution.

I have got an Angular JS directive that does some calculations based on other DOM elements' height to define the height of a container in the DOM.

Something similar to this is going on inside the directive:

return function(scope, element, attrs) {
    $('.main').height( $('.site-header').height() -  $('.site-footer').height() );
}

The issue is that when the directive runs, $('site-header') cannot be found, returning an empty array instead of the jQuery wrapped DOM element I need.

Is there a callback that I can use within my directive that only runs after the DOM has been loaded and I can access other DOM elements via the normal jQuery selector style queries?

You could use scope.$on() and scope.$emit() to use custom events. Not sure whether this is the right / recommended approach though.

C
Community

It depends on how your $('site-header') is constructed.

You can try to use $timeout with 0 delay. Something like:

return function(scope, element, attrs) {
    $timeout(function(){
        $('.main').height( $('.site-header').height() -  $('.site-footer').height() );
    });        
}

Explanations how it works: one, two.

Don't forget to inject $timeout in your directive:

.directive('sticky', function($timeout)

Thanks, I tried getting this to work for ages until I realised I hadn't passed $timeout into the directive. Doh. Everything works now, cheers.
Yes, you need to pass $timeout to directive like this: .directive('sticky', function($timeout) { return function (scope, element, attrs, controller) { $timeout(function(){ }); }); };
Your linked explanations explain why the timeout trick works in JavaScript, but not in the context of AngularJS. From the official documentation: "[...] 4. The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. This is usually done with setTimeout(0), but the setTimeout(0) approach suffers from slowness and may cause view flickering since the browser renders the view after each event.[...]" (emphasis mine)
I'm facing a similar problem and have found that I need around 300ms to allow the DOM to load before executing my directive. I really don't like plugging in seemingly arbitrary numbers like that. I'm sure DOM loading speeds will vary depending on the user. So how can I be sure 300ms will work for anyone using my app?
not too happy about this answer.. while it seems to answer the OP's question.. it's very specific to their case and it's relevance to the more general form of the problem (ie running a directive after a dom has loaded) isn't obvious + it's just too hacky.. nothing in it specific about angular at all
r
rjm226

Here is how I do it:

app.directive('example', function() {

    return function(scope, element, attrs) {
        angular.element(document).ready(function() {
                //MANIPULATE THE DOM
        });
    };

});

Shouldn't even need angular.element because element is already available there: element.ready(function(){
@timhc22 element is a reference to the DOMElement that triggered the directive, your recommendation would not result in a DOMElement reference to the pages Document object.
that doesn't work properly. I'm getting offsetWidth = 0 through this approach
B
Brijesh Bhatt

Probably the author won't need my answer anymore. Still, for sake of completeness i feel other users might find it useful. The best and most simple solution is to use $(window).load() inside the body of the returned function. (alternatively you can use document.ready. It really depends if you need all the images or not).

Using $timeout in my humble opinion is a very weak option and may fail in some cases.

Here is the complete code i'd use:

.directive('directiveExample', function(){
   return {
       restrict: 'A',
       link: function($scope, $elem, attrs){

           $(window).load(function() {
               //...JS here...
           });
       }
   }
});

Can you elaborate why it "may fail in some cases"? What cases do you mean?
You're assuming jQuery is available here.
@JonathanCremin jQuery selecting is the issue at hand as per the OP
This works great however if there's a post that builds new items with the directive then the window load will not fire after the initial load and therefore will not function correctly.
@BrianScott - I used a combination of $(window).load for initial page rendering (my use-case was awaiting embedded font files) and then element.ready to take care of switching views.
s
sunderls

there is a ngcontentloaded event, I think you can use it

.directive('directiveExample', function(){
   return {
       restrict: 'A',
       link: function(scope, elem, attrs){

                $$window = $ $window


                init = function(){
                    contentHeight = elem.outerHeight()
                    //do the things
                }

                $$window.on('ngcontentloaded',init)

       }
   }
});

Can you explain what the $ $window is doing?
looks like some coffeescript, maybe it was meant to be $($window) and $window being injected into directive
J
JSV

If you can't use $timeout due to external resources and cant use a directive due to a specific issue with timing, use broadcast.

Add $scope.$broadcast("variable_name_here"); after the desired external resource or long running controller/directive has completed.

Then add the below after your external resource has loaded.

$scope.$on("variable_name_here", function(){ 
   // DOM manipulation here
   jQuery('selector').height(); 
}

For example in the promise of a deferred HTTP request.

MyHttpService.then(function(data){
   $scope.MyHttpReturnedImage = data.image;
   $scope.$broadcast("imageLoaded");
});

$scope.$on("imageLoaded", function(){ 
   jQuery('img').height(80).width(80); 
}

This is not going to solve the problem, since loaded data does not mean, that they are already rendered in the DOM, even if they are in the proper scope variables bound to DOM elements. There's a timespan between the moment they are loaded in the scope and the rendered output in the dom.
j
jaheraho

I had the a similar problem and want to share my solution here.

I have the following HTML:

<div data-my-directive>
  <div id='sub' ng-include='includedFile.htm'></div>
</div>

Problem: In the link-function of directive of the parent div I wanted to jquery'ing the child div#sub. But it just gave me an empty object because ng-include hadn't finished when link function of directive ran. So first I made a dirty workaround with $timeout, which worked but the delay-parameter depended on client speed (nobody likes that).

Works but dirty:

app.directive('myDirective', [function () {
    var directive = {};
    directive.link = function (scope, element, attrs) {
        $timeout(function() {
            //very dirty cause of client-depending varying delay time 
            $('#sub').css(/*whatever*/);
        }, 350);
    };
    return directive;
}]);

Here's the clean solution:

app.directive('myDirective', [function () {
    var directive = {};
    directive.link = function (scope, element, attrs) {
        scope.$on('$includeContentLoaded', function() {
            //just happens in the moment when ng-included finished
            $('#sub').css(/*whatever*/);
        };
    };
    return directive;
}]);

Maybe it helps somebody.