Gibt es einen Post-Rendering-Rückruf für die Angular JS-Direktive?


139

Ich habe gerade meine Anweisung erhalten, eine Vorlage einzufügen, um sie wie folgt an ihr Element anzuhängen:

# CoffeeScript
.directive 'dashboardTable', ->
  controller: lineItemIndexCtrl
  templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
  (scope, element, attrs) ->
    element.parent('table#line_items').dataTable()
    console.log 'Just to make sure this is run'

# HTML
<table id="line_items">
    <tbody dashboard-table>
    </tbody>
</table>

Ich verwende auch ein jQuery-Plugin namens DataTables. Die allgemeine Verwendung lautet wie folgt: $ ('table # some_id'). DataTable (). Sie können die JSON-Daten an den Aufruf dataTable () übergeben, um die Tabellendaten bereitzustellen, ODER Sie können die Daten bereits auf der Seite haben und den Rest erledigen. Letzteres mache ich, da die Zeilen bereits auf der HTML-Seite sind .

Das Problem ist jedoch, dass ich die dataTable () in der Tabelle # line_items AFTER DOM ready aufrufen muss. Meine obige Direktive ruft die dataTable () -Methode auf, BEVOR die Vorlage an das Element der Direktive angehängt wird. Gibt es eine Möglichkeit, Funktionen nach dem Anhängen aufzurufen?

Danke für Ihre Hilfe!

UPDATE 1 nach Andys Antwort:

Ich möchte sicherstellen, dass die Link-Methode erst aufgerufen wird, nachdem sich alles auf der Seite befindet. Deshalb habe ich die Direktive für einen kleinen Test geändert:

# CoffeeScript
#angular.module(...)
.directive 'dashboardTable', ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.find('#sayboo').html('boo')

      controller: lineItemIndexCtrl
      template: "<div id='sayboo'></div>"

    }

Und ich sehe tatsächlich "boo" im div # sayboo.

Dann versuche ich meinen jquery datatable Anruf

.directive 'dashboardTable',  ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.parent('table').dataTable() # NEW LINE

      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

Kein Glück da

Dann versuche ich eine Auszeit hinzuzufügen:

.directive 'dashboardTable', ($timeout) ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        $timeout -> # NEW LINE
          element.parent('table').dataTable()
        ,5000
      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

Und das funktioniert. Ich frage mich also, was in der Nicht-Timer-Version des Codes schief geht.


1
@adardesign Nein, ich habe es nie getan, ich musste einen Timer verwenden. Aus irgendeinem Grund ist Rückruf hier eigentlich kein Rückruf. Ich habe eine Tabelle mit 11 Spalten und Hunderten von Zeilen, daher sieht Angular natürlich wie eine gute Wahl für die Datenbindung aus. Ich muss aber auch das Plugin jquery Datatables verwenden, das so einfach wie $ ('table'). datatable () ist. Wenn ich die Direktive verwende oder nur ein dummes JSON-Objekt mit allen Zeilen habe und ng-repeat zum Iterieren verwende, kann ich mein $ (). Datatable () nicht ausführen, nachdem das HTML-Element der Tabelle gerendert wurde. Daher besteht mein Trick derzeit darin, den Timer zu aktivieren um zu überprüfen, ob $ ('tr'). Länge> 3 (b / c der Kopf- / Fußzeile)
Nik So

