AngularJS: come eseguire codice aggiuntivo dopo che AngularJS ha eseguito il rendering di un modello?


182

Ho un modello angolare nel DOM. Quando il mio controller riceve nuovi dati da un servizio, aggiorna il modello in $ scope e restituisce nuovamente il modello. Tutto bene finora.

Il problema è che devo anche fare un po 'di lavoro extra dopo che il modello è stato ridistribuito ed è nel DOM (in questo caso un plugin jQuery).

Sembra che ci dovrebbe essere un evento da ascoltare, come AfterRender, ma non riesco a trovare nulla del genere. Forse una direttiva sarebbe una strada da percorrere, ma sembrava anche sparare troppo presto.

Ecco un jsFiddle che delinea il mio problema: Fiddle-AngularIssue

== AGGIORNAMENTO ==

Sulla base di utili commenti, di conseguenza sono passato a una direttiva per gestire la manipolazione del DOM e ho implementato un modello $ watch all'interno della direttiva. Tuttavia, sto ancora riscontrando lo stesso problema di base; il codice all'interno dell'evento $ watch viene generato prima che il modello sia stato compilato e inserito nel DOM, pertanto il plugin jquery valuta sempre una tabella vuota.

È interessante notare che se rimuovo la chiamata asincrona tutto funziona perfettamente, quindi è un passo nella giusta direzione.

Ecco il mio Fiddle aggiornato per riflettere queste modifiche: http://jsfiddle.net/uNREn/12/


Sembra simile a una domanda che avevo. stackoverflow.com/questions/11444494/… . Forse qualcosa lì dentro può aiutare.
dnc253,

Risposte:


39

Questo post è vecchio, ma cambio il tuo codice in:

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

vedi jsfiddle .

Spero che ti aiuti


Grazie, è molto utile. Questa è la soluzione migliore per me perché non richiede la manipolazione del dom dal controller e funzionerà comunque quando i dati vengono aggiornati da una risposta di servizio veramente asincrona (e non devo usare $ evalAsync nel mio controller).
gorebash,

9
Questo è stato deprecato? Quando provo questo, in realtà chiama appena prima che il DOM venga renderizzato. Dipende da un'ombra dom o qualcosa del genere?
Shayne,

Sono relativamente nuovo in Angular e non riesco a vedere come implementare questa risposta con il mio caso specifico. Ho esaminato varie risposte e ipotesi, ma non funziona. Non sono sicuro di ciò che dovrebbe essere la funzione 'watch'. La nostra app angolare ha diverse direttive diverse, che variano da pagina a pagina, quindi come faccio a sapere cosa "guardare"? Sicuramente c'è un modo semplice per attivare un po 'di codice quando una pagina ha finito di essere renderizzata?
Moosefetcher,

63

Innanzitutto, il posto giusto in cui pasticciare con il rendering sono le direttive. Il mio consiglio sarebbe di avvolgere DOM manipolando i plugin jQuery con direttive come questa.

Ho avuto lo stesso problema e ho trovato questo frammento. Utilizza $watche $evalAsyncper garantire l'esecuzione del codice dopo la ng-repeatrisoluzione di direttive come e {{ value }}il rendering di modelli come .

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

Spero che questo possa aiutarti a prevenire qualche lotta.


Questo potrebbe indicare l'ovvio, ma se questo non funziona per te, controlla le operazioni asincrone nelle tue funzioni / controller di collegamento. Non credo che $ evalAsync possa tenerne conto.
LOAS,

Vale la pena notare che è anche possibile combinare una linkfunzione con a templateUrl.
Wtower,

35

Seguendo il consiglio di Misko, se si desidera un'operazione asincrona, invece di $ timeout () (che non funziona)

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

usa $ evalAsync () (che funziona)

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

Violino . Ho anche aggiunto un link "rimuovi riga di dati" che modificherà $ scope.assignments, simulando una modifica ai dati / modello - per mostrare che la modifica dei dati funziona.

La sezione Runtime della pagina Panoramica concettuale spiega che evalAsync deve essere utilizzato quando è necessario che si verifichi qualcosa al di fuori del frame dello stack corrente, ma prima del rendering del browser. (Indovinare qui ... "l'attuale stack frame" probabilmente include aggiornamenti DOM angolari.) Utilizzare $ timeout se è necessario che si verifichi qualcosa dopo il rendering del browser.

Tuttavia, come hai già scoperto, non credo che ci sia bisogno di un'operazione asincrona qui.


4
$scope.$evalAsync($scope.assignmentsLoaded(data));non ha alcun senso IMO. L'argomento per $evalAsync()dovrebbe essere una funzione. Nel tuo esempio stai chiamando la funzione nel momento in cui una funzione dovrebbe essere registrata per un'esecuzione successiva.
hgoebl,

@hgoebl, l'argomento di $ evalAsync può essere una funzione o un'espressione. In questo caso ho usato un'espressione. Ma hai ragione, l'espressione viene eseguita immediatamente. Ho modificato la risposta per racchiuderla in una funzione per un'esecuzione successiva. Grazie per averlo colto.
Mark Rajcok,

