esiste un callback post rendering per la direttiva JS angolare?


139

Ho appena ottenuto la mia direttiva per inserire un modello da aggiungere al suo elemento in questo modo:

# 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>

Sto anche usando un plugin jQuery chiamato DataTables. Il suo uso generale è così: $ ('table # some_id'). DataTable (). Puoi trasferire i dati JSON nella chiamata dataTable () per fornire i dati della tabella OPPURE puoi avere i dati già nella pagina e farà il resto. Sto facendo quest'ultimo. Le righe sono già nella pagina HTML .

Ma il problema è che devo chiamare dataTable () nella tabella # line_items DOPO DOM pronto. La mia direttiva sopra chiama il metodo dataTable () PRIMA che il modello sia aggiunto all'elemento della direttiva. Esiste un modo per chiamare le funzioni DOPO l'appendice?

Grazie per l'aiuto!

AGGIORNAMENTO 1 dopo la risposta di Andy:

Voglio assicurarmi che il metodo di collegamento venga chiamato solo DOPO che tutto è sulla pagina, quindi ho modificato la direttiva per un piccolo test:

# 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>"

    }

E in effetti vedo "boo" nel div # sayboo.

Quindi provo la mia chiamata databile jquery

.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') %>"
    }

Nessuna fortuna lì

Quindi provo ad aggiungere un timeout:

.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') %>"
    }

E questo funziona. Quindi mi chiedo cosa vada storto nella versione non timer del codice?


1
@adardesign No, non l'ho mai fatto, ho dovuto usare un timer. Per qualche ragione, il callback non è un callback qui, davvero. Ho una tabella con 11 colonne e 100 di righe, quindi naturalmente angolare sembra una buona scommessa da utilizzare per l'associazione dei dati; ma ho anche bisogno di usare il plugin jquery Datatables che è semplice come $ ('table'). datatable (). Utilizzando la direttiva o semplicemente avere un oggetto json stupido con tutte le righe e usare ng-repeat per iterare, non riesco a far funzionare $ (). Datatable () DOPO che l'elemento html della tabella viene visualizzato, quindi il mio trucco è attualmente quello di timer per verificare se $ ('tr'). lunghezza> 3 (b / c di intestazione / piè di pagina)
Nik So

2
@adardesign E sì, ho provato tutti i metodi di compilazione, il metodo di compilazione restituendo un oggetto contenente i metodi postLink / preLink, il metodo di compilazione che restituisce solo una funzione (vale a dire la funzione di collegamento), il metodo di collegamento (senza il metodo di compilazione perché, per quanto posso dire, se si dispone di un metodo di compilazione che restituisce un metodo di collegamento, la funzione di collegamento viene ignorata). Nessuno ha funzionato, quindi è necessario fare affidamento sul buon vecchio timeout $. Aggiornerò questo post se trovo qualcosa che funzioni meglio o semplicemente quando trovo che il callback si comporta davvero come il callback
Nik So

Risposte:


215

Se non viene fornito il secondo parametro, "ritardo", il comportamento predefinito consiste nell'eseguire la funzione dopo che il DOM ha completato il rendering. Quindi, invece di setTimeout, usa $ timeout:

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

8
Perché non è spiegato nei documenti ?
Gaui,

23
Hai ragione, la mia risposta è un po 'fuorviante perché ho cercato di renderlo semplice. La risposta completa è che questo effetto non è il risultato di Angular ma piuttosto del browser. $timeout(fn)infine chiama setTimeout(fn, 0)che ha l'effetto di interrompere l'esecuzione di Javascript e di consentire al browser di visualizzare prima il contenuto, prima di continuare l'esecuzione di quel Javascript.
parlamento

7
Pensa al browser come ad una coda di alcune attività come "esecuzione javascript" e "rendering DOM" separatamente, e quale setTimeout (fn, 0) spinge la "esecuzione javascript" attualmente in esecuzione sul retro della coda, dopo il rendering .
parlamento

2
@GabLeRoux yup, che avrà lo stesso effetto tranne $ timeout ha l'ulteriore vantaggio di chiamare $ scope. $ Apply () dopo l'esecuzione. Con _.defer () dovrai chiamarlo manualmente se myFunction modifica le variabili nell'ambito.
parlamento,