2
@adardesign Und ja, ich habe alle Kompilierungsmethoden ausprobiert, Kompilierungsmethode, die ein Objekt zurückgibt, das Methoden postLink / preLink enthält, Kompilierungsmethode, die nur eine Funktion (nämlich die Verknüpfungsfunktion) zurückgibt, Verknüpfungsmethode (ohne die Kompilierungsmethode, weil, soweit ich das beurteilen kann, Wenn Sie eine Kompilierungsmethode haben, die eine Verknüpfungsmethode zurückgibt, wird die Verknüpfungsfunktion ignoriert. Keine hat funktioniert. Sie müssen sich also auf ein gutes altes $ timeout verlassen. Wird diesen Beitrag aktualisieren, wenn ich etwas finde, das besser funktioniert, oder einfach, wenn ich finde, dass der Rückruf wirklich wie ein Rückruf wirkt
Nik So

Antworten:


215

Wenn der zweite Parameter "Verzögerung" nicht angegeben ist, wird die Funktion standardmäßig ausgeführt, nachdem das DOM das Rendern abgeschlossen hat. Verwenden Sie also anstelle von setTimeout $ timeout:

$timeout(function () {
    //DOM has finished rendering
});

8
Warum wird es nicht in den Dokumenten erklärt ?
Gaui

23
Du hast recht, meine Antwort ist etwas irreführend, weil ich versucht habe, es einfach zu machen. Die vollständige Antwort lautet, dass dieser Effekt nicht auf Angular zurückzuführen ist, sondern auf den Browser. $timeout(fn)Letztendlich werden Aufrufe ausgeführt, setTimeout(fn, 0)wodurch die Ausführung von Javascript unterbrochen wird und der Browser den Inhalt zuerst rendern kann, bevor die Ausführung dieses Javascript fortgesetzt wird.
Parlament

7
Stellen Sie sich den Browser so vor, als würden bestimmte Aufgaben wie "Javascript-Ausführung" und "DOM-Rendering" separat in die Warteschlange gestellt. Mit setTimeout (fn, 0) wird die aktuell ausgeführte "Javascript-Ausführung" nach dem Rendern in den hinteren Bereich der Warteschlange verschoben .
Parlament

2
@GabLeRoux yup, das hat den gleichen Effekt, außer dass $ timeout den zusätzlichen Vorteil hat, $ scope aufzurufen. $ Apply (), nachdem es ausgeführt wird. Mit _.defer () müssen Sie es manuell aufrufen, wenn myFunction Variablen im Bereich ändert.
Parlament

2
Ich habe ein Szenario, in dem dies nicht hilft, wenn auf Seite 1 ng-repeat eine Reihe von Elementen rendert, dann gehe ich zu Seite 2 und dann gehe ich zurück zu Seite 1 und versuche, den Höchstwert der übergeordneten ng-repeat-Elemente zu erreichen ... es gibt die falsche Höhe zurück. Wenn ich eine Zeitüberschreitung von 1000 ms mache, funktioniert es.
Jodalr

14

Ich hatte das gleiche Problem und ich glaube, die Antwort ist wirklich nein. Siehe Miškos Kommentar und einige Diskussionen in der Gruppe .

Angular kann nachverfolgen, dass alle Funktionsaufrufe zur Manipulation des DOM abgeschlossen sind. Da diese Funktionen jedoch eine asynchrone Logik auslösen können, die das DOM nach ihrer Rückkehr immer noch aktualisiert, kann nicht erwartet werden, dass Angular davon erfährt. Jeder Rückruf Angular gibt vielleicht arbeitet manchmal, aber nicht sicher sein zu vertrauen.

Wir haben dies heuristisch mit einem setTimeout gelöst, wie Sie es getan haben.

(Bitte denken Sie daran, dass nicht jeder mit mir einverstanden ist - Sie sollten die Kommentare zu den obigen Links lesen und sehen, was Sie denken.)


7

Sie können die 'Link'-Funktion verwenden, die auch als postLink bezeichnet wird und nach dem Einfügen der Vorlage ausgeführt wird.

app.directive('myDirective', function() {
  return {
    link: function(scope, elm, attrs) { /*I run after template is put in */ },
    template: '<b>Hello</b>'
  }
});

Lesen Sie dies, wenn Sie Richtlinien erstellen möchten. Dies ist eine große Hilfe: http://docs.angularjs.org/guide/directive


Hallo Andy, vielen Dank für die Antwort. Ich habe die Link-Funktion ausprobiert, aber es würde mir nichts ausmachen, sie noch einmal genau so zu versuchen, wie Sie sie codieren. Ich habe die letzten 1,5 Tage damit verbracht, auf dieser Direktivenseite nachzulesen. und schauen Sie sich auch die Beispiele auf der Website von Angular an. Ich werde jetzt deinen Code ausprobieren.
Nik So

Ah, ich verstehe jetzt, dass Sie versucht haben, einen Link zu erstellen, aber Sie haben es falsch gemacht. Wenn Sie nur eine Funktion zurückgeben, wird davon ausgegangen, dass es sich um eine Verknüpfung handelt. Wenn Sie ein Objekt zurückgeben, müssen Sie es mit dem Schlüssel als 'Link' zurückgeben. Sie können auch eine Verknüpfungsfunktion von Ihrer Kompilierungsfunktion zurückgeben.
Andrew Joslin

Hallo Andy, habe meine Ergebnisse zurückbekommen; Ich habe fast meinen Verstand verloren, weil ich wirklich getan habe, was Ihre Antwort hier ist. Bitte sehen Sie mein Update
Nik So

Humm, versuchen Sie etwas wie: <table id = "bob"> <tbody dashboard-table = "# bob"> </ tbody> </ table> Führen Sie dann in Ihrem Link $ (attrs.dashboardTable) .dataTable () to aus Stellen Sie sicher, dass es richtig ausgewählt ist. Oder ich denke, Sie haben das bereits versucht. Ich bin mir wirklich nicht sicher, ob der Link nicht funktioniert.
Andrew Joslin

Dieser hat für mich funktioniert, ich wollte Elemente innerhalb des Dom-Post-Renderings der Vorlage für meine Anforderung verschieben, habe das in der Link-Funktion
getan. Danke

7

Obwohl sich meine Antwort nicht auf Datentabellen bezieht, befasst sie sich mit dem Problem der DOM-Manipulation und z. B. der Initialisierung des jQuery-Plugins für Anweisungen, die für Elemente verwendet werden, deren Inhalt asynchron aktualisiert wurde.

Anstatt eine Zeitüberschreitung zu implementieren, könnte man einfach eine Uhr hinzufügen, die Inhaltsänderungen (oder sogar zusätzliche externe Auslöser) abhört.

In meinem Fall habe ich diese Problemumgehung zum Initialisieren eines jQuery-Plugins verwendet, nachdem die ng-Wiederholung durchgeführt wurde, wodurch mein inneres DOM erstellt wurde. In einem anderen Fall habe ich es nur zum Bearbeiten des DOM verwendet, nachdem die Eigenschaft scope am Controller geändert wurde. So habe ich es gemacht ...

HTML:

<div my-directive my-directive-watch="!!myContent">{{myContent}}</div>

JS:

app.directive('myDirective', [ function(){
    return {
        restrict : 'A',
        scope : {
            myDirectiveWatch : '='
        },
        compile : function(){
            return {
                post : function(scope, element, attributes){

                    scope.$watch('myDirectiveWatch', function(newVal, oldVal){
                        if (newVal !== oldVal) {
                            // Do stuff ...
                        }
                    });

                }
            }
        }
    }
}]);

Hinweis: Anstatt nur die Variable myContent mit dem Attribut my-directive-watch in bool umzuwandeln, könnte man sich dort einen beliebigen Ausdruck vorstellen.

Hinweis: Das Isolieren des Bereichs wie im obigen Beispiel kann nur einmal pro Element durchgeführt werden. Wenn Sie dies mit mehreren Anweisungen für dasselbe Element tun, wird ein $ compile: multidir-Fehler angezeigt - siehe: https://docs.angularjs.org / error / $ compile / multidir


7

Möglicherweise bin ich zu spät, um diese Frage zu beantworten. Trotzdem kann jemand von meiner Antwort profitieren.

Ich hatte ein ähnliches Problem und in meinem Fall kann ich die Direktive nicht ändern, da es sich um eine Bibliothek handelt und das Ändern eines Codes der Bibliothek keine gute Praxis ist. Also habe ich eine Variable verwendet, um auf das Laden der Seite zu warten, und ng-if in meinem HTML-Code verwendet, um auf das Rendern des jeweiligen Elements zu warten.

In meinem Controller:

$scope.render=false;

//this will fire after load the the page

angular.element(document).ready(function() {
    $scope.render=true;
});

In meinem HTML (in meinem Fall ist die HTML-Komponente eine Zeichenfläche)

<canvas ng-if="render"> </canvas>

3

Ich hatte das gleiche Problem, aber ich verwendete Angular + DataTable mit einer fnDrawCallback+ Zeilengruppierung + $ kompilierten verschachtelten Anweisungen. Ich habe das $ timeout in meine fnDrawCallbackFunktion eingefügt, um das Paginierungs-Rendering zu korrigieren.

Vorheriges Beispiel, basierend auf der Quelle row_grouping:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  for(var i=0; i<nTrs.length; i++){
     //1. group rows per row_grouping example
     //2. $compile html templates to hook datatable into Angular lifecycle
  }
}

