AngularJS: Comment exécuter du code supplémentaire après qu'AngularJS a rendu un modèle?

182

J'ai un modèle angulaire dans le DOM. Lorsque mon contrôleur obtient de nouvelles données d'un service, il met à jour le modèle dans la portée $ et rend à nouveau le modèle. Tout va bien jusqu'à présent.

Le problème est que je dois également faire un travail supplémentaire après que le modèle a été re-rendu et se trouve dans le DOM (dans ce cas, un plugin jQuery).

Il semble qu'il devrait y avoir un événement à écouter, comme AfterRender, mais je ne trouve rien de tel. Une directive serait peut-être une voie à suivre, mais elle semblait également être déclenchée trop tôt.

Voici un jsFiddle décrivant mon problème: Fiddle-AngularIssue

== MISE À JOUR ==

Sur la base de commentaires utiles, je suis donc passé à une directive pour gérer la manipulation du DOM et implémenté un modèle $ watch à l'intérieur de la directive. Cependant, j'ai toujours le même problème de base; le code à l'intérieur de l'événement $ watch se déclenche avant que le modèle n'ait été compilé et inséré dans le DOM, par conséquent, le plugin jquery évalue toujours une table vide.

Fait intéressant, si je supprime l'appel asynchrone, tout fonctionne bien, c'est donc un pas dans la bonne direction.

Voici mon Fiddle mis à jour pour refléter ces changements: http://jsfiddle.net/uNREn/12/

gorebash
la source
Cela semble similaire à une question que j'avais. stackoverflow.com/questions/11444494/… . Peut-être que quelque chose peut vous aider.
dnc253

Réponses:

40

Ce message est ancien, mais je change votre code en:

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

voir jsfiddle .

J'espère que ça t'aide

dipold
la source
Merci, c'est très utile. C'est la meilleure solution pour moi car elle ne nécessite pas de manipulation dom de la part du contrôleur, et fonctionnera toujours lorsque les données seront mises à jour à partir d'une réponse de service vraiment asynchrone (et je n'ai pas à utiliser $ evalAsync dans mon contrôleur).
gorebash
9
Cela est-il obsolète? Quand j'essaye ceci, il appelle en fait juste avant que le DOM soit rendu. Cela dépend-il d'un shadow dom ou de quelque chose?
Shayne
Je suis relativement nouveau sur Angular et je ne vois pas comment implémenter cette réponse avec mon cas spécifique. J'ai regardé diverses réponses et deviné, mais cela ne fonctionne pas. Je ne suis pas sûr de ce que la fonction «regarder» devrait regarder. Notre application angulaire a différentes directives d'ingénierie, qui varient d'une page à l'autre, alors comment savoir quoi «regarder»? Il existe sûrement un moyen simple de déclencher du code lorsqu'une page a fini d'être rendue?
moosefetcher
63

Premièrement, les directives sont au bon endroit pour jouer avec le rendu. Mon conseil serait d'encapsuler le DOM manipulant les plugins jQuery par des directives comme celle-ci.

J'ai eu le même problème et j'ai trouvé cet extrait. Il utilise $watchet $evalAsyncpour garantir que votre code s'exécute après que des directives comme ng-repeatont été résolues et des modèles comme ont {{ value }}été rendus.

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);
                });
            });
        },
    };
});

J'espère que cela peut vous aider à éviter certaines difficultés.

danijar
la source
Cela peut être une évidence, mais si cela ne fonctionne pas pour vous, vérifiez les opérations asynchrones dans vos fonctions / contrôleurs de liaison. Je ne pense pas que $ evalAsync puisse en tenir compte.
LOAS
A noter que vous pouvez également combiner une linkfonction avec un templateUrl.
Wtower
34

Suivant les conseils de Misko, si vous voulez une opération asynchrone, alors au lieu de $ timeout () (qui ne fonctionne pas)

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

utilisez $ evalAsync () (qui fonctionne)

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

Violon . J'ai également ajouté un lien "supprimer une ligne de données" qui modifiera $ scope.assignments, simulant une modification des données / modèle - pour montrer que changer les données fonctionne.

La section Runtime de la page Présentation conceptuelle explique que evalAsync doit être utilisé lorsque vous avez besoin que quelque chose se produise en dehors du cadre de pile actuel, mais avant le rendu du navigateur. (Deviner ici ... "le frame de pile actuel" inclut probablement les mises à jour Angular DOM.) Utilisez $ timeout si vous avez besoin que quelque chose se produise après le rendu du navigateur.

Cependant, comme vous l'avez déjà découvert, je ne pense pas que le fonctionnement asynchrone soit nécessaire ici.

Mark Rajcok
la source
4
$scope.$evalAsync($scope.assignmentsLoaded(data));n'a aucun sens IMO. L'argument pour $evalAsync()devrait être une fonction. Dans votre exemple, vous appelez la fonction au moment où une fonction doit être enregistrée pour une exécution ultérieure.
hgoebl
@hgoebl, l'argument de $ evalAsync peut être une fonction ou une expression. Dans ce cas, j'ai utilisé une expression. Mais vous avez raison, l'expression est exécutée immédiatement. J'ai modifié la réponse pour l'envelopper dans une fonction pour une exécution ultérieure. Merci d'avoir attrapé ça.
Mark Rajcok
26

