AngularJS: Wie kann zusätzlicher Code ausgeführt werden, nachdem AngularJS eine Vorlage gerendert hat?

182

Ich habe eine Winkelvorlage im DOM. Wenn mein Controller neue Daten von einem Dienst erhält, aktualisiert er das Modell im Bereich $ und rendert die Vorlage erneut. Alles gut soweit.

Das Problem ist, dass ich auch zusätzliche Arbeit leisten muss, nachdem die Vorlage neu gerendert wurde und sich im DOM befindet (in diesem Fall ein jQuery-Plugin).

Es scheint, als sollte es ein Ereignis geben, das man sich anhören kann, wie AfterRender, aber ich kann so etwas nicht finden. Vielleicht wäre eine Richtlinie ein guter Weg, aber sie schien auch zu früh zu feuern.

Hier ist eine jsFiddle, die mein Problem umreißt: Fiddle-AngularIssue

== UPDATE ==

Aufgrund hilfreicher Kommentare habe ich entsprechend zu einer Direktive gewechselt, um die DOM-Manipulation zu handhaben, und ein Modell $ watch in die Direktive implementiert. Ich habe jedoch immer noch das gleiche Grundproblem. Der Code innerhalb des $ watch-Ereignisses wird ausgelöst, bevor die Vorlage kompiliert und in das DOM eingefügt wurde. Daher wertet das jquery-Plugin immer eine leere Tabelle aus.

Interessanterweise funktioniert das Ganze einwandfrei, wenn ich den asynchronen Aufruf entferne. Das ist also ein Schritt in die richtige Richtung.

Hier ist meine aktualisierte Geige, um diese Änderungen widerzuspiegeln: http://jsfiddle.net/uNREn/12/

Gorebash
quelle
Dies scheint einer Frage ähnlich zu sein, die ich hatte. stackoverflow.com/questions/11444494/… . Vielleicht kann da etwas helfen.
dnc253

Antworten:

39

Dieser Beitrag ist alt, aber ich ändere Ihren Code in:

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

siehe jsfiddle .

Ich hoffe es hilft dir

dipold
quelle
Danke, das ist sehr hilfreich. Dies ist die beste Lösung für mich, da keine Dom-Manipulation vom Controller erforderlich ist und weiterhin funktioniert, wenn die Daten von einer wirklich asynchronen Dienstantwort aktualisiert werden (und ich in meinem Controller nicht $ evalAsync verwenden muss).
Gorebash
9
Wurde dies veraltet? Wenn ich das versuche, wird es tatsächlich aufgerufen, kurz bevor das DOM gerendert wird. Ist es abhängig von einem Schattendom oder so?
Shayne
Ich bin relativ neu in Angular und kann nicht sehen, wie ich diese Antwort in meinem speziellen Fall implementieren kann. Ich habe mir verschiedene Antworten angesehen und geraten, aber das funktioniert nicht. Ich bin mir nicht sicher, was die 'Watch'-Funktion sehen soll. Unsere eckige App verfügt über verschiedene Anweisungen, die von Seite zu Seite variieren. Woher weiß ich also, was ich sehen muss? Sicher gibt es eine einfache Möglichkeit, Code abzufeuern, wenn das Rendern einer Seite abgeschlossen ist?
Elchfetcher
63

Erstens sind Direktiven der richtige Ort, um sich mit dem Rendern zu beschäftigen. Mein Rat wäre, DOM-manipulierende jQuery-Plugins durch Anweisungen wie diese zu verpacken.

Ich hatte das gleiche Problem und kam mit diesem Ausschnitt. Es verwendet $watchund stellt $evalAsyncsicher, dass Ihr Code ausgeführt wird, nachdem Anweisungen wie ng-repeataufgelöst und Vorlagen wie {{ value }}gerendert wurden.

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

Ich hoffe, dies kann Ihnen helfen, einen Kampf zu verhindern.

