web-dev-qa-db-fra.com

Envoi d'un événement lorsque AngularJS a fini de charger

Je me demandais quel était le meilleur moyen de détecter la fin du chargement/de l’amorçage de la page lorsque toutes les directives étaient compilées/reliées.

Un événement déjà là? Dois-je surcharger la fonction bootstrap?

113
Lior

Angular n’a pas permis de signaler la fin du chargement d’une page, peut-être parce que "terminé" dépend de votre application. Par exemple, si vous avez une arborescence hiérarchique de partiels, l’une chargeant les autres. "Terminer" voudrait dire que tous ont été chargés. N'importe quel framework aurait du mal à analyser votre code et à comprendre que tout est fait ou attendu. Pour cela, vous devez fournir une logique spécifique à l'application pour vérifier et déterminer cela.

15
Lior

Juste un pressentiment: pourquoi ne pas regarder comment la directive ngCloak le fait? Il est clair que la directive ngCloak parvient à afficher le contenu après le chargement des éléments. Je parie que regarder ngCloak mènera à la réponse exacte ...

EDIT 1 heure plus tard: Ok, eh bien, j'ai regardé ngCloak et c'est vraiment court. Cela implique évidemment que la fonction de compilation ne sera pas exécutée tant que les expressions {{template}} n'auront pas été évaluées (c'est-à-dire le modèle qu'elle a chargé), d'où la fonctionnalité Nice de la directive ngCloak.

Mon hypothèse serait de faire une directive avec la même simplicité que ngCloak, puis dans votre fonction de compilation, faites ce que vous voulez. :) Placez la directive sur l'élément racine de votre application. Vous pouvez appeler la directive quelque chose comme myOnload et l'utiliser comme attribut my-onload. La fonction de compilation s'exécutera une fois le modèle compilé (expressions évaluées et sous-modèles chargés).

EDIT, 23 heures plus tard: Ok, j'ai donc fait quelques recherches et j'ai aussi posé ma propre question . La question que j'ai posée était indirectement liée à cette question, mais elle m’a amené par hasard à la réponse qui résout cette question.

La réponse est que vous pouvez créer une directive simple et placer votre code dans la fonction de lien de la directive, qui (pour la plupart des cas d'utilisation, expliquée ci-dessous) s'exécutera lorsque votre élément est prêt/chargé. Basé sur description de Josh de l'ordre dans lequel les fonctions de compilation et de liaison sont exécutées ,

si vous avez ce balisage:

<div directive1>
  <div directive2>
    <!-- ... -->
  </div>
</div>

Ensuite, AngularJS créera les directives en exécutant les fonctions de directive dans un certain ordre:

directive1: compile
  directive2: compile
directive1: controller
directive1: pre-link
  directive2: controller
  directive2: pre-link
  directive2: post-link
directive1: post-link

Par défaut, une fonction "lien" directe est un post-lien. Par conséquent, la fonction de lien de votre directive externe1 ne sera exécutée qu'après l'exécution de la fonction de lien de la directive intérieure2. C'est pourquoi nous disons qu'il est seulement sécuritaire de manipuler le DOM dans le post-lien. Donc, en ce qui concerne la question initiale, il ne devrait y avoir aucun problème pour accéder au code HTML interne de la directive enfant à partir de la fonction de lien de la directive externe, bien que le contenu inséré dynamiquement soit compilé, comme indiqué plus haut.

Nous pouvons en conclure que nous pouvons simplement créer une directive pour exécuter notre code lorsque tout est prêt/compilé/lié/chargé:

    app.directive('ngElementReady', [function() {
        return {
            priority: -1000, // a low number so this directive loads after all other directives have loaded. 
            restrict: "A", // attribute only
            link: function($scope, $element, $attributes) {
                console.log(" -- Element ready!");
                // do what you want here.
            }
        };
    }]);

Maintenant, ce que vous pouvez faire est de mettre la directive ngElementReady sur l'élément racine de l'application, et le console.log se déclenchera lorsqu'il sera chargé:

<body data-ng-app="MyApp" data-ng-element-ready="">
   ...
   ...
</body>

C'est si simple! Il suffit de faire une directive simple et l'utiliser. ;)

Vous pouvez le personnaliser davantage afin qu'il puisse exécuter une expression (c'est-à-dire une fonction) en y ajoutant $scope.$eval($attributes.ngElementReady);:

    app.directive('ngElementReady', [function() {
        return {
            priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
            restrict: "A",
            link: function($scope, $element, $attributes) {
                $scope.$eval($attributes.ngElementReady); // execute the expression in the attribute.
            }
        };
    }]);

