czy istnieje wywołanie zwrotne po renderowaniu dla dyrektywy Angular JS?

139

Właśnie otrzymałem dyrektywę, aby pobrać szablon, aby dołączyć do jego elementu w następujący sposób:

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

Używam również wtyczki jQuery o nazwie DataTables. Jego ogólne użycie jest takie: $ ('table # some_id'). DataTable (). Możesz przekazać dane JSON do wywołania dataTable (), aby dostarczyć dane tabeli LUB możesz mieć dane już na stronie i zrobi resztę .. Robię to drugie, mając wiersze już na stronie HTML .

Ale problem polega na tym, że muszę wywołać funkcję dataTable () w tabeli # line_items PO gotowym DOM. Moja dyrektywa powyżej wywołuje metodę dataTable () ZANIM szablon zostanie dołączony do elementu dyrektywy. Czy istnieje sposób, w jaki mogę wywoływać funkcje PO dołączeniu?

Dziękuję za pomoc!

UPDATE 1 po odpowiedzi Andy'ego:

Chcę się upewnić, że metoda linku zostanie wywołana tylko PO tym, jak wszystko jest na stronie, więc zmieniłem dyrektywę na krótki 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>"

    }

I rzeczywiście widzę „boo” w div # sayboo.

Następnie próbuję wywołać jquery datatable

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

Nie ma szczęścia

Następnie próbuję dodać limit czasu:

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

I to działa. Więc zastanawiam się, co poszło nie tak w wersji kodu bez timera?

Nik So
źródło
1
@adardesign Nie, nigdy tego nie robiłem, musiałem użyć timera. Z jakiegoś powodu callback nie jest tutaj tak naprawdę oddzwonieniem. Mam tabelę z 11 kolumnami i setkami wierszy, więc naturalnie kanciasty wygląda na dobry zakład do powiązania danych; ale muszę też użyć wtyczki jquery Datatables, która jest tak prosta, jak $ ('table'). datatable (). Używając dyrektywy lub po prostu mam głupi obiekt json ze wszystkimi wierszami i używam ng-powtarzania do iteracji, nie mogę uruchomić mojej $ (). Datatable () po wyrenderowaniu elementu html tabeli, więc moja sztuczka polega obecnie na liczniku czasu aby sprawdzić, czy $ ('tr'). length> 3 (b / c nagłówka / stopki)
Nik So,
2
@adardesign I tak, próbowałem wszystkich metod kompilacji, metody kompilacji zwracającej obiekt zawierający metody postLink / preLink, metody kompilacji zwracającej tylko funkcję (a mianowicie funkcji łączącej), metody łączenia (bez metody kompilacji, ponieważ o ile wiem, jeśli masz metodę kompilacji, która zwraca metodę łączącą, funkcja łącząca jest ignorowana). Żadna nie działała, więc musisz polegać na dobrym, starym, dobrym $ timeout. Zaktualizuję ten post, jeśli znajdę coś, co działa lepiej lub po prostu, gdy stwierdzę, że wywołanie zwrotne naprawdę działa jak oddzwonienie
Nik So

Odpowiedzi:

215

Jeśli drugi parametr „opóźnienie” nie zostanie podany, domyślnym zachowaniem jest wykonanie funkcji po zakończeniu renderowania modelu DOM. Więc zamiast setTimeout użyj $ timeout:

$timeout(function () {
    //DOM has finished rendering
});
parlament
źródło
8
Dlaczego nie jest to wyjaśnione w dokumentach ?
Gaui,
23
Masz rację, moja odpowiedź jest trochę myląca, ponieważ starałem się to uprościć. Pełna odpowiedź jest taka, że ​​ten efekt nie jest wynikiem działania Angulara, ale raczej przeglądarki. $timeout(fn)ostatecznie wywołania, setTimeout(fn, 0)które powodują przerwanie wykonywania Javascript i pozwolenie przeglądarce na renderowanie treści w pierwszej kolejności, przed kontynuowaniem wykonywania tego Javascript.
parlament
7
Pomyśl o przeglądarce jako o kolejkowaniu pewnych zadań, takich jak „wykonywanie javascript” i „renderowanie DOM” osobno, oraz o tym, co setTimeout (fn, 0) przesyła aktualnie uruchomione „wykonanie javascript” na koniec kolejki po wyrenderowaniu .
parlament
2
@GabLeRoux tak, to będzie miało ten sam efekt, ale $ timeout ma dodatkową zaletę wywołania $ scope. $ Apply () po uruchomieniu. Z _.defer () będziesz musiał wywołać ją ręcznie, jeśli myFunction zmieni zmienne w zakresie.
parlament
2
Mam scenariusz, w którym to nie pomaga, gdzie na stronie 1 ng-repeat renderuje kilka elementów, potem idę na stronę 2, a potem wracam do strony 1 i próbuję uzyskać maksimum elementów rodzicielskich ng-repeat ... zwraca nieprawidłową wysokość. Jeśli przekroczę limit czasu na 1000 ms, to działa.
yodalr
14