Nach dem Beispiel:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  $timeout(function requiredRenderTimeoutDelay(){
    for(var i=0; i<nTrs.length; i++){
       //1. group rows per row_grouping example
       //2. $compile html templates to hook datatable into Angular lifecycle
    }
  ,50); //end $timeout
}

Schon eine kurze Timeout-Verzögerung reichte aus, um Angular das Rendern meiner kompilierten Angular-Direktiven zu ermöglichen.


Nur neugierig, haben Sie einen ziemlich großen Tisch mit vielen Spalten? weil ich festgestellt habe, dass ich eine Menge nerviger Millisekunden (> 100) brauche, damit die dataTable () nicht erstickt
Nik So

Ich habe festgestellt, dass das Problem bei der Navigation auf der DataTable- Seite für Ergebnismengen von 2 bis über 150 Zeilen aufgetreten ist. Also, nein - ich glaube nicht, dass die Größe der Tabelle das Problem war, aber vielleicht hat DataTable genug Rendering-Overhead hinzugefügt, um einige dieser Millisekunden wegzukauen. Mein Fokus lag darauf, dass die Zeilengruppierung in DataTable mit minimaler AngularJS-Integration funktioniert.
JJ Zabkar

2

Keine der für mich funktionierenden Lösungen akzeptiert die Verwendung eines Timeouts. Dies liegt daran, dass ich eine Vorlage verwendet habe, die während des postLink dynamisch erstellt wurde.

