Chamando uma função quando ng-repeat terminar

249

O que estou tentando implementar é basicamente um manipulador "on ng repeat terminou rendering". Sou capaz de detectar quando isso é feito, mas não consigo descobrir como acionar uma função a partir dela.

Verifique o violino: http://jsfiddle.net/paulocoelho/BsMqq/3/

JS

var module = angular.module('testApp', [])
    .directive('onFinishRender', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                element.ready(function () {
                    console.log("calling:"+attr.onFinishRender);
                    // CALL TEST HERE!
                });
            }
        }
    }
});

function myC($scope) {
    $scope.ta = [1, 2, 3, 4, 5, 6];
    function test() {
        console.log("test executed");
    }
}

HTML

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>

Resposta : Violino de trabalho de finishmove: http://jsfiddle.net/paulocoelho/BsMqq/4/

PCoelho
fonte
Por curiosidade, qual é o objetivo do element.ready()snippet? Quero dizer .. é algum tipo de plugin jQuery que você possui ou deve ser acionado quando o elemento estiver pronto?
gion_13
1
Pode-se fazê-lo usando built-in directivas como ng-init
semiomant
Possível duplicado do evento ng-repeat acabamento
Damjan Pavlica
duplicado de stackoverflow.com/questions/13471129/… , veja minha resposta lá #
bresleveloper

Respostas:

400
var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                $timeout(function () {
                    scope.$emit(attr.onFinishRender);
                });
            }
        }
    }
});

Observe que eu não usei, .ready()mas o envolvi em um $timeout. $timeoutgarante que ele seja executado quando os elementos ng-repetidos REALMENTE terminarem a renderização (porque eles $timeoutserão executados no final do ciclo de compilação atual - e também serão chamados $applyinternamente, ao contráriosetTimeout ). Portanto, após o ng-repeattérmino, usamos $emitpara emitir um evento para escopos externos (escopos irmão e pai).

E então no seu controlador, você pode capturá-lo com $on:

$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
    //you also get the actual event object
    //do stuff, execute functions -- whatever...
});

Com html que se parece com isso:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>
princípio holográfico
fonte
10
+1, mas eu usaria $ eval antes de usar um evento - menos acoplamento. Veja minha resposta para mais detalhes.
Mark Rajcok
1
@PigalevPavel Eu acho que você está confundindo $timeout(que é basicamente o setTimeout+ Angular $scope.$apply()) setInterval. $timeoutserá executado apenas uma vez por ifcondição, e isso será no início do próximo $digestciclo. Para mais informações sobre limites de tempo JavaScript, consulte: ejohn.org/blog/how-javascript-timers-work
holográfica-princípio
1
@finishingmove Talvez eu não entenda alguma coisa :) Mas veja, eu tenho ng-repeat em outro ng-repeat. E quero saber com certeza quando todos eles terminaram. Quando eu uso seu script com $ timeout apenas para pais ng-repeat, tudo funciona bem. Mas se eu não usar $ timeout, recebo uma resposta antes que as repetições ng de crianças sejam concluídas. Eu quero saber porque? E posso ter certeza de que, se eu usar seu script com $ timeout para pais ng-repeat, sempre receberei uma resposta quando todas as ng-repetições forem concluídas?
Pigalev Pavel
1
Por que você usaria algo como setTimeout()0 atraso é outra pergunta, embora eu deva dizer que nunca encontrei uma especificação real para a fila de eventos do navegador em qualquer lugar, apenas o que está implícito no encadeamento único e na existência de setTimeout().
rakslice
1
Obrigado por esta resposta. Eu tenho uma pergunta, por que precisamos atribuir on-finish-render="ngRepeatFinished"? Quando eu atribuo on-finish-render="helloworld", funciona da mesma maneira.
Arnaud #
89

Use $ evalAsync se desejar que seu retorno de chamada (ou seja, test ()) seja executado após a construção do DOM, mas antes da renderização do navegador. Isso evitará tremulações - ref .

if (scope.$last) {
   scope.$evalAsync(attr.onFinishRender);
}

Violino .

Se você realmente deseja chamar seu retorno de chamada após a renderização, use $ timeout:

if (scope.$last) {
   $timeout(function() { 
      scope.$eval(attr.onFinishRender);
   });
}

Prefiro $ eval em vez de um evento. Com um evento, precisamos saber o nome do evento e adicionar código ao nosso controlador para esse evento. Com $ eval, há menos acoplamento entre o controlador e a diretiva.

Mark Rajcok
fonte
O que a verificação $lastfaz? Não é possível encontrá-lo nos documentos. Foi removido?
Erik Aigner
2
@ErikAigner, $lasté definido no escopo se um ng-repeatestiver ativo no elemento.
Mark Rajcok
Parece que o seu Fiddle está absorvendo desnecessariamente $timeout.
Bbodenmiller
1
Ótimo. Essa deve ser a resposta aceita, visto que a emissão / transmissão custa mais do ponto de vista do desempenho.
Florin Vistig 30/09/16
29

