web-dev-qa-db-fra.com

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

J'ai un modèle Angular 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 aussi faire un travail supplémentaire après que le modèle a été rendu et rendu dans le DOM (dans ce cas, un plugin jQuery).

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

Voici un jsFiddle soulignant mon problème: Fiddle-AngularIssue

== UPDATE ==

Sur la base de commentaires utiles, je suis donc passé à une directive pour gérer la manipulation du DOM et ai implémenté un modèle $ watch dans 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 ne soit compilé et inséré dans le DOM; par conséquent, le plug-in jquery évalue toujours une table vide.

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

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

178
gorebash

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

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

39
dipold

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

J'ai eu le même problème et est venu avec cet extrait. Il utilise $watch et $evalAsync pour s'assurer que votre code est exécuté une fois que des directives telles que ng-repeat ont été résolues et que des modèles tels que {{ value }} ont é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 à prévenir certaines difficultés.

63
danijar

Suivant les conseils de Misko, si vous voulez une opération asynchrone, alors au lieu de $ timeout () (ce 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, en simulant une modification des données/du modèle - pour indiquer que la modification des données fonctionne.

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

Cependant, comme vous l'avez déjà constaté, je ne pense pas qu'il soit nécessaire d'opérer de manière asynchrone ici.

34
Mark Rajcok

J'ai trouvé la solution la plus simple (pas chère et gaie) est simplement d'ajouter une étendue vide avec ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" à la fin du dernier élément affiché. 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 réalise que ce n'est pas la manière la plus élégante de faire les choses, cependant, ça marche 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 insuffisant car l'indicateur de chargement a été 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 restitué et, dans cette fonction, j'ai retourné la variable var qui masque l'indicateur de chargement. (Bien sûr, j'ai ajouté un ng-hide à l'indicateur de chargement déclenché par cette fonction.

26
fuzion9

Je pense que vous recherchez $ evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync

7
Misko Hevery

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

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

j'ai dû le faire assez souvent. J'ai une directive et j'ai besoin de faire quelques trucs jquery après que les trucs de modèle soient complètement chargés dans le DOM. donc j'ai mis ma logique dans le lien: fonction de la directive et envelopper le code dans un setTimeout (function () {.....}, 1); setTimout se déclenchera après le chargement du DOM et 1 milliseconde est la durée la plus courte après le chargement du DOM avant l'exécution du code. cela semble fonctionner pour moi, mais je souhaite que angular déclenche le chargement d'un événement une fois le chargement du gabarit terminé, afin que les directives utilisées par ce gabarit puissent effectuer des opérations jQuery et accéder aux éléments DOM. J'espère que cela t'aides.

3
mgrimmenator

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

Voir cette réponse stackoverflow .

2
Anthony O.

Ni $ scope. $ EvalAsync () ni $ timeout (fn, 0) n'ont fonctionné de manière fiable pour moi.

Je devais combiner les deux. J'ai fait une directive et mis également une priorité supérieure à 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, vous feriez ceci:

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

Si vous voulez l'appeler après l'exécution d'une répétition ng, j'ai ajouté une plage vide dans mes répétitions ng 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>
2
Xchai

Je suis venu avec une solution assez simple. Je ne suis pas sûr que ce soit 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 quelques ng-repeats, je ferais attention à la longueur du texte (vous pourriez avoir autre chose!) De paragraphes ou de tout le code 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
            });
        }
}]);

NOTE que le code s'exécute réellement après le rendu des modifications est un rendu plutôt complet. Mais je suppose que dans la plupart des cas, vous pouvez gérer les situations à chaque changement de rendu. Vous pouvez également envisager de comparer cette longueur de ps (ou toute autre mesure) avec votre modèle si vous souhaitez exécuter votre code uniquement une fois après rendu terminé. J'apprécie vos pensées/commentaires à ce sujet.

1
Ehsan88

Dans certains scénarios où vous mettez à jour un service et redirigez vers une nouvelle vue (page), puis que votre directive soit chargée avant la mise à jour de vos services, 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);
        }
   };
});
0
Sudharshan

Vous pouvez utiliser le module 'jQuery Passthrough' de angular-ui utils . J'ai lié avec succès un plugin jQuery Touch Carousel à des images que je récupérais en async d'un service Web et les rendais avec ng-repeat.

0
Michael Kork.