ChatGPT解决这个技术问题 Extra ChatGPT

AngularJS: How to run additional code after AngularJS has rendered a template?

I have an Angular template in the DOM. When my controller gets new data from a service, it updates the model in the $scope, and re-renders the template. All good so far.

The issue is that I need to also do some extra work after the template has been re-rendered and is in the DOM (in this case a jQuery plugin).

It seems like there should be an event to listen to, such as AfterRender, but I can't find any such thing. Maybe a directive would be a way to go, but it seemed to fire too early as well.

Here is a jsFiddle outlining my problem: Fiddle-AngularIssue

== UPDATE ==

Based on helpful comments, I've accordingly switched to a directive to handle DOM manipulation, and implemented a model $watch inside the directive. However, I still am having the same base issue; the code inside of the $watch event fires before the template has been compiled and inserted into the DOM, therefore, the jquery plugin is always evaluating an empty table.

Interestingly, if I remove the async call the whole thing works fine, so that's a step in the right direction.

Here is my updated Fiddle to reflect these changes: http://jsfiddle.net/uNREn/12/

This seems similar to a question I had. stackoverflow.com/questions/11444494/…. Maybe something in there can help.

d
danijar

First, the right place to mess with rendering are directives. My advice would be to wrap DOM manipulating jQuery plugins by directives like this one.

I had the same problem and came up with this snippet. It uses $watch and $evalAsync to ensure your code runs after directives like ng-repeat have been resolved and templates like {{ value }} got rendered.

app.directive('name', function() {
    return {
        link: function($scope, element, attrs) {
            // Trigger when number of children changes,
            // including by directives like ng-repeat
            var watch = $scope.$watch(function() {
                return element.children().length;
            }, function() {
                // Wait for templates to render
                $scope.$evalAsync(function() {
                    // Finally, directives are evaluated
                    // and templates are renderer here
                    var children = element.children();
                    console.log(children);
                });
            });
        },
    };
});

Hope this can help you prevent some struggle.


This might be stating the obvious, but if this doesn't work for you, check for asynchronous operations in your link functions/controllers. I don't think $evalAsync can take those into account.
Worth noting that you can also combine a link function with a templateUrl.
P
Prashant Pokhriyal

This post is old, but I change your code to:

scope.$watch("assignments", function (value) {//I change here
  var val = value || null;            
  if (val)
    element.dataTable({"bDestroy": true});
  });
}

see jsfiddle.

I hope it helps you


Thanks, that's very helpful. This is the best solution for me because it doesn't require dom manipulation from the controller, and will still work when the data is updated from a truly asynchronous service response (and I don't have to use $evalAsync in my controller).
Has this been deprecated? When I try this it actually calls just before the DOM is rendered. Is it dependent on a shadow dom or something?
I'm relatively new to Angular and I can't see how to implement this answer with my specific case. I've been looking at various answers and guessing, but that's not working. I'm not sure what the 'watch' function should be watching. Our angular app has various different eng- directives, which will vary from page to page, so how do I know what to 'watch'? Surely there's a simple way to fire off some code when a page has finished being rendered?
g
georgeawg

Following Misko's advice, if you want async operation, then instead of $timeout() (which doesn't work)

$timeout(function () { $scope.assignmentsLoaded(data); }, 1000);

use $evalAsync() (which does work)

$scope.$evalAsync(function() { $scope.assignmentsLoaded(data); } );

Fiddle. I also added a "remove row of data" link that will modify $scope.assignments, simulating a change to the data/model -- to show that changing the data works.

The Runtime section of the Conceptual Overview page explains that evalAsync should be used when you need something to occur outside the current stack frame, but before the browser renders. (Guessing here... "current stack frame" probably includes Angular DOM updates.) Use $timeout if you need something to occur after the browser renders.

However, as you already found out, I don't think there is any need for async operation here.


$scope.$evalAsync($scope.assignmentsLoaded(data)); doesn't make any sense IMO. The argument for $evalAsync() should be a function. In your example you're calling the function at the time a function should be registered for later execution.
@hgoebl, the argument to $evalAsync can be a function or an expression. In this case I used an expression. But you're right, the expression gets executed immediately. I modified the answer to wrap it in a function for later execution. Thanks for catching that.
f
fuzion9