Danijar
quelle
Dies könnte offensichtlich sein, aber wenn dies bei Ihnen nicht funktioniert, überprüfen Sie Ihre Verbindungsfunktionen / -controller auf asynchrone Operationen. Ich glaube nicht, dass $ evalAsync diese berücksichtigen kann.
LOAS
Beachten Sie, dass Sie eine linkFunktion auch mit a kombinieren können templateUrl.
Wtower
35

Befolgen Sie die Anweisungen von Misko, wenn Sie eine asynchrone Operation wünschen, statt $ timeout () (was nicht funktioniert).

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

benutze $ evalAsync () (was funktioniert)

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

Geige . Ich habe auch einen Link "Datenzeile entfernen" hinzugefügt, der $ scope.assignments ändert und eine Änderung an den Daten / dem Modell simuliert - um zu zeigen, dass das Ändern der Daten funktioniert.

Im Abschnitt " Laufzeit " der Seite "Konzeptionelle Übersicht" wird erläutert, dass evalAsync verwendet werden sollte, wenn etwas außerhalb des aktuellen Stapelrahmens auftreten muss, jedoch bevor der Browser gerendert wird. (Vermutung hier ... "aktueller Stapelrahmen" enthält wahrscheinlich Angular DOM-Updates.) Verwenden Sie $ timeout, wenn nach dem Rendern des Browsers etwas auftreten muss.

Wie Sie jedoch bereits herausgefunden haben, besteht hier meines Erachtens keine Notwendigkeit für einen asynchronen Betrieb.

Mark Rajcok
quelle
4
$scope.$evalAsync($scope.assignmentsLoaded(data));macht keinen Sinn IMO. Das Argument für $evalAsync()sollte eine Funktion sein. In Ihrem Beispiel rufen Sie die Funktion zu dem Zeitpunkt auf, zu dem eine Funktion für die spätere Ausführung registriert werden soll.
Hgoebl
@hgoebl, das Argument für $ evalAsync kann eine Funktion oder ein Ausdruck sein. In diesem Fall habe ich einen Ausdruck verwendet. Aber Sie haben Recht, der Ausdruck wird sofort ausgeführt. Ich habe die Antwort geändert, um sie für die spätere Ausführung in eine Funktion zu verpacken. Danke, dass du das verstanden hast.
Mark Rajcok
26

Ich habe festgestellt, dass die einfachste (billige und fröhliche) Lösung darin besteht, einfach eine leere Spanne mit ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" am Ende des zuletzt gerenderten Elements hinzuzufügen. Diese Funktion wird ausgeführt, wenn geprüft wird, ob das span-Element angezeigt werden soll. Führen Sie einen anderen Code in dieser Funktion aus.

Mir ist klar, dass dies nicht die eleganteste Art ist, Dinge zu tun, aber es funktioniert für mich ...