Miałem ten sam problem i uważam, że odpowiedź brzmi „nie”. Zobacz komentarz Miško i dyskusję w grupie .

Angular może śledzić, że wszystkie wywołania funkcji, które wykonuje w celu manipulowania DOM, są kompletne, ale ponieważ te funkcje mogą wyzwalać logikę asynchroniczną, która nadal aktualizuje DOM po ich powrocie, nie można było oczekiwać, że będzie o tym wiedział. Każde wywołanie zwrotne, które daje Angular, może czasami działać, ale nie można by na nim polegać.

Rozwiązaliśmy to heurystycznie za pomocą setTimeout, tak jak Ty.

(Pamiętaj, że nie wszyscy się ze mną zgadzają - przeczytaj komentarze pod powyższymi linkami i zobacz, co myślisz.)

Roy Truelove
źródło
7

Możesz skorzystać z funkcji „link”, znanej również jako postLink, która jest uruchamiana po wstawieniu szablonu.

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

Przeczytaj, jeśli planujesz tworzyć dyrektywy, to duża pomoc: http://docs.angularjs.org/guide/directive

Andrew Joslin
źródło
Cześć Andy, bardzo dziękuję za odpowiedź; Wypróbowałem funkcję link, ale nie miałbym nic przeciwko ponownej próbie dokładnie tak, jak ją kodujesz; Ostatnie 1,5 dnia spędziłem na czytaniu tej strony z dyrektywą; i patrząc również na przykłady na stronie angulara. Spróbuję teraz twojego kodu.
Nik So
Ach, teraz widzę, że próbowałeś utworzyć link, ale zrobiłeś to źle. Jeśli po prostu zwrócisz funkcję, zakłada się, że jest to link. Jeśli zwracasz obiekt, musisz go zwrócić z kluczem jako „link”. Możesz również zwrócić funkcję łączącą z funkcji kompilującej.
Andrew Joslin
Cześć Andy, otrzymałem moje wyniki; Prawie straciłem zdrowie psychiczne, ponieważ naprawdę zrobiłem w zasadzie to, co tutaj jest twoja odpowiedź. Proszę zobaczyć moją aktualizację
Nik So
Humm, spróbuj czegoś takiego: <table id = "bob"> <tbody dashboard-table = "# bob"> </tbody> </table> Następnie w swoim linku zrób $ (attrs.dashboardTable) .dataTable (), aby upewnij się, że jest wybrany prawidłowo. Albo chyba już tego próbowałeś ... Naprawdę nie jestem pewien, czy łącze nie działa.
Andrew Joslin,
Ten działał dla mnie, chciałem przenieść elementy w ramach renderowania szablonu po dom, aby spełnić moje wymagania, zrobiłem to w funkcji linku. Dzięki
abhi
7

Chociaż moja odpowiedź nie jest związana z danymi, to dotyczy problemu manipulacji DOM i np. Inicjalizacji wtyczki jQuery dla dyrektyw używanych na elementach, których zawartość jest aktualizowana w sposób asynchroniczny.

Zamiast implementować limit czasu, można po prostu dodać zegarek, który będzie nasłuchiwał zmian treści (lub nawet dodatkowych zewnętrznych wyzwalaczy).

W moim przypadku użyłem tego obejścia do zainicjowania wtyczki jQuery po wykonaniu ng-repeat, które utworzyło mój wewnętrzny DOM - w innym przypadku użyłem go tylko do manipulowania DOM po zmianie właściwości scope na kontrolerze. Oto jak to zrobiłem ...

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

                }
            }
        }
    }
}]);

Uwaga: Zamiast po prostu rzutować zmienną myContent na wartość bool w atrybucie my-Directive-watch, można sobie wyobrazić dowolne wyrażenie.

Uwaga: Izolowanie zakresu, jak w powyższym przykładzie, można wykonać tylko raz na element - próba zrobienia tego z wieloma dyrektywami na tym samym elemencie spowoduje $ compile: multidir Error - patrz: https://docs.angularjs.org / error / $ compile / multidir

conceptdeluxe
źródło
7

Może spóźnię się na odpowiedź na to pytanie. Ale nadal ktoś może skorzystać z mojej odpowiedzi.

Miałem podobny problem iw moim przypadku nie mogę zmienić dyrektywy, ponieważ jest to biblioteka i zmiana kodu biblioteki nie jest dobrą praktyką. Więc użyłem zmiennej, aby poczekać na załadowanie strony i użyć ng-if w moim html, aby czekać, wyrenderować określony element.

W moim kontrolerze:

$scope.render=false;