2
Sto avendo uno scenario in cui questo non aiuta in cui a pagina1 ng-repeat esegue il rendering del gruppo di elementi, quindi vado a pagina2 e poi torno a pagina1 e provo a ottenere il massimo di elementi ng-repeat padre ... restituisce l'altezza sbagliata. Se eseguo il timeout per circa 1000 ms, allora funziona.
yodalr,

14

Ho avuto lo stesso problema e credo che la risposta sia davvero no. Vedi il commento di Miško e alcune discussioni nel gruppo .

Angular può tracciare che tutte le chiamate di funzione che effettua per manipolare il DOM sono complete, ma dal momento che tali funzioni potrebbero innescare la logica asincrona che sta ancora aggiornando il DOM dopo il loro ritorno, Angular non ci si può aspettare che ne sia a conoscenza. Qualsiasi callback angolare può funzionare a volte, ma non è sicuro fare affidamento su.

L'abbiamo risolto euristicamente con un setTimeout, come hai fatto tu.

(Tieni presente che non tutti sono d'accordo con me: dovresti leggere i commenti sui link sopra e vedere cosa ne pensi.)


7

È possibile utilizzare la funzione 'link', nota anche come postLink, che viene eseguita dopo l'inserimento del modello.

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

Dagli una lettura se hai intenzione di fare direttive, è di grande aiuto: http://docs.angularjs.org/guide/directive


Ciao Andy, grazie mille per aver risposto; Ho provato la funzione di collegamento, ma non mi dispiacerebbe riprovare esattamente mentre la codice; Ho passato gli ultimi 1,5 giorni a leggere quella pagina della direttiva; e guardando anche gli esempi sul sito di angular. Ora proverò il tuo codice.
Nik So,

Ah, vedo che ora stavi cercando di creare un link ma lo stavi facendo male. Se si restituisce semplicemente una funzione, si presume che sia un collegamento. Se si restituisce un oggetto, è necessario restituirlo con la chiave come "collegamento". È inoltre possibile restituire una funzione di collegamento dalla funzione di compilazione.
Andrew Joslin,

Ciao Andy, ho ottenuto i miei risultati; Ho quasi perso la sanità mentale, perché ho praticamente fatto davvero la tua risposta qui. Si prega di consultare il mio aggiornamento
Nik So,

Humm, prova qualcosa del tipo: <table id = "bob"> <tbody dashboard-table = "# bob"> </tbody> </table> Quindi nel tuo link, fai $ (attrs.dashboardTable) .dataTable () a assicurati che sia selezionato correttamente. O immagino tu l'abbia già provato .. Non sono davvero sicuro che il link non funzioni.
Andrew Joslin,

Questo ha funzionato per me, volevo spostare elementi all'interno del rendering dom post del modello per il mio requisito, lo ha fatto nella funzione di collegamento.Grazie
abhi,

7

Sebbene la mia risposta non sia correlata ai database, affronta il problema della manipolazione del DOM e, ad esempio, dell'inizializzazione del plugin jQuery per le direttive utilizzate su elementi che hanno i loro contenuti aggiornati in modo asincrono.

Invece di implementare un timeout, si potrebbe semplicemente aggiungere un orologio che ascolterà le modifiche al contenuto (o anche ulteriori trigger esterni).

Nel mio caso ho usato questa soluzione alternativa per inizializzare un plug-in jQuery una volta eseguita la ripetizione ng che ha creato il mio DOM interno - in un altro caso l'ho usato solo per manipolare il DOM dopo che la proprietà scope era stata modificata sul controller. Ecco come ho fatto ...

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

                }
            }
        }
    }
}]);

Nota: Invece di lanciare semplicemente la variabile myContent per booleare l'attributo my-direttiva-watch, si può immaginare qualsiasi espressione arbitraria lì.

Nota: L' isolamento dell'ambito come nell'esempio precedente può essere eseguito una sola volta per elemento: tentare di farlo con più direttive sullo stesso elemento comporterà un errore $ compile: multidir - consultare: https://docs.angularjs.org / error / $ di compilazione / MULTIDIR


7

Potrebbe essere in ritardo per rispondere a questa domanda. Ma qualcuno potrebbe trarre beneficio dalla mia risposta.

