AngularJS: Jak uruchomić dodatkowy kod po tym, jak AngularJS wykonał szablon?

182

Mam szablon Angular w DOM. Kiedy mój kontroler otrzymuje nowe dane z usługi, aktualizuje model w zakresie $ i ponownie renderuje szablon. Jak dotąd wszystko dobrze.

Problem polega na tym, że muszę wykonać dodatkową pracę po ponownym renderowaniu szablonu i umieszczeniu go w DOM (w tym przypadku wtyczka jQuery).

Wygląda na to, że powinno być jakieś wydarzenie, na przykład AfterRender, ale nie mogę znaleźć czegoś takiego. Być może dobrym rozwiązaniem byłaby dyrektywa, ale wydawało się, że również zaczęła działać zbyt wcześnie.

Oto jsFiddle przedstawiający mój problem: Fiddle-AngularIssue

== AKTUALIZACJA ==

W oparciu o pomocne komentarze odpowiednio zmieniłem dyrektywę na obsługę manipulacji DOM i zaimplementowałem model $ watch wewnątrz dyrektywy. Nadal jednak mam ten sam problem podstawowy; kod wewnątrz zdarzenia $ watch zostanie uruchomiony przed skompilowaniem szablonu i wstawieniem go do DOM, dlatego wtyczka jquery zawsze ocenia pustą tabelę.

Co ciekawe, jeśli usunę wywołanie asynchroniczne, wszystko działa dobrze, więc jest to krok we właściwym kierunku.

Oto mój zaktualizowany Fiddle, aby odzwierciedlić te zmiany: http://jsfiddle.net/uNREn/12/

gorebash
źródło
To wydaje się podobne do pytania, które miałem. stackoverflow.com/questions/11444494/… . Może coś tam może pomóc.
dnc253

Odpowiedzi:

39

Ten post jest stary, ale zmieniam kod na:

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

patrz jsfiddle .

Mam nadzieję, że Ci to pomoże

dipold
źródło
Dzięki, to bardzo pomocne. Jest to dla mnie najlepsze rozwiązanie, ponieważ nie wymaga manipulacji domem ze strony kontrolera i nadal będzie działać, gdy dane zostaną zaktualizowane na podstawie naprawdę asynchronicznej odpowiedzi usługi (i nie muszę używać $ evalAsync w moim kontrolerze).
gorebash,
9
Czy to było przestarzałe? Kiedy próbuję, to faktycznie wywołuje się tuż przed renderowaniem DOM. Czy to zależy od cienia dom czy coś takiego?
Shayne
Jestem stosunkowo nowy w Angular i nie widzę, jak zaimplementować tę odpowiedź w moim konkretnym przypadku. Patrzyłem na różne odpowiedzi i zgadywanie, ale to nie działa. Nie jestem pewien, co powinna oglądać funkcja „obserwuj”. Nasza aplikacja kątowa ma różne różne języki, które będą się różnić w zależności od strony, więc skąd mam wiedzieć, co „oglądać”? Czy na pewno istnieje prosty sposób na uruchomienie kodu po zakończeniu renderowania strony?
moosefetcher
63

Po pierwsze, właściwym miejscem na bałagan z renderowaniem są dyrektywy. Radzę zawinąć DOM manipulujący wtyczkami jQuery za pomocą dyrektyw takich jak ta.

Miałem ten sam problem i wymyśliłem ten fragment kodu. Wykorzystuje $watchi $evalAsyncdo zapewnienia, że ​​Twój kod działa po rozwiązaniu dyrektyw takich ng-repeatjak {{ value }}i renderowania szablonów .

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

Mam nadzieję, że pomoże Ci to w uniknięciu niektórych zmagań.

danijar
źródło
Może to oznaczać coś oczywistego, ale jeśli to nie zadziała, sprawdź operacje asynchroniczne w funkcjach / kontrolerach łącza. Nie sądzę, że $ evalAsync może wziąć to pod uwagę.
LOAS
Warto zauważyć, że można również połączyć linkfunkcję z templateUrl.
Wtower,
35

Zgodnie z radą Misko, jeśli chcesz wykonać operację asynchroniczną, zamiast $ timeout () (która nie działa)

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

użyj $ evalAsync () (która działa)

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

Fiddle . Dodałem również link „usuń wiersz danych”, który zmodyfikuje $ scope.assignments, symulując zmianę danych / modelu - aby pokazać, że zmiana danych działa.

W sekcji Środowisko wykonawcze strony Przegląd koncepcyjny wyjaśniono, że evalAsync należy stosować, gdy potrzebujesz czegoś poza bieżącą ramką stosu, ale przed renderowaniem przeglądarki. (Zgadywanie tutaj ... „bieżąca ramka stosu” prawdopodobnie zawiera aktualizacje Angular DOM.) Użyj limitu czasu $, jeśli potrzebujesz czegoś po renderowaniu przeglądarki.

Jednak, jak już się dowiedziałeś, nie sądzę, aby była tu potrzeba operacji asynchronicznej.

Mark Rajcok
źródło
4
$scope.$evalAsync($scope.assignmentsLoaded(data));nie ma żadnego sensu IMO. Argumentem za $evalAsync()powinna być funkcja. W twoim przykładzie wywołujesz funkcję w momencie, gdy funkcja powinna zostać zarejestrowana do późniejszego wykonania.
hgoebl
@hgoebl, argumentem $ evalAsync może być funkcja lub wyrażenie. W tym przypadku użyłem wyrażenia. Ale masz rację, wyrażenie zostaje natychmiast wykonane. Zmodyfikowałem odpowiedź, aby zawinąć ją w funkcję do późniejszego wykonania. Dzięki za złapanie tego.
Mark Rajcok
26

