¿Cómo se dispara un método cuando Angular termina de agregar actualizaciones de scope al DOM?

Estoy buscando una forma de ejecutar código cuando después de agregar cambios a una variable $ scope, en este caso $ scope.results. Necesito hacer esto para llamar a algún código heredado que requiera que los artículos estén en el DOM antes de que se pueda ejecutar.

Mi código real está activando una llamada AJAX y actualizando una variable de ámbito para actualizar la interfaz de usuario. Así que actualmente mi código se está ejecutando inmediatamente después de presionar el scope, pero el código heredado está fallando porque los elementos dom no están disponibles aún.

Podría agregar un retraso desagradable con setTimeout (), pero eso no garantiza que el DOM esté realmente listo.

Mi pregunta es, ¿hay alguna forma en que pueda vincularme a un evento tipo “renderizado”?

var myApp = angular.module('myApp', []); myApp.controller("myController", ['$scope', function($scope){ var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}]; $scope.results = []; $scope.loadResults = function(){ for(var i=0; i < resultsToLoad.length; i++){ $scope.results.push(resultsToLoad[i]); } } function doneAddingToDom(){ // do something awesome like trigger a service call to log } }]); angular.bootstrap(document, ['myApp']); 

Enlace al código simulado: http://jsfiddle.net/acolchado/BhApF/5/

¡Gracias por adelantado!

La cola $ evalAsync se utiliza para progtwigr el trabajo que debe producirse fuera del marco de stack actual, pero antes de que se visualice la vista del navegador. – http://docs.angularjs.org/guide/concepts#runtime

De acuerdo, entonces, ¿qué es un “marco de stack”? Un comentario de Github revela más:

si encuelas desde un controlador, será antes, pero si entras de la directiva, será después. – https://github.com/angular/angular.js/issues/734#issuecomment-3675158

Arriba, Misko está discutiendo cuándo se ejecuta el código que se pone en cola para ejecución por $ evalAsync, en relación a cuando Angular actualiza el DOM. Sugiero leer los dos comentarios de Github antes también, para obtener el contexto completo.

Entonces, si el código se pone en cola usando $ evalAsync de una directiva , debería ejecutarse después de que Angular manipule el DOM, pero antes de que el navegador lo renderice. Si necesita ejecutar algo después de que el navegador lo renderice, o después de que un controlador actualice un modelo, use $timeout(..., 0);

Consulte también https://stackoverflow.com/a/13619324/215945 , que también tiene un violín de ejemplo que usa $ evalAsync ().

Bifurqué tu violín. http://jsfiddle.net/xGCmp/7/

Agregué una directiva llamada emitir-cuándo. Toma dos parámetros. El evento que se emitirá y la condición que se debe cumplir para que se emita el evento. Esto funciona porque cuando la función de enlace se ejecuta en la directiva, sabemos que el elemento se ha representado en el DOM. Mi solución es emitir un evento cuando se ha procesado el último elemento en ng-repeat.

Si tuviéramos una solución Angular, no recomendaría hacer esto. Es un poco hacky. Pero, podría ser una buena solución para manejar el tipo de código heredado que mencionas.

 var myApp = angular.module('myApp', []); myApp.controller("myController", ['$scope', function($scope){ var resultsToLoad = [ {id: 1, name: "one"}, {id: 2, name: "two"}, {id: 3, name: "three"} ]; function doneAddingToDom() { console.log(document.getElementById('renderedList').children.length); } $scope.results = []; $scope.loadResults = function(){ $scope.results = resultsToLoad; // If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console. doneAddingToDom(); } // If we run on doneAddingToDom here, we will find 3 list elements in the DOM. $scope.$on('allRendered', doneAddingToDom); }]); myApp.directive("emitWhen", function(){ return { restrict: 'A', link: function(scope, element, attrs) { var params = scope.$eval(attrs.emitWhen), event = params.event, condition = params.condition; if(condition){ scope.$emit(event); } } } }); angular.bootstrap(document, ['myApp']); 

Usar tiempo de espera no es la forma correcta de hacer esto. Use una directiva para agregar / manipular el DOM. Si usa el tiempo de espera, asegúrese de usar $ timeout que está enganchado en Angular (por ejemplo, devuelve una promesa).

Si eres como yo, notarás que en muchos casos $ timeout con una espera de 0 carreras mucho antes de que el DOM sea realmente estable y completamente estático. Cuando quiero que el DOM sea estable, quiero que sea estable . Y entonces la solución que he encontrado es establecer un observador en el elemento (o como en el ejemplo debajo del documento completo), para el evento “DOMSubtreeModified”. Una vez que he esperado 500 milisegundos y no ha habido cambios DOM, emití un evento como “domRendered”.

ES DECIR:

  //todo: Inject $rootScope and $window, //Every call to $window.setTimeout will use this function var broadcast = function () {}; if (document.addEventListener) { document.addEventListener("DOMSubtreeModified", function (e) { //If less than 500 milliseconds have passed, the previous broadcast will be cleared. clearTimeout(broadcast) broadcast = $window.setTimeout(function () { //This will only fire after 500 ms have passed with no changes $rootScope.$broadcast('domRendered') }, 500) }); //IE stupidity } else { document.attachEvent("DOMSubtreeModified", function (e) { clearTimeout(broadcast) broadcast = $window.setTimeout(function () { $rootScope.$broadcast('domRendered') }, 500) }); } 

Este evento se puede enganchar, como todas las transmisiones, de la siguiente manera:

 $rootScope.$on("domRendered", function(){ //do something }) 

Tenía una directiva personalizada y necesitaba la propiedad de height() resultante height() del element dentro de mi directiva, lo que significaba que necesitaba leerla después de que angular había ejecutado todo el $digest y el navegador había salido del diseño.

En la función de link de mi directiva;

Esto no funcionó de manera confiable, no lo suficientemente tarde;

 scope.$watch(function() {}); 

Esto todavía no era lo suficientemente tarde;

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

Lo siguiente parecía funcionar (incluso con 0ms en Chrome) donde curiosamente incluso ẁindow.setTimeout() con scope.$apply() ẁindow.setTimeout() no;

 $timeout(function() {}, 0); 

Sin embargo, el parpadeo era una preocupación, así que al final recurrí al uso de requestAnimationFrame() con una suspensión de $timeout dentro de mi directiva (con los prefijos de proveedor apropiados, según corresponda). Simplificado, esto esencialmente parece;

 scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) { $window.requestAnimationFrame(function() { scope.$apply(function() { scope.height = element.height(); // OK, this seems to be accurate for the layout }); }); }); 

Entonces, por supuesto, puedo usar un;

 scope.$watch("height", function() { // Adjust view model based on new layout metrics }); 

el intervalo funciona para mí, por ejemplo:

 interval = $interval(function() { if ($("#target").children().length === 0) { return; } doSomething(); $interval.cancel(interval); }, 0);