//this will fire after load the the page

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

W moim html (w moim przypadku komponent html to płótno)

<canvas ng-if="render"> </canvas>
Madura Pradeep
źródło
3

Miałem ten sam problem, ale używając Angular + DataTable z fnDrawCallback+ grupowaniem wierszy + $ skompilowanymi zagnieżdżonymi dyrektywami. Umieściłem $ timeout w moimfnDrawCallback funkcji, aby naprawić renderowanie stronicowania.

Przed przykładem, na podstawie źródła 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
  }
}

Po przykładzie:

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
}

Nawet krótkie opóźnienie wystarczyło, aby Angular mógł wyrenderować moje skompilowane dyrektywy Angulara.

JJ Zabkar
źródło
Ciekawe, czy masz dość duży stół z wieloma kolumnami? ponieważ stwierdziłem, że potrzebuję irytujących wielu milisekund (> 100), więc nie pozwól, aby dataTable () zadzwoniła do dławienia
Nik So
Zauważyłem, że problem wystąpił podczas nawigacji strony DataTable dla zestawów wyników od 2 do ponad 150 wierszy. Tak więc nie - nie sądzę, że problemem był rozmiar tabeli, ale być może DataTable dodał wystarczająco dużo narzutów renderowania, aby przeżuć niektóre z tych milisekund. Skupiłem się na tym, aby grupowanie wierszy działało w DataTable przy minimalnej integracji z AngularJS.
JJ Zabkar
2

Żadne z rozwiązań nie działało dla mnie, nie zgadzam się z użyciem limitu czasu. Dzieje się tak, ponieważ korzystałem z szablonu, który był dynamicznie tworzony podczas postLink.

Należy jednak pamiętać, że może wystąpić przekroczenie limitu czasu „0”, ponieważ limit czasu powoduje dodanie wywoływanej funkcji do kolejki przeglądarki, która nastąpi po silniku renderowania kątowego, ponieważ znajduje się on już w kolejce.

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

Michael Smolenski
źródło
0

Oto dyrektywa, która ma zaprogramować akcje po płytkim renderowaniu. Przez płytkie mam na myśli, że oceni on po wyrenderowaniu tego samego elementu i nie będzie to miało związku z tym, kiedy jego zawartość zostanie renderowana. Więc jeśli potrzebujesz jakiegoś elementu podrzędnego wykonującego akcję po renderowaniu, powinieneś rozważyć użycie go tam:

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

wtedy możesz:

<div after-render></div>

lub z jakimkolwiek przydatnym wyrażeniem, takim jak:

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

Sebastian Sastre
źródło
Tak naprawdę nie dzieje się tak po wyrenderowaniu treści. Gdybym miał w tym momencie wyrażenie wewnątrz elementu <div after-render> {{blah}} </div>, wyrażenie nie jest jeszcze oceniane. Zawartość div jest nadal {{blah}} wewnątrz funkcji link. Więc technicznie rzecz biorąc, uruchamiasz zdarzenie, zanim treść zostanie wyrenderowana.
Edward Olamisan
To jest płytka akcja po renderowaniu, nigdy nie twierdziłem, że jest głęboka
Sebastian Sastre
0

Mam to działając z następującą dyrektywą:

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

A w HTML:

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

rozwiązywanie problemów, jeśli powyższe nie działa.

1) zauważ, że „datatableSetup” jest odpowiednikiem „datatable-setup”. Angular zmienia format na kopertę wielbłąda.

2) upewnij się, że aplikacja jest zdefiniowana przed dyrektywą. np. prosta definicja i dyrektywa aplikacji.

var app = angular.module('app', []);
app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});
Anton
źródło
0

Ponieważ nie można przewidzieć kolejności załadunku, można zastosować proste rozwiązanie.

Przyjrzyjmy się relacji dyrektywa-„użytkownik dyrektywy”. Zwykle użytkownik dyrektywy dostarczy pewne dane do dyrektywy lub użyje pewnych funkcji (funkcji), które zapewnia dyrektywa. Z drugiej strony dyrektywa wymaga określenia niektórych zmiennych dotyczących jej zakresu.

Jeśli uda nam się upewnić, że wszyscy gracze spełnili wszystkie wymagania dotyczące akcji, zanim podejmą próbę wykonania tych czynności - wszystko powinno być w porządku.

A teraz dyrektywa:

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

        };
})

a teraz użytkownik dyrektywy html

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

i gdzieś w kontrolerze komponentu, który używa dyrektywy:

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

O to chodzi. Jest dużo narzutów, ale możesz stracić limit czasu $. Zakładamy również, że komponent, który używa dyrektywy, jest tworzony przed dyrektywą, ponieważ zależymy od zmiennej kontrolnej, aby istniała podczas tworzenia wystąpienia dyrektywy.

Eli
źródło