Ensuite, vous pouvez l'utiliser sur n'importe quel élément:

<body data-ng-app="MyApp" data-ng-controller="BodyCtrl" data-ng-element-ready="bodyIsReady()">
    ...
    <div data-ng-element-ready="divIsReady()">...<div>
</body>

Assurez-vous simplement que vos fonctions (par exemple, bodyIsReady et divIsReady) sont définies dans l'étendue (dans le contrôleur) sous laquelle votre élément vit.

Mises en garde: J'ai dit que cela fonctionnerait pour la plupart des cas. Soyez prudent lorsque vous utilisez certaines directives telles que ngRepeat et ngIf. Ils créent leur propre champ d'application et votre directive ne peut pas être déclenchée. Par exemple, si vous placez notre nouvelle directive ngElementReady sur un élément contenant également ngIf et que la condition de ngIf est définie sur false, notre directive ngElementReady ne sera pas chargée. Ou, par exemple, si vous placez notre nouvelle directive ngElementReady sur un élément qui possède également une directive ngInclude, notre directive ne sera pas chargée si le modèle pour ngInclude n'existe pas. Vous pouvez contourner certains de ces problèmes en veillant à imbriquer les directives au lieu de les placer toutes sur le même élément. Par exemple, en faisant ceci:

<div data-ng-element-ready="divIsReady()">
    <div data-ng-include="non-existent-template.html"></div>
<div>

au lieu de cela:

<div data-ng-element-ready="divIsReady()" data-ng-include="non-existent-template.html"></div>

La directive ngElementReady sera compilée dans le dernier exemple, mais sa fonction de lien ne sera pas exécutée. Remarque: les directives sont toujours compilées, mais leurs fonctions de liaison ne sont pas toujours exécutées en fonction de certains scénarios, comme ci-dessus.

EDIT, quelques minutes plus tard:

Oh, et pour bien répondre à la question, vous pouvez maintenant $emit ou $broadcast votre événement à partir de l'expression ou de la fonction exécutée dans l'attribut ng-element-ready. :) Par exemple.:

<div data-ng-element-ready="$emit('someEvent')">
    ...
<div>

EDIT, encore plus quelques minutes plus tard:

La réponse de @ satchmorun fonctionne également, mais uniquement pour le chargement initial. Voici une très utile SO question qui décrit l'ordre d'exécution, y compris les fonctions de liaison, app.run, et autres. Donc, selon votre cas d'utilisation, app.run pourrait être bon, mais pas pour des éléments spécifiques, auquel cas les fonctions de lien sont meilleures.

EDIT, cinq mois plus tard, le 17 octobre à 8h11 HNP:

Cela ne fonctionne pas avec les partiels chargés de manière asynchrone. Vous aurez besoin d’ajouter de la comptabilité dans vos partiels (par exemple, vous pouvez faire en sorte que chaque partiel garde la trace de son contenu, puis émettez un événement afin que la portée parente puisse compter le nombre de partiels chargés et enfin faire ce qu’il faut. faire après que tous les partiels sont chargés).

EDIT, le 23 octobre à 22h52 HNP:

J'ai fait une directive simple pour tirer du code quand une image est chargée:

/*
 * This img directive makes it so that if you put a loaded="" attribute on any
 * img element in your app, the expression of that attribute will be evaluated
 * after the images has finished loading. Use this to, for example, remove
 * loading animations after images have finished loading.
 */
  app.directive('img', function() {
    return {
      restrict: 'E',
      link: function($scope, $element, $attributes) {
        $element.bind('load', function() {
          if ($attributes.loaded) {
            $scope.$eval($attributes.loaded);
          }
        });
      }
    };
  });

EDIT, le 24 octobre à 00h48, heure du Pacifique:

J'ai amélioré ma directive originale ngElementReady et je l'ai renommée whenReady.

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. done loading all sub directives and DOM
 * content except for things that load asynchronously like partials and images).
 *
 * Execute multiple expressions by delimiting them with a semi-colon. If there
 * is more than one expression, and the last expression evaluates to true, then
 * all expressions prior will be evaluated after all text nodes in the element
 * have been interpolated (i.e. {{placeholders}} replaced with actual values). 
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length == 0) { return; }

      if (expressions.length > 1) {
        if ($scope.$eval(expressions.pop())) {
          waitForInterpolation = true;
        }
      }

      if (waitForInterpolation) {
        requestAnimationFrame(function checkIfInterpolated() {
          if ($element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            requestAnimationFrame(checkIfInterpolated);
          }
          else {
            evalExpressions(expressions);
          }
        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  }
}]);