Odkryłem, że najprostszym (tanim i wesołym) rozwiązaniem jest po prostu dodanie pustego zakresu za pomocą ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" na końcu ostatniego renderowanego elementu. Ta funkcja zostanie uruchomiona, aby sprawdzić, czy powinien zostać wyświetlony element zakresu. Wykonaj dowolny inny kod w tej funkcji.

Zdaję sobie sprawę, że nie jest to najbardziej elegancki sposób na robienie rzeczy, jednak działa dla mnie ...

Miałem podobną sytuację, choć nieco odwróconą, gdy musiałem usunąć wskaźnik ładowania, gdy animacja się rozpoczęła, na urządzeniach mobilnych kątowanie inicjowało się znacznie szybciej niż animacja, która ma być wyświetlana, a użycie płaszcza ng było niewystarczające, ponieważ wskaźnik ładowania był usunięte na długo przed wyświetleniem jakichkolwiek rzeczywistych danych. W tym przypadku właśnie dodałem funkcję return 0 do pierwszego renderowanego elementu iw tej funkcji odwróciłem var, który ukrywa wskaźnik ładowania. (oczywiście dodałem ng-hide do wskaźnika ładowania wyzwalanego przez tę funkcję.

fuzion9
źródło
3
To na pewno hack, ale właśnie tego potrzebowałem. Zmusił mój kod do działania dokładnie po renderowaniu (lub bardzo dokładnie dokładnie). W idealnym świecie nie zrobiłbym tego, ale oto jesteśmy ...
Andrew
Potrzebowałem $anchorScrollwezwania wbudowanej usługi po renderowaniu DOM i nic nie poradziło na to poza tym. Hackish, ale działa.
Pat Migliaccio,
ng-init jest lepszą alternatywą
Flying Gambit
1
ng-init może nie zawsze działać. Na przykład mam powtórzenie ng, które dodaje pewną liczbę obrazów do domena. Jeśli chcę wywołać funkcję po dodaniu każdego obrazu do powtórzenia ng, muszę użyć ng-show.
Michael Bremerkamp
4

W końcu znalazłem rozwiązanie, korzystałem z usługi REST do aktualizacji mojej kolekcji. Aby przekonwertować jquery bazującą na danych, należy wykonać następujący kod:

$scope.$watchCollection( 'conferences', function( old, nuew ) {
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () {
                $( '#dataTablex' ).dataTable();
        });
    });
Sergio Ceron
źródło
3

musiałem to robić dość często. Mam dyrektywę i muszę zrobić kilka rzeczy po tym, jak model zostanie w pełni załadowany do DOM. więc umieszczam moją logikę w linku: funkcja dyrektywy i zawijam kod w setTimeout (function () {.....}, 1); setTimout zostanie uruchomiony po załadowaniu DOM, a 1 milisekunda to najkrótszy czas po załadowaniu DOM, zanim kod zostanie wykonany. wydaje mi się, że to działa, ale chciałbym, aby kątowe wywołało zdarzenie, gdy szablon został załadowany, aby dyrektywy używane przez ten szablon mogły wykonywać zapytania i uzyskiwać dostęp do elementów DOM. mam nadzieję że to pomoże.

mgrimmenator
źródło
2

Możesz także utworzyć dyrektywę, która uruchamia kod w funkcji link.

Zobacz tę odpowiedź Stackoverflow .

Anthony O.
źródło
2

Ani $ scope. $ EvalAsync () ani $ timeout (fn, 0) nie działały dla mnie niezawodnie.

Musiałem połączyć te dwa. Stworzyłem dyrektywę, a dla dobrego pomiaru nadałem priorytet wyższy niż domyślna wartość. Oto jego instrukcja (uwaga: używam ngInject do wstrzykiwania zależności):

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

Aby wywołać dyrektywę, zrobiłbyś to:

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

Jeśli chcesz go wywołać po zakończeniu działania powtórzenia ng, dodałem pusty zakres w moim powtórzeniu ng i 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>
Xchai
źródło
1

W niektórych scenariuszach, w których aktualizujesz usługę i przekierowujesz do nowego widoku (strony), a następnie dyrektywa jest ładowana przed aktualizacją usług, możesz użyć $ rootScope.

Widok

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

Kontroler

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

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

}]);

Dyrektywa

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
źródło
1

Przyszło mi całkiem proste rozwiązanie. Nie jestem pewien, czy jest to właściwy sposób, ale działa w sensie praktycznym. Zobaczmy bezpośrednio, co chcemy renderować. Na przykład w dyrektywie, która zawiera niektóre ng-repeats, uważałbym na długość tekstu (możesz mieć inne rzeczy!) Akapitów lub całego HTML. Dyrektywa będzie taka:

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

UWAGA: kod faktycznie działa po renderowaniu zmian raczej po całkowitym renderowaniu. Ale wydaje mi się, że w większości przypadków możesz poradzić sobie z sytuacjami za każdym razem, gdy pojawią się zmiany renderowania. Możesz również pomyśleć o porównaniu tej pdługości (lub dowolnej innej miary) z modelem, jeśli chcesz uruchomić kod tylko raz po zakończeniu renderowania. Doceniam wszelkie przemyślenia / komentarze na ten temat.

Ehsan88
źródło
0

Możesz użyć modułu „jQuery Passthrough” narzędzi angular -ui . Z powodzeniem powiązałem wtyczkę karuzeli dotykowej jQuery z niektórymi obrazami, które pobieram asynchronicznie z usługi sieciowej i renderuję za pomocą ng-repeat.

Michael Kork.
źródło