Ich hatte eine ähnliche Situation, obwohl sie leicht umgekehrt war, als ich zu Beginn einer Animation eine Ladeanzeige entfernen musste. Auf Mobilgeräten wurde der Winkel viel schneller initialisiert als die anzuzeigende Animation, und die Verwendung eines ng-Umhangs war nicht ausreichend, da die Ladeanzeige war entfernt, lange bevor echte Daten angezeigt wurden. In diesem Fall habe ich gerade die Funktion my return 0 zum ersten gerenderten Element hinzugefügt und in dieser Funktion die Variable umgedreht, die den Ladeindikator verbirgt. (Natürlich habe ich der durch diese Funktion ausgelösten Ladeanzeige ein ng-hide hinzugefügt.

fuzion9
quelle
3
Dies ist sicher ein Hack, aber es ist genau das, was ich brauchte. Erzwang, dass mein Code genau nach dem Rendern ausgeführt wird (oder fast genau). In einer idealen Welt würde ich das nicht tun, aber hier sind wir ...
Andrew
Ich brauchte den eingebauten $anchorScrollDienst, um nach dem Rendern des DOM aufgerufen zu werden, und nichts schien den Trick zu tun, außer dies. Hackish aber funktioniert.
Pat Migliaccio
ng-init ist eine bessere Alternative
Flying Gambit
1
ng-init funktioniert möglicherweise nicht immer. Zum Beispiel habe ich eine ng-Wiederholung, die dem Dom eine Reihe von Bildern hinzufügt. Wenn ich eine Funktion aufrufen möchte, nachdem jedes Bild in der ng-Wiederholung hinzugefügt wurde, muss ich ng-show verwenden.
Michael Bremerkamp
4

Schließlich fand ich die Lösung, ich verwendete einen REST-Service, um meine Sammlung zu aktualisieren. Um eine datierbare Abfrage zu konvertieren, ist der folgende Code:

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

Ich musste das ziemlich oft tun. Ich habe eine Direktive und muss einige JQuery-Sachen machen, nachdem Model-Sachen vollständig in das DOM geladen sind. Also habe ich meine Logik in den Link gesetzt: Funktion der Direktive und den Code in ein setTimeout (function () {.....}, 1) einschließen; Das setTimout wird ausgelöst, nachdem das DOM geladen wurde, und 1 Millisekunde ist die kürzeste Zeit nach dem Laden des DOM, bevor der Code ausgeführt wird. Dies scheint für mich zu funktionieren, aber ich wünschte, Angular hätte ein Ereignis ausgelöst, sobald eine Vorlage geladen wurde, damit die von dieser Vorlage verwendeten Anweisungen Abfragen ausführen und auf DOM-Elemente zugreifen können. hoffe das hilft.

mgrimmenator
quelle
2

Sie können auch eine Direktive erstellen, die Ihren Code in der Link-Funktion ausführt.

Siehe diese Stackoverflow-Antwort .

Anthony O.
quelle
2

Weder $ scope. $ EvalAsync () noch $ timeout (fn, 0) haben bei mir zuverlässig funktioniert.

Ich musste beides kombinieren. Ich habe eine Direktive erstellt und auch eine Priorität höher als den Standardwert für eine gute Maßnahme gesetzt. Hier ist eine Anweisung dafür (Hinweis: Ich verwende ngInject, um Abhängigkeiten einzufügen):

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

Um die Richtlinie aufzurufen, würden Sie Folgendes tun:

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

Wenn Sie es aufrufen möchten, nachdem eine ng-Wiederholung ausgeführt wurde, habe ich eine leere Spanne in meine ng-Wiederholung und ng-if = "$ last" eingefügt:

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

In einigen Szenarien, in denen Sie einen Dienst aktualisieren und zu einer neuen Ansicht (Seite) umleiten und dann Ihre Direktive geladen wird, bevor Ihre Dienste aktualisiert werden, können Sie $ rootScope. $ Broadcast verwenden, wenn Ihre $ watch oder $ timeout fehlschlägt

Aussicht

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

Regler

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

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

}]);

Richtlinie

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

Ich kam mit einer ziemlich einfachen Lösung. Ich bin mir nicht sicher, ob es der richtige Weg ist, aber es funktioniert im praktischen Sinne. Lassen Sie uns direkt beobachten, was gerendert werden soll. Zum Beispiel ng-repeatwürde ich in einer Direktive, die einige s enthält, auf die Länge des Textes (Sie haben vielleicht andere Dinge!) Von Absätzen oder des gesamten HTML-Codes achten. Die Richtlinie wird folgendermaßen aussehen:

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

HINWEIS : Der Code wird nach dem Rendern tatsächlich ausgeführt. Änderungen sind eher vollständig. Aber ich denke, in den meisten Fällen können Sie mit den Situationen umgehen, wenn Änderungen beim Rendern auftreten. Sie können diese pLänge (oder eine andere Kennzahl) auch mit Ihrem Modell vergleichen, wenn Sie Ihren Code nach Abschluss des Renderns nur einmal ausführen möchten . Ich freue mich über alle Gedanken / Kommentare dazu.

Ehsan88
quelle
0

Sie können das Modul 'jQuery Passthrough' der Angular-UI-Utils verwenden . Ich habe erfolgreich ein jQuery Touch-Karussell-Plugin an einige Bilder gebunden, die ich asynchron von einem Webdienst abrufe, und sie mit ng-repeat gerendert.

Michael Kork.
quelle