Par exemple, utilisez-le comme ceci pour déclencher someFunction lorsqu'un élément est chargé et {{placeholders}} non encore remplacé:

<div when-ready="someFunction()">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction sera appelé avant que tous les espaces réservés item.property soient remplacés.

Evaluez autant d'expressions que vous le souhaitez et faites en sorte que la dernière expression true attende que {{placeholders}} soit évalué comme suit:

<div when-ready="someFunction(); anotherFunction(); true">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction et anotherFunction seront déclenchés après le remplacement de {{placeholders}}.

Cela ne fonctionne que la première fois qu'un élément est chargé, pas lors de modifications ultérieures. Cela peut ne pas fonctionner comme souhaité si un $digest continue à se produire après que les espaces réservés aient été initialement remplacés (un $ digest peut se produire jusqu'à 10 fois jusqu'à ce que les données cessent de changer). Cela conviendra à la grande majorité des cas d'utilisation.

EDIT, le 31 octobre à 19h26 HNP:

Bon, c'est probablement ma dernière et dernière mise à jour. Cela fonctionnera probablement pour 99,999 des cas d'utilisation suivants:

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. when it's done loading all sub directives and DOM
 * content). See: https://stackoverflow.com/questions/14968690/sending-event-when-angular-js-finished-loading
 *
 * Execute multiple expressions in the when-ready attribute by delimiting them
 * with a semi-colon. when-ready="doThis(); doThat()"
 *
 * Optional: If the value of a wait-for-interpolation attribute on the
 * element evaluates to true, then the expressions in when-ready will be
 * evaluated after all text nodes in the element have been interpolated (i.e.
 * {{placeholders}} have been replaced with actual values).
 *
 * Optional: Use a ready-check attribute to write an expression that
 * specifies what condition is true at any given moment in time when the
 * element is ready. The expression will be evaluated repeatedly until the
 * condition is finally true. The expression is executed with
 * requestAnimationFrame so that it fires at a moment when it is least likely
 * to block rendering of the page.
 *
 * If wait-for-interpolation and ready-check are both supplied, then the
 * when-ready expressions will fire after interpolation is done *and* after
 * the ready-check condition evaluates to true.
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;
      var hasReadyCheckExpression = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length === 0) { return; }

    if ($attributes.waitForInterpolation && $scope.$eval($attributes.waitForInterpolation)) {
        waitForInterpolation = true;
    }

      if ($attributes.readyCheck) {
        hasReadyCheckExpression = true;
      }

      if (waitForInterpolation || hasReadyCheckExpression) {
        requestAnimationFrame(function checkIfReady() {
          var isInterpolated = false;
          var isReadyCheckTrue = false;

          if (waitForInterpolation && $element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            isInterpolated = false;
          }
          else {
            isInterpolated = true;
          }

          if (hasReadyCheckExpression && !$scope.$eval($attributes.readyCheck)) { // if the ready check expression returns false
            isReadyCheckTrue = false;
          }
          else {
            isReadyCheckTrue = true;
          }

          if (isInterpolated && isReadyCheckTrue) { evalExpressions(expressions); }
          else { requestAnimationFrame(checkIfReady); }

        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  };
}]);

Utilisez-le comme ça

<div when-ready="isReady()" ready-check="checkIfReady()" wait-for-interpolation="true">
   isReady will fire when this {{placeholder}} has been evaluated
   and when checkIfReady finally returns true. checkIfReady might
   contain code like `$('.some-element').length`.
</div>

Bien sûr, il peut probablement être optimisé, mais je vais en rester là. requestAnimationFrame est gentil.

200
trusktr

Dans docs pour angular.Module , il existe une entrée décrivant la fonction run:

Utilisez cette méthode pour enregistrer le travail à effectuer lorsque l’injecteur est chargé en chargeant tous les modules.

Donc, si vous avez un module qui est votre application:

var app = angular.module('app', [/* module dependencies */]);

Vous pouvez exécuter des tâches après le chargement des modules avec:

app.run(function() {
  // Do post-load initialization stuff here
});

EDIT: Initialisation manuelle à la rescousse

Il a donc été signalé que la run ne soit pas appelée lorsque le DOM est prêt et lié. Il est appelé lorsque le $injector du module référencé par ng-app a chargé toutes ses dépendances, ce qui est distinct de l'étape de compilation DOM.