As respostas que foram dadas até agora só funcionam na primeira vez em que ng-repeatsão renderizadas , mas se você tiver uma dinâmica ng-repeat, significa que você irá adicionar / excluir / filtrar itens e precisará ser notificado sempre que o ng-repeatfor renderizada, essas soluções não funcionarão para você.

Então, se você precisar ser notificado TODAS AS VEZES de que as imagens ng-repeatsão renderizadas novamente e não apenas a primeira vez, eu encontrei uma maneira de fazer isso, é bastante 'hacky', mas funcionará bem se você souber o que é fazendo. Use isso $filterno seu ng-repeat antes de usar qualquer outro$filter :

.filter('ngRepeatFinish', function($timeout){
    return function(data){
        var me = this;
        var flagProperty = '__finishedRendering__';
        if(!data[flagProperty]){
            Object.defineProperty(
                data, 
                flagProperty, 
                {enumerable:false, configurable:true, writable: false, value:{}});
            $timeout(function(){
                    delete data[flagProperty];                        
                    me.$emit('ngRepeatFinished');
                },0,false);                
        }
        return data;
    };
})

Será $emitum evento chamado ngRepeatFinishedsempre que a ng-repeatrenderização for renderizada.

Como usá-lo:

<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >

O ngRepeatFinishfiltro precisa ser aplicado diretamente a um Arrayou a um Objectdefinido no seu $scope, você pode aplicar outros filtros depois.

Como NÃO usá-lo:

<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >

Não aplique outros filtros primeiro e depois aplique o ngRepeatFinishfiltro.

Quando devo usar isso?

Se você deseja aplicar certos estilos css no DOM após a conclusão da renderização da lista, é necessário ter em conta as novas dimensões dos elementos DOM que foram renderizados novamente pelo ng-repeat. (BTW: esse tipo de operação deve ser realizado dentro de uma diretiva)

O que NÃO FAZER na função que lida com o ngRepeatFinishedevento:

  • Não execute a $scope.$applynessa função ou você colocará o Angular em um loop infinito que o Angular não poderá detectar.

  • Não use-o para fazer alterações nas $scopepropriedades, porque essas alterações não serão refletidas na sua visualização até o próximo $digestloop e, como você não pode executar uma, $scope.$applyelas não serão de uso algum.

"Mas os filtros não devem ser usados ​​assim !!"

Não, eles não são, isso é um hack, se você não gostar, não use. Se você souber uma maneira melhor de realizar a mesma coisa, informe-me.

Resumindo

Isso é um truque , e usá-lo da maneira errada é perigoso, use-o apenas para aplicar estilos após a ng-repeatconclusão da renderização e você não deve ter nenhum problema.

Josep
fonte
Thx 4 a ponta. De qualquer forma, um plunkr com um exemplo de trabalho seria muito apreciado.
Holográfico #
2
Infelizmente, isso não funcionou para mim, pelo menos para o AngularJS 1.3.7 mais recente. Então, descobri outra solução alternativa, não a mais agradável, mas se isso for essencial para você, funcionará. O que eu fiz é que sempre que adiciono / altero / excluo elementos, sempre adiciono outro elemento fictício ao final da lista, portanto, como o $lastelemento também é alterado, uma ngRepeatFinishdiretiva simples , verificando $lastagora, funcionará. Assim que ngRepeatFinish é chamado, removo o item fictício. (Eu também escondê-lo com CSS por isso dooesn't aparecer brevemente)
darklow
Esse truque é legal, mas não perfeito; cada vez que o filtro chutes no evento é despachado cinco vezes: - /
Sjeiti
O abaixo Mark Rajcokfunciona perfeitamente em todas as repetições do ng-repeat (por exemplo, devido à alteração do modelo). Portanto, essa solução hacky não é necessária.
Dzhu
1
@ Joseph, você está certo - quando os itens são emendados / sem deslocamento (ou seja, a matriz está sendo alterada), ele não funciona. Meu teste anterior, provavelmente estava com matriz que foram realocados (usando sugarjs), portanto, ele trabalhou cada vez ... Obrigado por apontar isto
Dzhu
10

Se você precisar chamar funções diferentes para diferentes repetições ng no mesmo controlador, tente algo como isto:

A diretiva:

var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
            $timeout(function () {
                scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
            });
            }
        }
    }
});

No seu controlador, capture eventos com $ on:

$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});

$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});

No seu modelo com várias repetições ng

<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
    <div>{{item.name}}}<div>
</div>