Ho avuto un problema simile e nel mio caso non posso cambiare la direttiva poiché è una libreria e cambiare un codice della libreria non è una buona pratica. Quindi quello che ho fatto è stato usare una variabile per attendere il caricamento della pagina e usare ng-if all'interno del mio html per attendere il rendering dell'elemento particolare.

Nel mio controller:

$scope.render=false;

//this will fire after load the the page

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

Nel mio HTML (nel mio caso il componente HTML è una tela)

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

3

Ho avuto lo stesso problema, ma utilizzando angolare + DataTable con un fnDrawCallback+ fila raggruppamento + $ direttive nidificate compilati. Ho inserito $ timeout nella mia fnDrawCallbackfunzione per correggere il rendering dell'impaginazione.

Prima dell'esempio, basato sull'origine 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
  }
}

Dopo esempio:

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
}

Anche un breve ritardo è stato sufficiente per consentire ad Angular di eseguire il rendering delle mie direttive angolari compilate.


Solo curioso, hai una tabella piuttosto grande con molte colonne? perché ho scoperto che ho bisogno di molti fastidiosi millisecondi (> 100), quindi non lasciare che la dataTable () chiami per soffocare
Nik So,

Ho riscontrato che il problema si verificava nella navigazione della pagina DataTable per set di risultati da 2 righe a oltre 150 righe. Quindi no: non credo che la dimensione della tabella sia stata il problema, ma forse DataTable ha aggiunto un overhead di rendering sufficiente per masticare alcuni di quei millisecondi. Il mio obiettivo era quello di far funzionare il raggruppamento di righe in DataTable con una minima integrazione di AngularJS.
JJ Zabkar,

2

Nessuna delle soluzioni ha funzionato per me accetta di usare un timeout. Questo perché stavo usando un modello creato dinamicamente durante il postLink.

Si noti tuttavia che può esserci un timeout di '0' poiché il timeout aggiunge la funzione chiamata alla coda del browser che si verificherà dopo il motore di rendering angolare poiché questo è già nella coda.

Fare riferimento a questo: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering


0

Ecco una direttiva per avere azioni programmate dopo un rendering superficiale. Con superficiale intendo che valuterà dopo quel vero elemento reso e che non sarà correlato a quando il suo contenuto verrà reso. Quindi, se hai bisogno di qualche elemento secondario che fa un'azione post render, dovresti considerare di usarlo lì:

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

allora puoi fare:

<div after-render></div>

o con qualsiasi espressione utile come:

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


Questo non è realmente dopo il rendering del contenuto. Se avessi un'espressione all'interno dell'elemento <div after-render> {{blah}} </div> a questo punto l'espressione non viene ancora valutata. Il contenuto del div è ancora {{blah}} all'interno della funzione di collegamento. Quindi tecnicamente stai lanciando l'evento prima che il contenuto venga reso.
Edward Olamisan,

Questa è un'azione superficiale dopo il rendering, non ho mai preteso che fosse profonda
Sebastian Sastre,

0

Ho ottenuto questo lavoro con la seguente direttiva:

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

E in HTML:

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

risoluzione dei problemi se quanto sopra non funziona per te.

1) notare che 'datatableSetup' è l'equivalente di 'datatable-setup'. Angolare cambia il formato in custodia color cammello.

2) assicurarsi che l'app sia definita prima della direttiva. ad es. semplice definizione e direttiva dell'app.

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

0

A causa del fatto che l'ordine di caricamento non può essere anticipato, è possibile utilizzare una soluzione semplice.

Diamo un'occhiata al rapporto direttiva-utente della direttiva. Di solito l'utente della direttiva fornirà alcuni dati alla direttiva o utilizzerà alcune funzionalità (funzioni) fornite dalla direttiva. La direttiva prevede invece che alcune variabili siano definite nel suo ambito di applicazione.

Se possiamo assicurarci che tutti i giocatori abbiano soddisfatto tutti i loro requisiti di azione prima di tentare di eseguire quelle azioni, tutto dovrebbe andare bene.

E ora la direttiva:

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

        };
})

e ora l'utente della direttiva html

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

e da qualche parte nel controller del componente che utilizza la direttiva:

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

Questo è tutto. C'è un sacco di spese generali ma puoi perdere il timeout di $. Supponiamo inoltre che il componente che utilizza la direttiva sia istanziato prima della direttiva perché dipendiamo dalla variabile di controllo che esiste quando la direttiva viene istanziata.

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.