J'ai trouvé que la solution la plus simple (bon marché et gaie) est simplement d'ajouter une plage vide avec ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" à la fin du dernier élément rendu. Cette fonction sera exécutée pour vérifier si l'élément span doit être affiché. Exécutez tout autre code dans cette fonction.

Je me rends compte que ce n'est pas la façon la plus élégante de faire les choses, cependant, cela fonctionne pour moi ...

J'ai eu une situation similaire, bien que légèrement inversée où je devais supprimer un indicateur de chargement au début d'une animation, sur les appareils mobiles, angular s'initialisait beaucoup plus rapidement que l'animation à afficher, et l'utilisation d'un ng-cloak était insuffisante car l'indicateur de chargement était supprimé bien avant que des données réelles ne soient affichées. Dans ce cas, je viens d'ajouter la fonction my return 0 au premier élément rendu, et dans cette fonction, j'ai inversé la variable qui cache l'indicateur de chargement. (bien sûr, j'ai ajouté un ng-hide à l'indicateur de chargement déclenché par cette fonction.

fuzion9
la source
3
C'est un hack à coup sûr, mais c'est exactement ce dont j'avais besoin. J'ai forcé mon code à s'exécuter exactement après le rendu (ou très proche de exactement). Dans un monde idéal, je ne ferais pas cela, mais nous voici ...
Andrew
J'avais besoin que le $anchorScrollservice intégré soit appelé après le rendu du DOM et rien ne semblait faire l'affaire à part ça. Hackish mais fonctionne.
Pat Migliaccio
ng-init est une meilleure alternative
Flying Gambit
1
ng-init peut ne pas toujours fonctionner. Par exemple, j'ai un ng-repeat qui ajoute un certain nombre d'images au dom. Si je veux appeler une fonction après que chaque image est ajoutée dans le ng-repeat, je dois utiliser ng-show.
Michael Bremerkamp
4

Enfin j'ai trouvé la solution, j'utilisais un service REST pour mettre à jour ma collection. Afin de convertir jquery datatable est le code suivant:

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

j'ai dû faire ça assez souvent. J'ai une directive et dois faire des trucs jquery après que le truc du modèle soit complètement chargé dans le DOM. donc je mets ma logique dans le lien: fonction de la directive et enveloppe le code dans un setTimeout (function () {.....}, 1); le setTimout se déclenchera après le chargement du DOM et 1 milliseconde est le temps le plus court après le chargement du DOM avant que le code ne s'exécute. cela semble fonctionner pour moi mais je souhaite que angular déclenche un événement une fois qu'un modèle a été chargé, afin que les directives utilisées par ce modèle puissent faire des tâches jquery et accéder aux éléments DOM. J'espère que cela t'aides.

agrégateur
la source
2

Vous pouvez également créer une directive qui exécute votre code dans la fonction de lien.

Voir cette réponse stackoverflow .

Anthony O.
la source
2

Ni $ scope. $ EvalAsync () ou $ timeout (fn, 0) ne fonctionnaient de manière fiable pour moi.

J'ai dû combiner les deux. J'ai fait une directive et j'ai également mis une priorité plus élevée que la valeur par défaut pour une bonne mesure. Voici une directive pour cela (notez que j'utilise ngInject pour injecter des dépendances):

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);
    }
}

Pour appeler la directive, procédez comme suit:

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

Si vous voulez l'appeler après l'exécution d'une ng-repeat, j'ai ajouté un span vide dans mon ng-repeat et 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>
Xchai
la source
1

Dans certains scénarios où vous mettez à jour un service et redirigez vers une nouvelle vue (page), puis votre directive est chargée avant que vos services ne soient mis à jour, vous pouvez utiliser $ rootScope. $ Broadcast si votre $ watch ou $ timeout échoue

Vue

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

Manette

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

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

}]);

Directif

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);
        }
   };
});
Sudharshan
la source
1

Je suis venu avec une solution assez simple. Je ne sais pas si c'est la bonne façon de procéder, mais cela fonctionne dans un sens pratique. Regardons directement ce que nous voulons être rendu. Par exemple, dans une directive qui inclut des ng-repeats, je ferais attention à la longueur du texte (vous pouvez avoir d'autres choses!) Des paragraphes ou de tout le HTML. La directive sera comme ceci:

.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
            });
        }
}]);

Notez que le code s'exécute réellement après le rendu des changements de rendu plutôt complet. Mais je suppose que dans la plupart des cas, vous pouvez gérer les situations chaque fois que des changements de rendu se produisent. Vous pouvez également penser à comparer cette plongueur (ou toute autre mesure) avec votre modèle si vous souhaitez exécuter votre code une seule fois après la fin du rendu. J'apprécie toute réflexion / commentaire à ce sujet.

Ehsan88
la source
0

Vous pouvez utiliser le module 'jQuery Passthrough' des utils angular-ui . J'ai lié avec succès un plugin de carrousel tactile jQuery à certaines images que je récupère de manière asynchrone à partir d'un service Web et les rend avec ng-repeat.

Michael Kork.
la source