<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
    <div>{{item.name}}}<div>
</div>
MCurbelo
fonte
5

As outras soluções funcionarão bem no carregamento inicial da página, mas chamar $ timeout do controlador é a única maneira de garantir que sua função seja chamada quando o modelo for alterado. Aqui está um violino que usa $ timeout. Para o seu exemplo, seria:

.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
    $timeout(function () {
       test();
    });
});

O ngRepeat avaliará apenas uma diretiva quando o conteúdo da linha for novo; portanto, se você remover itens da sua lista, o onFinishRender não será acionado. Por exemplo, tente inserir valores de filtro nesses toques emitidos .

John Harley
fonte
Da mesma forma, test () não é sempre chamado na solução evalAsynch quando o modelo muda fiddle
John Harley
2
o que está tadentro $scope.$watch("ta",...?
Muhammad Adeel Zahid
4

Se você não é avesso a usar adereços de escopo com dois dólares e está escrevendo uma diretiva cujo único conteúdo é uma repetição, existe uma solução bastante simples (supondo que você se preocupe apenas com a renderização inicial). Na função de link:

const dereg = scope.$watch('$$childTail.$last', last => {
    if (last) {
        dereg();
        // do yr stuff -- you may still need a $timeout here
    }
});

Isso é útil nos casos em que você tem uma diretiva que precisa manipular o DOM com base nas larguras ou alturas dos membros de uma lista renderizada (que eu acho que é o motivo mais provável de alguém fazer essa pergunta), mas não é tão genérica como as outras soluções propostas.

Ponto e vírgula
fonte
1

Uma solução para esse problema com um ngRepeat filtrado poderia estar com o Mutation eventos de , mas eles foram preteridos (sem substituição imediata).

Então pensei em outro fácil:

app.directive('filtered',function($timeout) {
    return {
        restrict: 'A',link: function (scope,element,attr) {
            var elm = element[0]
                ,nodePrototype = Node.prototype
                ,timeout
                ,slice = Array.prototype.slice
            ;

            elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
            elm.removeChild = alt.bind(null,nodePrototype.removeChild);

            function alt(fn){
                fn.apply(elm,slice.call(arguments,1));
                timeout&&$timeout.cancel(timeout);
                timeout = $timeout(altDone);
            }

            function altDone(){
                timeout = null;
                console.log('Filtered! ...fire an event or something');
            }
        }
    };
});

Isso se conecta aos métodos Node.prototype do elemento pai com um timeout $ timeout para observar as modificações sucessivas.

Funciona principalmente correto, mas recebi alguns casos em que o altDone seria chamado duas vezes.

Novamente ... adicione esta diretiva ao pai do ngRepeat.

Sjeiti
fonte
Até agora, isso funciona melhor, mas não é perfeito. Por exemplo, eu vou de 70 itens para 68, mas ele não dispara.
Gaui 14/05
1
O que poderia funcionar é adicionar appendChildtambém (desde que eu usei apenas insertBeforee removeChild) no código acima.
Sjeiti 15/05
1

Muito fácil, foi assim que eu fiz.

.directive('blockOnRender', function ($blockUI) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            
            if (scope.$first) {
                $blockUI.blockElement($(element).parent());
            }
            if (scope.$last) {
                $blockUI.unblockElement($(element).parent());
            }
        }
    };
})

CPhelefu
fonte
Esse código funciona, é claro, se você tiver seu próprio serviço $ blockUI.
precisa saber é o seguinte
0

Por favor, dê uma olhada no violino, http://jsfiddle.net/yNXS2/ . Como a diretiva que você criou não criou um novo escopo, continuei no caminho.

$scope.test = function(){... fez isso acontecer.

Rajkamal Subramanian
fonte
Violino errado? O mesmo que o da pergunta.
James M
humm, você atualizou o violino? Está atualmente mostrando uma cópia do meu código original.
PCoelho 4/03/13
0

Estou muito surpreso por não encontrar a solução mais simples entre as respostas a esta pergunta. O que você deseja fazer é adicionar uma ngInitdiretiva ao seu elemento repetido (o elemento com a ngRepeatdiretiva) verificando $last(uma variável especial definida no escopo pela ngRepeatqual indica que o elemento repetido é o último da lista). Se $lastfor verdade, renderizamos o último elemento e podemos chamar a função que queremos.

ng-init="$last && test()"

O código completo para sua marcação HTML seria:

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>

Você não precisa de nenhum código JS extra em seu aplicativo além da função de escopo que deseja chamar (neste caso test) , pois ngInité fornecida pelo Angular.js. Apenas certifique-se de ter sua testfunção no escopo para que possa ser acessada a partir do modelo:

$scope.test = function test() {
    console.log("test executed");
}
Alejandro García Iglesias
fonte