J'ai jeté un autre coup d'oeil à initialisation manuelle , et il semble que cela devrait faire l'affaire.

j'ai fait un violon pour illustrer .

Le HTML est simple:

<html>
    <body>
        <test-directive>This is a test</test-directive>
    </body>
</html>

Notez le manque de ng-app. Et j'ai une directive qui va manipuler le DOM, nous pouvons donc nous assurer de l'ordre et du timing des choses.

Comme d'habitude, un module est créé:

var app = angular.module('app', []);

Et voici la directive:

app.directive('testDirective', function() {
    return {
        restrict: 'E',
        template: '<div class="test-directive"><h1><div ng-transclude></div></h1></div>',
        replace: true,
        transclude: true,
        compile: function() {
            console.log("Compiling test-directive");
            return {
                pre: function() { console.log("Prelink"); },
                post: function() { console.log("Postlink"); }
            };
        }
    };
});

Nous allons remplacer la balise test-directive par une div de la classe test-directive, et en envelopper le contenu dans un h1.

J'ai ajouté une fonction de compilation qui renvoie à la fois les fonctions avant et après liaison afin que nous puissions voir quand ces choses fonctionnent.

Voici le reste du code:

// The bootstrapping process

var body = document.getElementsByTagName('body')[0];

// Check that our directive hasn't been compiled

function howmany(classname) {
    return document.getElementsByClassName(classname).length;
}

Avant de faire quoi que ce soit, il ne devrait y avoir aucun élément avec une classe de test-directive dans le DOM, et après cela, il devrait y en avoir un.

console.log('before (should be 0):', howmany('test-directive'));

angular.element(document).ready(function() {
    // Bootstrap the body, which loades the specified modules
    // and compiled the DOM.
    angular.bootstrap(body, ['app']);

    // Our app is loaded and the DOM is compiled
    console.log('after (should be 1):', howmany('test-directive'));
});

C'est assez simple. Lorsque le document est prêt, appelez angular.bootstrap avec l'élément racine de votre application et un tableau de noms de modules.

En fait, si vous associez une fonction run au module app , vous verrez qu'elle est exécutée avant toute compilation.

Si vous utilisez le violon et regardez la console, vous verrez ce qui suit:

before (should be 0): 0 
Compiling test-directive 
Prelink
Postlink
after (should be 1): 1 <--- success!
38
satchmorun

J'ai mis au point une solution relativement précise pour évaluer le moment où l'initialisation angular est terminée.

La directive est:

.directive('initialisation',['$rootScope',function($rootScope) {
            return {
                restrict: 'A',
                link: function($scope) {
                    var to;
                    var listener = $scope.$watch(function() {
                        clearTimeout(to);
                        to = setTimeout(function () {
                            console.log('initialised');
                            listener();
                            $rootScope.$broadcast('initialised');
                        }, 50);
                    });
                }
            };
        }]);

Cela peut alors être simplement ajouté en tant qu'attribut à l'élément body, puis écouté pour utiliser $scope.$on('initialised', fn)

Cela fonctionne en supposant que l'application est initialisée lorsqu'il n'y a plus de cycles $ digest. $ watch est appelé à chaque cycle de résumé et un minuteur est démarré (setTimeout pas $ timeout pour qu'aucun nouveau cycle de résumé ne soit déclenché). Si un cycle de résumé ne se produit pas dans le délai imparti, l'application est supposée s'être initialisée.

Elle n’est évidemment pas aussi précise que la solution satchmoruns (car il est possible qu’un cycle de digestion prenne plus de temps que le délai d’expiration), mais ma solution n’a pas besoin de vous demander de garder une trace des modules, ce qui la rend beaucoup plus facile à gérer (en particulier pour les grands projets ). Quoi qu'il en soit, semble être assez précis pour mes besoins. J'espère que ça aide.

14
Theo

Si vous utilisez Angular UI Router , vous pouvez écouter le $viewContentLoadedevent.

"$ viewContentLoaded - déclenché une fois la vue chargée, après le rendu du DOM . Le '$ scope' de la vue émet l'événement. " - Lien

$scope.$on('$viewContentLoaded', 
function(event){ ... });
10
Jordan Skole

Si vous n'utilisez pas le module ngRoute , c'est-à-dire que vous n'avez pas l'événement $ viewContentLoaded .

