AngularJS: ¿Cómo ejecutar código adicional después de que AngularJS haya generado una plantilla?

182

Tengo una plantilla angular en el DOM. Cuando mi controlador obtiene nuevos datos de un servicio, actualiza el modelo en $ scope y vuelve a representar la plantilla. Todo bien hasta ahora.

El problema es que también necesito hacer un poco de trabajo adicional después de que la plantilla se haya vuelto a procesar y esté en el DOM (en este caso, un complemento jQuery).

Parece que debería haber un evento para escuchar, como AfterRender, pero no puedo encontrar tal cosa. Tal vez una directiva sería un camino a seguir, pero también parecía dispararse demasiado pronto.

Aquí hay un jsFiddle que describe mi problema: Fiddle-AngularIssue

== ACTUALIZACIÓN ==

Basado en comentarios útiles, en consecuencia, cambié a una directiva para manejar la manipulación del DOM e implementé un modelo $ watch dentro de la directiva. Sin embargo, todavía tengo el mismo problema básico; el código dentro del evento $ watch se dispara antes de que la plantilla se haya compilado e insertado en el DOM, por lo tanto, el complemento jquery siempre evalúa una tabla vacía.

Curiosamente, si elimino la llamada asíncrona, todo funciona bien, por lo que es un paso en la dirección correcta.

Aquí está mi Fiddle actualizado para reflejar estos cambios: http://jsfiddle.net/uNREn/12/

gorebash
fuente
Esto parece similar a una pregunta que tenía. stackoverflow.com/questions/11444494/… . Quizás algo allí pueda ayudar.
dnc253

Respuestas:

39

Esta publicación es antigua, pero cambio su código a:

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

ver jsfiddle .

Espero que te ayude

dipold
fuente
Gracias, eso es muy útil. Esta es la mejor solución para mí porque no requiere la manipulación de dom del controlador, y seguirá funcionando cuando los datos se actualicen desde una respuesta de servicio verdaderamente asincrónica (y no tengo que usar $ evalAsync en mi controlador).
gorebash
9
¿Esto ha quedado en desuso? Cuando intento esto, en realidad llama justo antes de que se represente el DOM. ¿Depende de una sombra dom o algo así?
Shayne
Soy relativamente nuevo en Angular y no puedo ver cómo implementar esta respuesta con mi caso específico. He estado mirando varias respuestas y adivinando, pero eso no funciona. No estoy seguro de qué debería estar mirando la función 'watch'. Nuestra aplicación angular tiene varias directivas de ingeniería diferentes, que variarán de una página a otra, entonces, ¿cómo sé qué "mirar"? ¿Seguramente hay una manera simple de disparar algún código cuando una página ha terminado de ser renderizada?
moosefetcher
63

Primero, el lugar correcto para meterse con el renderizado son las directivas. Mi consejo sería envolver DOM manipulando complementos jQuery por directivas como esta.

Tuve el mismo problema y se me ocurrió este fragmento. Utiliza $watchy $evalAsyncpara garantizar que su código se ejecute después de que ng-repeatse hayan resuelto las directivas y se hayan {{ value }}procesado las plantillas .

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

Espero que esto pueda ayudarlo a evitar un poco de lucha.

danijar
fuente
Esto podría indicar lo obvio, pero si esto no funciona para usted, verifique las operaciones asincrónicas en sus funciones / controladores de enlace. No creo que $ evalAsync pueda tenerlos en cuenta.
LOAS
Vale la pena señalar que también puede combinar una linkfunción con a templateUrl.
Wtower
35

Siguiendo los consejos de Misko, si desea una operación asincrónica, entonces en lugar de $ timeout () (que no funciona)

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

use $ evalAsync () (que funciona)

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

Violín . También agregué un enlace "eliminar fila de datos" que modificará $ scope.assignments, simulando un cambio en los datos / modelo, para mostrar que cambiar los datos funciona.

La sección Tiempo de ejecución de la página Descripción general conceptual explica que evalAsync debe usarse cuando necesita que ocurra algo fuera del marco de la pila actual, pero antes de que se muestre el navegador. (Adivinando aquí ... el "marco de pila actual" probablemente incluye actualizaciones de Angular DOM). Use $ timeout si necesita que ocurra algo después de que se visualice el navegador.

Sin embargo, como ya descubrió, no creo que haya necesidad de una operación asincrónica aquí.

Mark Rajcok
fuente
44
$scope.$evalAsync($scope.assignmentsLoaded(data));no tiene ningún sentido IMO. El argumento para $evalAsync()debería ser una función. En su ejemplo, está llamando a la función en el momento en que una función debe registrarse para su posterior ejecución.
hgoebl
@hgoebl, el argumento de $ evalAsync puede ser una función o una expresión. En este caso usé una expresión. Pero tienes razón, la expresión se ejecuta de inmediato. Modifiqué la respuesta para envolverla en una función para su posterior ejecución. Gracias por atrapar eso.
Mark Rajcok
26

