AngularJS: comment exécuter du code supplémentaire après AngularJS a rendu un modèle?
J'ai un modèle angulaire dans le DOM. Lorsque mon contrôleur obtient de nouvelles données à partir d'un service, il met à jour le modèle dans le $scope et restitue le modèle. Tout bon jusqu'ici.
Le problème est que je dois également faire un travail supplémentaire après que le modèle a été rendu à nouveau et est dans le DOM (dans ce cas, un plugin jQuery).
Il semble qu'il devrait y avoir un événement à écouter, comme Aferrender, mais je ne peux pas trouver une telle chose. Peut-être qu'une directive serait une solution, mais il semblait tirer trop tôt aussi.
Voici un jsFiddle décrivant mon problème: Violon AngularIssue
== mise à JOUR ==
Sur la base de commentaires utiles, je suis donc passé à une directive pour gérer la manipulation DOM, et mis en œuvre 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 ait été compilé et inséré dans le DOM, par conséquent, le jquery le plugin évalue toujours une table vide.
Fait intéressant, si je supprime l'appel asynchrone, tout fonctionne bien, donc c'est un pas dans la bonne direction.
Voici mon violon mis à jour pour refléter ces changements: http://jsfiddle.net/uNREn/12/
12 réponses
Tout d'abord, le bon endroit pour jouer avec le rendu sont les directives. Mon conseil serait d'envelopper 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 $watch
et $evalAsync
pour s'assurer que votre code s'exécute après que des directives comme ng-repeat
ont été résolues et que des modèles comme {{ 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 une lutte.
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.affectations, simulant une modification des données / modèle-pour montrer que la modification des données fonctionne.
La section Runtime de la page de présentation conceptuelle explique que evalAsync doit être utilisé lorsque vous avez besoin de se produit en dehors du cadre de pile actuel, mais avant le rendu du navigateur. (Deviner ici... "cadre de pile actuel" inclut probablement des mises à jour DOM angulaires.) 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 qu'il y ait besoin d'une opération asynchrone ici.
J'ai trouvé la solution la plus simple (bon marché et joyeuse) est simplement d'ajouter un span vide avec ng-show = "someFunctionThatAlwaysReturnsZeroornothing()" à la fin du dernier élément rendu. Cette fonction sera exécutée quand vérifier si l'élément span doit être affiché. Exécutez n'importe quel 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 là où j'en avais besoin supprimer un indicateur de chargement lorsqu'une animation a commencé, 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 l'affichage des données réelles. Dans ce cas, je viens d'ajouter la fonction my return 0 au premier élément rendu, et dans cette fonction, j'ai retourné le var qui cache l'indicateur de chargement. (bien sûr, j'ai ajouté un ng-hide à l'indicateur de chargement déclenché par ceci fonction.
Je pense que vous recherchez $ evalAsync http://docs.angularjs.org/api/ng. $ rootScope. Scope# $ evalAsync
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();
});
});
J'ai dû le faire assez souvent. j'ai une directive et j'ai besoin de faire des choses jquery après que les choses du modèle soient complètement chargées dans le DOM. donc, je mets ma logique dans le lien: fonction de la directive et envelopper le code dans un setTimeout (function () {..... }, 1); le setTimout se déclenchera après le chargement du DOM et 1 miliseconde est le temps le plus court après le chargement du DOM avant l'exécution du code. cela semble fonctionner pour moi mais je souhaite qu'angular déclenche un événement une fois qu'un modèle a été chargé pour que les directives utilisées par ce modèle puissent faire des choses jquery et accéder aux éléments DOM. espérons que cette aide.
Vous pouvez également créer une directive qui exécute votre code dans la fonction link.
Voir cette réponse stackoverflow .
Ni $champ d'application.$evalAsync () ou $timeout (fn, 0) a fonctionné de manière fiable pour moi.
Je devais 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 faire 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'un ng-repeat, j'ai ajouté un span vide dans Mes 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>
Je suis venu avec une solution assez simple. Je ne suis pas sûr que ce soit la bonne façon de le faire, mais cela fonctionne dans un sens pratique. Regardons directement ce que nous voulons être rendus. Par exemple, dans une directive qui inclut certains ng-repeat
s, je ferais attention à la longueur du texte (vous pouvez avoir d'autres choses!) des paragraphes ou du html entier. 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 {[7] } que le code s'exécute réellement après modifications de rendu 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. Aussi, vous pourriez penser à comparer cette longueur de p
(ou toute autre mesure) avec votre modèle si vous voulez exécuter votre code seulement Une fois après le rendu terminé. j'apprécie toutes les pensées / commentaires à ce sujet.
Vous pouvez utiliser le module 'jQuery Passthrough' des utilitaires angular-ui . J'ai réussi à lier un plugin jQuery touch carousel à certaines images que je récupère asynchrone à partir d'un service web et les rend avec ng-repeat.
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.$diffusion si votre $regarder ou $timeout échoue
Vue
<service-history log="log" data-ng-repeat="log in requiedData"></service-history>
Contrôleur
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);
}
};
});