I have found the simplest (cheap and cheerful) solution is simply add an empty span with ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing()" to the end of the last element rendered. This function will be run when to check if the span element should be displayed. Execute any other code in this function.

I realize this is not the most elegant way to do things, however, it works for me...

I had a similar situation, though slightly reversed where I needed to remove a loading indicator when an animation began, on mobile devices angular was initializing much faster than the animation to be displayed, and using an ng-cloak was insufficient as the loading indicator was removed well before any real data was displayed. In this case I just added the my return 0 function to the first rendered element, and in that function flipped the var that hides the loading indicator. (of course I added an ng-hide to the loading indicator triggered by this function.


This is a hack for sure but it is exactly what i needed. Forced my code to run exactly after render (or very close to exactly). In an ideal world I would not do this but here we are...
I needed the built in $anchorScroll service to be called after the DOM rendered and nothing seemed to do the trick but this. Hackish but works.
ng-init is a better alternative
ng-init might not always work. For example, I have an ng-repeat that adds a number of images to the dom. If I want to call a function after each image is added in the ng-repeat I have to use ng-show.
WOW! great idea. using Angular against itself. This is the only one that works, all the solutions involving an async operation simply don't work since the view is visible before the change, what causes a flicker. Angular is simply great, but these trivial things (e.g. simply focusing an element DIRECTLY) are very hard to implement in it and require hacks
e
eterps

I think you are looking for $evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync


Thanks for the comment. Perhaps I'm too new to Angular but I'm not seeing how to work this into my code. Are there any examples you could point me to?
S
Sergio Ceron

Finally i found the solution, i was using a REST service to update my collection. In order to convert datatable jquery is the follow code:

$scope.$watchCollection( 'conferences', function( old, nuew ) {
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () {
                $( '#dataTablex' ).dataTable();
        });
    });

m
mgrimmenator

i've had to do this quite often. i have a directive and need to do some jquery stuff after model stuff is fully loaded into the DOM. so i put my logic in the link: function of the directive and wrap the code in a setTimeout(function() { ..... }, 1); the setTimout will fire after the DOM is loaded and 1 milisecond is the shortest amount of time after DOM is loaded before code would execute. this seems to work for me but i do wish angular raised an event once a template was done loading so that directives used by that template could do jquery stuff and access DOM elements. hope this helps.


C
Community

You can also create a directive that runs your code in the link function.

See that stackoverflow reply.


X
Xchai

Neither $scope.$evalAsync() or $timeout(fn, 0) worked reliably for me.

I had to combine the two. I made a directive and also put a priority higher than the default value for good measure. Here's a directive for it (Note I use ngInject to inject dependencies):

app.directive('postrenderAction', postrenderAction);

/* @ngInject */
function postrenderAction($timeout) {
    // ### Directive Interface
    // Defines base properties for the directive.
    var directive = {
        restrict: 'A',
        priority: 101,
        link: link
    };
    return directive;

    // ### Link Function
    // Provides functionality for the directive during the DOM building/data binding stage.
    function link(scope, element, attrs) {
        $timeout(function() {
            scope.$evalAsync(attrs.postrenderAction);
        }, 0);
    }
}

To call the directive, you would do this:

<div postrender-action="functionToRun()"></div>

If you want to call it after an ng-repeat is done running, I added an empty span in my ng-repeat and ng-if="$last":

<li ng-repeat="item in list">
    <!-- Do stuff with list -->
    ...

    <!-- Fire function after the last element is rendered -->
    <span ng-if="$last" postrender-action="$ctrl.postRender()"></span>
</li>

S
Sudharshan

In some scenarios where you update a service and redirect to a new view(page) and then your directive gets loaded before your services are updated then you can use $rootScope.$broadcast if your $watch or $timeout fails

View

<service-history log="log" data-ng-repeat="log in requiedData"></service-history>

Controller