He encontrado que la solución más simple (barata y alegre) es simplemente agregar un espacio vacío con ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" al final del último elemento renderizado. Esta función se ejecutará cuando se verifique si se debe mostrar el elemento span. Ejecute cualquier otro código en esta función.

Me doy cuenta de que esta no es la forma más elegante de hacer las cosas, sin embargo, funciona para mí ...

Tuve una situación similar, aunque ligeramente invertida donde necesitaba eliminar un indicador de carga cuando comenzó una animación, en dispositivos móviles, el inicio angular era mucho más rápido que la animación que se mostraba, y el uso de una capa ng era insuficiente ya que el indicador de carga era insuficiente. eliminado mucho antes de que se muestren los datos reales. En este caso, acabo de agregar la función my return 0 al primer elemento renderizado, y en esa función volteé la var que oculta el indicador de carga. (por supuesto, agregué un ng-hide al indicador de carga activado por esta función.

fuzion9
fuente
3
Este es un truco seguro, pero es exactamente lo que necesitaba. Obligé a mi código a ejecutarse exactamente después de renderizar (o muy cerca de exactamente). En un mundo ideal no haría esto, pero aquí estamos ...
Andrew
Necesitaba $anchorScrollque se llamara al servicio integrado después de que se renderizara el DOM y nada parecía funcionar pero esto. Hackish pero funciona.
Pat Migliaccio
ng-init es una mejor alternativa
Flying Gambit
1
ng-init podría no funcionar siempre. Por ejemplo, tengo una repetición ng que agrega varias imágenes al dom. Si quiero llamar a una función después de agregar cada imagen en ng-repeat tengo que usar ng-show.
Michael Bremerkamp
4

Finalmente encontré la solución, estaba usando un servicio REST para actualizar mi colección. Para convertir la tabla de datos jquery es el siguiente código:

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

He tenido que hacer esto con bastante frecuencia. Tengo una directiva y necesito hacer algunas cosas jquery después de que las cosas del modelo estén completamente cargadas en el DOM. así que pongo mi lógica en el enlace: función de la directiva y envuelvo el código en un setTimeout (function () {.....}, 1); setTimout se activará después de cargar el DOM y 1 milisegundo es el tiempo más corto después de cargar el DOM antes de que se ejecute el código. Esto parece funcionar para mí, pero deseo que angular genere un evento una vez que se cargó una plantilla para que las directivas utilizadas por esa plantilla puedan hacer cosas jquery y acceder a elementos DOM. espero que esto ayude.

mgrimmenator
fuente
2

Ni $ scope. $ EvalAsync () ni $ timeout (fn, 0) funcionaron de manera confiable para mí.

Tuve que combinar los dos. Hice una directiva y también puse una prioridad más alta que el valor predeterminado para una buena medida. Aquí hay una directiva para ello (Nota: uso ngInject para inyectar dependencias):

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

Para llamar a la directiva, haría esto:

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

Si desea llamarlo después de que se haya ejecutado un ng-repeat, agregué un espacio vacío en mi ng-repeat y 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
fuente
1

En algunos escenarios donde actualiza un servicio y redirige a una nueva vista (página) y luego su directiva se carga antes de que se actualicen sus servicios, puede usar $ rootScope. $ Broadcast si su $ watch o $ timeout falla

Ver

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

Controlador

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

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

}]);

Directiva

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
fuente
1

Vine con una solución bastante simple. No estoy seguro de si es la forma correcta de hacerlo, pero funciona en un sentido práctico. Miremos directamente lo que queremos que se represente. Por ejemplo, en una directiva que incluye algunos ng-repeats, estaría atento a la longitud del texto (¡puede que tenga otras cosas!) De párrafos o todo el html. La directiva será así:

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

TENGA EN CUENTA que el código realmente se ejecuta después de representar los cambios en lugar de completar la representación. Pero supongo que en la mayoría de los casos puede manejar las situaciones siempre que ocurran cambios de representación. También podría pensar en comparar pla longitud de esta s (o cualquier otra medida) con su modelo si desea ejecutar su código solo una vez después de completar la representación. Agradezco cualquier pensamiento / comentario sobre esto.

Ehsan88
fuente
0

Puede usar el módulo 'jQuery Passthrough' de las utilidades angular-ui . Encontré con éxito un complemento de carrusel táctil jQuery a algunas imágenes que recupero asíncrono de un servicio web y las renderizo con ng-repeat.

Michael Kork
fuente