Beachten Sie jedoch, dass es eine Zeitüberschreitung von '0' geben kann, da die Zeitüberschreitung die aufgerufene Funktion zur Warteschlange des Browsers hinzufügt, die nach der Winkel-Rendering-Engine auftritt, da sich diese bereits in der Warteschlange befindet.

Siehe hierzu: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering


0

Hier ist eine Anweisung, Aktionen nach einem flachen Render programmieren zu lassen. Mit flach meine ich, dass es nach genau diesem gerenderten Element ausgewertet wird und dass es nichts damit zu tun hat, wann sein Inhalt gerendert wird. Wenn Sie also ein Unterelement benötigen, das eine Post-Rendering-Aktion ausführt, sollten Sie es dort verwenden:

define(['angular'], function (angular) {
  'use strict';
  return angular.module('app.common.after-render', [])
    .directive('afterRender', [ '$timeout', function($timeout) {
    var def = {
        restrict : 'A', 
        terminal : true,
        transclude : false,
        link : function(scope, element, attrs) {
            if (attrs) { scope.$eval(attrs.afterRender) }
            scope.$emit('onAfterRender')
        }
    };
    return def;
    }]);
});

dann können Sie tun:

<div after-render></div>

oder mit einem nützlichen Ausdruck wie:

<div after-render="$emit='onAfterThisConcreteThingRendered'"></div>


Dies ist nicht wirklich, nachdem der Inhalt gerendert wurde. Wenn ich zu diesem Zeitpunkt einen Ausdruck im Element <div after-render> {{blah}} </ div> hatte, wird der Ausdruck noch nicht ausgewertet. Der Inhalt des div befindet sich immer noch {{blah}} in der Link-Funktion. Technisch gesehen lösen Sie das Ereignis aus, bevor der Inhalt gerendert wird.
Edward Olamisan

Dies ist eine flache After-Render-Aktion, ich habe nie behauptet, sie sei tief
Sebastian Sastre

0

Ich habe dies mit der folgenden Direktive zum Laufen gebracht:

app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});

Und im HTML:

<table class="table table-hover dataTable dataTable-columnfilter " datatable-setup="">

Fehlerbehebung, wenn das oben genannte für Sie nicht funktioniert.

1) Beachten Sie, dass 'datatableSetup' dem 'datatable-setup' entspricht. Angular ändert das Format in Kameletui.

2) Stellen Sie sicher, dass die App vor der Direktive definiert ist. zB einfache App Definition und Direktive.

var app = angular.module('app', []);
app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});

0

Da die Ladereihenfolge nicht vorhersehbar ist, kann eine einfache Lösung verwendet werden.

Schauen wir uns die Beziehung zwischen Richtlinie und Benutzer der Richtlinie an. Normalerweise liefert der Benutzer der Direktive einige Daten an die Direktive oder verwendet einige Funktionen, die die Direktive bereitstellt. Die Richtlinie erwartet andererseits, dass einige Variablen in ihrem Geltungsbereich definiert werden.

Wenn wir sicherstellen können, dass alle Spieler alle ihre Aktionsanforderungen erfüllt haben, bevor sie versuchen, diese Aktionen auszuführen, sollte alles in Ordnung sein.

Und jetzt die Richtlinie:

app.directive('aDirective', function () {
    return {
        scope: {
            input: '=',
            control: '='
        },
        link: function (scope, element) {
            function functionThatNeedsInput(){
                //use scope.input here
            }
            if ( scope.input){ //We already have input 
                functionThatNeedsInput();
            } else {
                scope.control.init = functionThatNeedsInput;
            }
          }

        };
})

und jetzt der Benutzer der Direktive HTML

<a-directive control="control" input="input"></a-directive>

und irgendwo in der Steuerung der Komponente, die die Direktive verwendet:

$scope.control = {};
...
$scope.input = 'some data could be async';
if ( $scope.control.functionThatNeedsInput){
    $scope.control.functionThatNeedsInput();
}

Das ist alles. Es gibt viel Overhead, aber Sie können das $ Timeout verlieren. Wir gehen auch davon aus, dass die Komponente, die die Direktive verwendet, vor der Direktive instanziiert wird, da wir von der Steuervariablen abhängen, die bei der Instanziierung der Direktive vorhanden ist.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.