app.controller("MyController",['$scope','$rootScope', function($scope, $rootScope) {

   $scope.$on('$viewContentLoaded', function () {
       SomeSerive.getHistory().then(function(data) {
           $scope.requiedData = data;
           $rootScope.$broadcast("history-updation");
       });
  });

}]);

Directive

app.directive("serviceHistory", function() {
    return {
        restrict: 'E',
        replace: true,
        scope: {
           log: '='
        },
        link: function($scope, element, attrs) {
            function updateHistory() {
               if(log) {
                   //do something
               }
            }
            $rootScope.$on("history-updation", updateHistory);
        }
   };
});

E
Ehsan88

I came with a pretty simple solution. I'm not sure whether it is the correct way to do it but it works in a practical sense. Let's directly watch what we want to be rendered. For example in a directive that includes some ng-repeats, I would watch out for the length of text (you may have other things!) of paragraphs or the whole html. The directive will be like this:

.directive('myDirective', [function () {
    'use strict';
    return {

        link: function (scope, element, attrs) {
            scope.$watch(function(){
               var whole_p_length = 0;
               var ps = element.find('p');
                for (var i=0;i<ps.length;i++){
                    if (ps[i].innerHTML == undefined){
                        continue
                    }
                    whole_p_length+= ps[i].innerHTML.length;
                }
                //it could be this too:  whole_p_length = element[0].innerHTML.length; but my test showed that the above method is a bit faster
                console.log(whole_p_length);
                return whole_p_length;
            }, function (value) {   
                //Code you want to be run after rendering changes
            });
        }
}]);

NOTE that the code actually runs after rendering changes rather complete rendering. But I guess in most cases you can handle the situations whenever rendering changes happen. Also you could think of comparing this ps length (or any other measure) with your model if you want to run your code only once after rendering completed. I appreciate any thoughts/comments on this.


M
Michael Kork.

You can use the 'jQuery Passthrough' module of the angular-ui utils. I successfully binded a jQuery touch carousel plugin to some images that I retrieve async from a web service and render them with ng-repeat.


s
sksallaj

In my solution, I had a few custom directives that needed to be loaded first because they contained the definition of functions that their sibling directives call. For example:

<div id="container">
    <custom-directive1></custom-directive1>
    <custom-directive2></custom-directive2>
    <custom-directive3></custom-directive3>
</div>

Unfortunately, none of the solutions on here worked for me, because they only worked after rendering a directive, not the directive code behind.

So when I implemented any of the solutions above, to execute some load function, even though the directive were rendered, the scope didn't know what the functions inside those directives were.

So I created an observable anywhere in my controller:

//Call every time a directive is loaded
$scope.$watch('directiveLoaded', function (value) {
            debugger;
    if (value == document.querySelector('#container').children.length) {
        //Its ok to use childHead as we have only one child scope
        $scope.$$childHead.function1_Of_Directive1();
        $scope.$$childHead.function1_Of_Directive2();
    }
});

Then I have these two directives, where I place

scope.$parent.directiveLoaded += 1;

at the bottom of every directive. Because in the controller I have the observable defined, each time I update the variable directiveLoaded, it executes the observable function. Yes, I know this is a hack, but it's a small price to pay to guarantee all directives finish the rendering along with their code behind before executing the final function.

To complete the demo here are two directives that define the functions that need to be called later.

Directive1

(function () {
    app.directive('customDirective1', function () {
        return {
            restrict: 'E',
            templateUrl: '/directive1.html',
            link: function (scope) {

                scope.function1_Of_Directive1 = function() {
                    scope.function2_Of_Directive2();
                    console.log("F1_D1")
                }
     
                //AT BOTTOM OF EVERY DIRECTIVE
                scope.$parent.directiveLoaded += 1;
            }
        }
    });
})();

Directive2

(function () {
    app.directive('customDirective2', function () {
        return {
            restrict: 'E',
            templateUrl: '/directive1.html',
            link: function (scope) {
                
                scope.function1_Of_Directive2 = function() {
                    console.log("F1_D2")
                }
                scope.function2_Of_Directive2 = function() {
                    console.log("F2_D2")
                }

                //AT BOTTOM OF EVERY DIRECTIVE
                scope.$parent.directiveLoaded += 1;
            }
        }
    });
})();