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/


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


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>

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.


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.


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>

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 .


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.


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.


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


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.


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");
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.