Vous pouvez utiliser une autre méthode de directive:

    angular.module('someModule')
        .directive('someDirective', someDirective);

    someDirective.$inject = ['$rootScope', '$timeout']; //Inject services

    function someDirective($rootScope, $timeout){
        return {
            restrict: "A",
            priority: Number.MIN_SAFE_INTEGER, //Lowest priority
            link    : function(scope, element, attr){
                $timeout(
                    function(){
                        $rootScope.$emit("Some:event");
                    }
                );
            }
        };
    }

Conformément à réponse de trusktr , sa priorité est la plus basse. De plus, $ timeout fera en sorte que Angular s'exécute dans toute une boucle d'événements avant l'exécution du rappel.

$ rootScope utilisé, car il permet de placer une directive dans n’importe quelle portée de l’application et de notifier uniquement les écouteurs nécessaires.

$ rootScope. $ emit déclenchera un événement pour tous les utilisateurs $ rootScope. $. La partie intéressante est que $ rootScope. $ Broadcast avertira tous les utilisateurs $ rootScope. $ Ainsi que $ scope. $ On listeners Source

3
Vadim P.

j'observe la manipulation DOM de angular avec JQuery et j’ai fixé la finition de mon application (une sorte de situation prédéfinie et satisfaisante dont j’ai besoin pour mon résumé d’application). Par exemple, j’attends que mon répéteur ng produise 7 résultat et là pour je vais définir une fonction d'observation à l'aide de setInterval à cet effet.

$(document).ready(function(){

  var interval = setInterval(function(){

  if($("article").size() == 7){
     myFunction();
     clearInterval(interval);
  }

  },50);

});
3
Hassan Gilak

J'avais un fragment qui était chargé après/par le partiel principal via le routage.

J'avais besoin d'exécuter une fonction après le chargement de cette sous-partie et je ne voulais pas écrire une nouvelle directive et j'ai compris que vous pouviez utiliser un effronté ngIf

Contrôleur de parent partiel:

$scope.subIsLoaded = function() { /*do stuff*/; return true; };

HTML de subpartial

<element ng-if="subIsLoaded()"><!-- more html --></element>
2
Hashbrown

Selon l'équipe Angular et this numéro de Github :

nous avons maintenant les événements $ viewContentLoaded et $ includeContentLoaded qui sont émis dans ng-view et ng-include, respectivement. Je pense que c'est aussi proche que possible de savoir quand nous en aurons fini avec la compilation.

Sur cette base, il semble que ce soit actuellement impossible à faire de manière fiable, sinon Angular aurait fourni l'événement à l'extérieur de la boîte.

Amorcer l'application implique d'exécuter le cycle de résumé sur la portée racine et il n'y a pas non plus d'événement terminé.

Selon le Angular 2 document de conception :

Il est impossible de déterminer et d’indiquer au composant que le modèle est stable, en raison de multiples condensés. En effet, la notification peut modifier davantage les données, ce qui peut relancer le processus de liaison.

Selon cela, le fait que cela n’est pas possible est l’une des raisons pour lesquelles la décision a été prise de procéder à une réécriture en Angular 2.

2
Angular University

Si vous souhaitez générer JS avec des données côté serveur (JSP, PHP), vous pouvez ajouter votre logique à un service, qui sera chargé automatiquement lors du chargement de votre contrôleur.

De plus, si vous souhaitez réagir lorsque toutes les directives sont compilées/liées, vous pouvez ajouter les solutions proposées appropriées ci-dessus dans la logique d'initialisation.

module.factory('YourControllerInitService', function() {

    // add your initialization logic here

    // return empty service, because it will not be used
    return {};
});


module.controller('YourController', function (YourControllerInitService) {
});
1
Nikolay Georgiev

peut-être que je peux vous aider par cet exemple

Dans la boîte personnalisée, je montre le contenu avec des valeurs interpolées.

dans le service, dans la méthode "ouverte" fancybox, je fais

open: function(html, $compile) {
        var el = angular.element(html);
     var compiledEl = $compile(el);
        $.fancybox.open(el); 
      }

$ compile renvoie les données compilées. vous pouvez vérifier les données compilées

0
shailendra pathak

Ce sont toutes d'excellentes solutions. Cependant, si vous utilisez actuellement le routage, j'ai trouvé cette solution comme étant la plus simple et la plus petite quantité de code nécessaire. Utilisation de la propriété 'résoudre' pour attendre la fin d'une promesse avant de déclencher l'itinéraire. par exemple.

$routeProvider
.when("/news", {
    templateUrl: "newsView.html",
    controller: "newsController",
    resolve: {
        message: function(messageService){
            return messageService.getMessage();
    }
}

})

Cliquez ici pour la documentation complète - Crédit à K. Scott Allen

0
aholtry