26

Ho trovato la soluzione più semplice (economica e allegra) è semplicemente aggiungere un intervallo vuoto con ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" alla fine dell'ultimo elemento reso. Questa funzione verrà eseguita per verificare se l'elemento span deve essere visualizzato. Eseguire qualsiasi altro codice in questa funzione.

Mi rendo conto che questo non è il modo più elegante di fare le cose, tuttavia, funziona per me ...

Ho avuto una situazione simile, anche se leggermente invertita dove avevo bisogno di rimuovere un indicatore di caricamento quando è iniziata un'animazione, sui dispositivi mobili l'angolazione si stava inizializzando molto più velocemente dell'animazione da visualizzare e l'uso di un ng-mantello era insufficiente come l'indicatore di caricamento era rimosso ben prima che venissero visualizzati dati reali. In questo caso ho appena aggiunto la funzione my return 0 al primo elemento renderizzato e in quella funzione ho capovolto la var che nasconde l'indicatore di caricamento. (ovviamente ho aggiunto un ng-hide all'indicatore di caricamento attivato da questa funzione.


3
Questo è sicuramente un trucco, ma è esattamente quello di cui avevo bisogno. Ho costretto il mio codice ad essere eseguito esattamente dopo il rendering (o molto vicino a esattamente). In un mondo ideale non lo farei ma eccoci qui ...
Andrew,

Avevo bisogno che il $anchorScrollservizio integrato venisse chiamato dopo il rendering del DOM e nulla sembrava fare il trucco, ma questo. Hackish ma funziona.
Pat Migliaccio,

ng-init è un'alternativa migliore
Flying Gambit,

1
ng-init potrebbe non funzionare sempre. Ad esempio, ho una ripetizione ng che aggiunge un numero di immagini al dom. Se voglio chiamare una funzione dopo aver aggiunto ciascuna immagine in ng-repeat, devo usare ng-show.
Michael Bremerkamp,


4

Finalmente ho trovato la soluzione, stavo usando un servizio REST per aggiornare la mia collezione. Per convertire jquery datatable è il seguente codice:

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

3

ho dovuto farlo abbastanza spesso. Ho una direttiva e devo fare alcune cose jquery dopo che le cose del modello sono state completamente caricate nel DOM. così ho messo la mia logica nel link: funzione della direttiva e ho racchiuso il codice in un setTimeout (function () {.....}, 1); setTimout si attiva dopo il caricamento del DOM e 1 millisecondo è il tempo più breve dopo il caricamento del DOM prima dell'esecuzione del codice. questo sembra funzionare per me, ma desidero che angular abbia generato un evento una volta che un template è stato caricato in modo che le direttive utilizzate da quel template possano fare cose jquery e accedere agli elementi DOM. spero che questo ti aiuti.



2

Né $ scope. $ EvalAsync () o $ timeout (fn, 0) hanno funzionato in modo affidabile per me.

Ho dovuto unire i due. Ho fatto una direttiva e ho anche posto una priorità superiore al valore predefinito per una buona misura. Ecco una direttiva per questo (nota che uso ngInject per iniettare dipendenze):

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

Per chiamare la direttiva, devi fare questo:

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

Se vuoi chiamarlo dopo aver eseguito ng-repeat, ho aggiunto un intervallo vuoto nel mio ng-repeat e 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>

1

In alcuni scenari in cui si aggiorna un servizio e si reindirizza a una nuova vista (pagina) e quindi la direttiva viene caricata prima che i servizi vengano aggiornati, è possibile utilizzare $ rootScope. $ Broadcast se $ watch o $ timeout falliscono

Visualizza

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

controllore

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

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

}]);

Direttiva

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

1

Sono arrivato con una soluzione piuttosto semplice. Non sono sicuro che sia il modo corretto di farlo, ma funziona in senso pratico. Guardiamo direttamente ciò che vogliamo che sia reso. Ad esempio, in una direttiva che include alcuni messaggi ng-repeat, farei attenzione alla lunghezza del testo (potresti avere altre cose!) Di paragrafi o dell'intero codice HTML. La direttiva sarà così:

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

Si noti che il codice viene effettivamente eseguito dopo il rendering delle modifiche piuttosto rendering completo. Ma suppongo che nella maggior parte dei casi è possibile gestire le situazioni ogni volta che si verificano cambiamenti di rendering. Inoltre potresti pensare di confrontare questa plunghezza (o qualsiasi altra misura) con il tuo modello se vuoi eseguire il tuo codice una sola volta dopo aver completato il rendering. Apprezzo qualsiasi pensiero / commento su questo.


0

È possibile utilizzare il modulo 'jQuery Passthrough' dei programmi di utilità angular -ui . Ho collegato correttamente un plug-in carousel jQuery touch ad alcune immagini che ho recuperato asincrono da un servizio Web e le ho rese con ng-repeat.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.