evento de conclusão ng-repeat


207

Eu quero chamar alguma função jQuery visando div com tabela. Essa tabela está preenchida com ng-repeat.

Quando eu ligo

$(document).ready()

Eu não tenho resultado.

Além disso

$scope.$on('$viewContentLoaded', myFunc);

não ajuda.

Existe alguma maneira de executar a função logo após a conclusão da população ng-repeat? Eu li um conselho sobre o uso personalizado directive, mas não tenho idéia de como usá-lo com ng-repeat e minha div ...

Respostas:


241

De fato, você deve usar diretivas e não há evento vinculado ao final de um loop ng-Repeat (já que cada elemento é construído individualmente e possui seu próprio evento). Mas a) usar diretivas pode ser tudo o que você precisa eb) existem algumas propriedades específicas do ng-Repeat que você pode usar para criar seu evento "no ngRepeat concluído".

Especificamente, se tudo o que você deseja é estilizar / adicionar eventos a toda a tabela, é possível fazê-lo usando uma diretiva que inclua todos os elementos ngRepeat. Por outro lado, se você quiser endereçar cada elemento especificamente, poderá usar uma diretiva dentro do ngRepeat e ela atuará em cada elemento, depois que ele for criado.

Então, há o $index, $first, $middlee $lastpropriedades que você pode usar para eventos de disparo. Então, para este HTML:

<div ng-controller="Ctrl" my-main-directive>
  <div ng-repeat="thing in things" my-repeat-directive>
    thing {{thing}}
  </div>
</div>

Você pode usar diretivas assim:

angular.module('myApp', [])
.directive('myRepeatDirective', function() {
  return function(scope, element, attrs) {
    angular.element(element).css('color','blue');
    if (scope.$last){
      window.alert("im the last!");
    }
  };
})
.directive('myMainDirective', function() {
  return function(scope, element, attrs) {
    angular.element(element).css('border','5px solid red');
  };
});

Veja em ação neste Plunker . Espero que ajude!


Posso fazer com que myMainDirectiveo código seja executado somente após o final do loop? Preciso atualizar a barra de rolagem da div pai.
ChruS

2
Claro! Basta alterar o "window.alert" para uma função de disparo de eventos e capturá-lo com a diretiva principal. Eu atualizei o de Plunker para fazer isso, como um exemplo.
Tiago Roldão

9
É recomendável incluir a biblioteca jQuery primeiro (antes do Angular). Isso fará com que todos os elementos Angular sejam agrupados com jQuery (em vez de jqLite). Então, em vez de angular.element (elemento) .css () e $ (elemento) .children (). Css () você pode simplesmente escrever element.css () e element.children (). Css (). Consulte também groups.google.com/d/msg/angular/6A3Skwm59Z4/oJ0WhKGAFK0J
Mark Rajcok

11
Any1 tem alguma idéia de como fazer isso funcionar para renderizações subseqüentes na diretiva ngRepeat? ou seja: link
RavenHursT

1
Esta é uma convenção de nomenclatura para todas as diretivas angulares. Veja docs.angularjs.org/guide/directive#normalization
Tiago Roldão

68

Se você simplesmente deseja executar algum código no final do loop, aqui está uma variação um pouco mais simples que não requer manipulação extra de eventos:

<div ng-controller="Ctrl">
  <div class="thing" ng-repeat="thing in things" my-post-repeat-directive>
    thing {{thing}}
  </div>
</div>
function Ctrl($scope) {
  $scope.things = [
    'A', 'B', 'C'  
  ];
}

angular.module('myApp', [])
.directive('myPostRepeatDirective', function() {
  return function(scope, element, attrs) {
    if (scope.$last){
      // iteration is complete, do whatever post-processing
      // is necessary
      element.parent().css('border', '1px solid black');
    }
  };
});

Veja uma demonstração ao vivo.


Isso funciona se eu utilizar o controlador como sintaxe? Por favor? Não, existe alguma outra maneira que funcione com o controlador como?
21817 Dan Nguyen #

63

Não há necessidade de criar uma diretiva, especialmente apenas para ter um ng-repeatevento completo.

ng-init faz a mágica para você.

  <div ng-repeat="thing in things" ng-init="$last && finished()">

isso $lastgarante que finishedapenas seja acionado quando o último elemento for renderizado no DOM.

Não se esqueça de criar $scope.finishedevento.

Feliz codificação !!

Edição: 23 out 2016

Caso você também deseje chamar a finishedfunção quando não houver nenhum item na matriz, poderá usar a seguinte solução alternativa

<div style="display:none" ng-init="things.length < 1 && finished()"></div>
//or
<div ng-if="things.length > 0" ng-init="finished()"></div>

Basta adicionar a linha acima na parte superior do ng-repeatelemento. Ele verificará se a matriz não está tendo nenhum valor e chamará a função adequadamente.

Por exemplo

<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">

E se "coisas" forem uma matriz vazia?
Frane Poljak

1
@FranePoljak Adicionei a solução na resposta.
Vikas Bansal 23/10

1
No caso de uma matriz vazia, manipule-a no controlador
JSAddict 24/10

Corrija a resposta do Vikas Bansal: // use first div se quiser verificar se a matriz não está tendo nenhum valor e chame a função de acordo. <div ng-if = "! things.length" ng-hide = "true" ng-init = "terminado ()"> </div> <div ng-repeat = "coisa nas coisas" ng-init = "$ last && terminou () ">
Alex Teslenko 23/02

41

Aqui está uma diretiva repetida que chama uma função especificada quando verdadeira. Eu descobri que a função chamada deve usar $ timeout com intervalo = 0 antes de fazer a manipulação do DOM, como inicializar dicas de ferramentas nos elementos renderizados. jsFiddle: http://jsfiddle.net/tQw6w/

Em $ scope.layoutDone, tente comentar a linha $ timeout e descomentar a mensagem "NÃO CORRETO!" linha para ver a diferença nas dicas de ferramentas.

<ul>
    <li ng-repeat="feed in feedList" repeat-done="layoutDone()" ng-cloak>
    <a href="{{feed}}" title="view at {{feed | hostName}}" data-toggle="tooltip">{{feed | strip_http}}</a>
    </li>
</ul>

JS:

angular.module('Repeat_Demo', [])

    .directive('repeatDone', function() {
        return function(scope, element, attrs) {
            if (scope.$last) { // all are rendered
                scope.$eval(attrs.repeatDone);
            }
        }
    })

    .filter('strip_http', function() {
        return function(str) {
            var http = "http://";
            return (str.indexOf(http) == 0) ? str.substr(http.length) : str;
        }
    })

    .filter('hostName', function() {
        return function(str) {
            var urlParser = document.createElement('a');
            urlParser.href = str;
            return urlParser.hostname;
        }
    })

    .controller('AppCtrl', function($scope, $timeout) {

        $scope.feedList = [
            'http://feeds.feedburner.com/TEDTalks_video',
            'http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/',
            'http://sfbay.craigslist.org/eng/index.rss',
            'http://www.slate.com/blogs/trending.fulltext.all.10.rss',
            'http://feeds.current.com/homepage/en_US.rss',
            'http://feeds.current.com/items/popular.rss',
            'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml'
        ];

        $scope.layoutDone = function() {
            //$('a[data-toggle="tooltip"]').tooltip(); // NOT CORRECT!
            $timeout(function() { $('a[data-toggle="tooltip"]').tooltip(); }, 0); // wait...
        }

    })

Eu não queria usar um tempo limite de $, mas quando você disse que poderia ser definido como 0, decidi experimentá-lo e funcionou bem. Gostaria de saber se isso é uma questão de digestão e se existe uma maneira de fazer o que o $ timeout está fazendo sem usar $ timeout.
Jameela Huq

Você pode explicar por que isso funciona? Estou tentando acessar IDs dinâmicos e só funciona depois de um tempo limite com 0 atraso. Eu vi essa solução "hacky" em várias postagens, mas ninguém explica por que ela funciona.
187 Jin Jin

30

Aqui está uma abordagem simples ng-initque nem sequer exige uma diretiva personalizada. Funcionou bem para mim em certos cenários, por exemplo, a necessidade de rolar automaticamente uma div de itens repetidos em ng para um item específico no carregamento da página; portanto, a função de rolagem precisa esperar até que a ng-repeatrenderização seja concluída no DOM antes que ele possa ser acionado.

<div ng-controller="MyCtrl">
    <div ng-repeat="thing in things">
        thing: {{ thing }}
    </div>
    <div ng-init="fireEvent()"></div>
</div>

myModule.controller('MyCtrl', function($scope, $timeout){
    $scope.things = ['A', 'B', 'C'];

    $scope.fireEvent = function(){

        // This will only run after the ng-repeat has rendered its things to the DOM
        $timeout(function(){
            $scope.$broadcast('thingsRendered');
        }, 0);

    };
});

Observe que isso é útil apenas para funções que você precisa chamar uma vez após o ng-repeat ser processado inicialmente. Se você precisar chamar uma função sempre que o conteúdo de ng-repeat for atualizado, será necessário usar uma das outras respostas nesse segmento com uma diretiva personalizada.


1
Bonito, isso fez o trabalho. Eu queria selecionar automaticamente o texto em uma caixa de texto e o tempo limite foi o suficiente. Caso contrário, o texto {{model.value}} foi selecionado e, em seguida, desmarcado quando o model.value ligado a dados foi injetado.
JoshGough 14/05

27

Complementando a resposta de Pavel , algo mais legível e facilmente compreensível seria:

<ul>
    <li ng-repeat="item in items" 
        ng-init="$last ? doSomething() : angular.noop()">{{item}}</li>
</ul>

Por que mais você acha angular.noop existe em primeiro lugar ...?

Vantagens:

Você não precisa escrever uma diretiva para isso ...


20
Por que usar um ternário quando você pode usar a lógica booleana:ng-init="$last && doSomething( )"
Nate Whittaker

É mais fácil ler o @NateWhittaker (e ser mais difícil de ler do que um operador ternário é impressionante). É bom ter um código limpo e simples de entender
Justin

21

Talvez uma abordagem um pouco mais simples com a de ngInitLodashdebounce método sem a necessidade de diretiva personalizada:

Controlador:

$scope.items = [1, 2, 3, 4];

$scope.refresh = _.debounce(function() {
    // Debounce has timeout and prevents multiple calls, so this will be called 
    // once the iteration finishes
    console.log('we are done');
}, 0);

Modelo:

<ul>
    <li ng-repeat="item in items" ng-init="refresh()">{{item}}</li>
</ul>

Atualizar

Existe ainda mais solução AngularJS pura e simples usando o operador ternário:

Modelo:

<ul>
    <li ng-repeat="item in items" ng-init="$last ? doSomething() : null">{{item}}</li>
</ul>

Esteja ciente de que o ngInit usa a fase de compilação de pré-link - ou seja, a expressão é invocada antes que as diretivas filhas sejam processadas. Isso significa que ainda pode ser necessário um processamento assíncrono.


Observe que você precisa do lodash.js para que isso funcione :) Além disso: a parte ", 20" do $ scope.refresh =, é esse número de milis antes que esse método seja chamado após a última iteração?
Jørgen Skår Fischer

Adicionado Lodash na frente do link de debounce . Sim, 20 é um atraso antes da execução do método - alterado para 0, pois não importa, desde que a chamada seja assíncrona.
Pavel Horal

1
Também pensando nisso, como o operador ternário é permitido em expressões simples, ng-init="$last ? refresh() : false"também funcionaria. E isso nem exige Lodash.
Pavel Horal

<div ng-repeat = "i in [1,2,4]" ng-init = "doSomething ($ last)"> </div> $ scope.doSomething = function (lastElement) {if (lastElem) blá blá blá }
JSAddict 24/10

4

Também pode ser necessário quando você checa a scope.$lastvariável para envolver seu gatilho com a setTimeout(someFn, 0). A setTimeout 0é uma técnica aceita em javascript e era essencial que eu directivefuncionasse corretamente.


Encontrei o mesmo problema. tanto no backbone + angular, se você precisar fazer algo como ler valores de propriedade css aplicados a um elemento DOM, não consegui descobrir uma maneira sem o hack de timeout.
Chovy

3

Isso é uma melhoria das idéias expressas em outras respostas para mostrar como obter acesso às propriedades ngRepeat ($ index, $ first, $ middle, $ last, $ even, $ odd) ao usar sintaxe declarativa e isolar escopo ( Prática recomendada pelo Google) com uma diretiva de elemento. Observe a diferença principal: scope.$parent.$last.

angular.module('myApp', [])
.directive('myRepeatDirective', function() {
  return {
    restrict: 'E',
    scope: {
      someAttr: '='
    },
    link: function(scope, element, attrs) {
      angular.element(element).css('color','blue');
      if (scope.$parent.$last){
        window.alert("im the last!");
      }
    }
  };
});

Não seria melhor usar diretivas como atributos do que elementos? ou seja, restringir: 'A'?
Tejasvi Hegde

2
O objetivo era mostrar a sintaxe declarativa + isolar o escopo ... sim, você pode usar uma diretiva de atributo aqui, se desejar. Há um tempo e um local para cada um, dependendo do seu objetivo.
JoshuaDavid

3

Eu fiz assim.

Crie a diretiva

function finRepeat() {
    return function(scope, element, attrs) {
        if (scope.$last){
            // Here is where already executes the jquery
            $(document).ready(function(){
                $('.materialboxed').materialbox();
                $('.tooltipped').tooltip({delay: 50});
            });
        }
    }
}

angular
    .module("app")
    .directive("finRepeat", finRepeat);

Depois de adicioná-lo no rótulo onde esta repetição ng

<ul>
    <li ng-repeat="(key, value) in data" fin-repeat> {{ value }} </li>
</ul>

E pronto com isso será executado no final do ng-repeat.


3
<div ng-repeat="i in items">
        <label>{{i.Name}}</label>            
        <div ng-if="$last" ng-init="ngRepeatFinished()"></div>            
</div>

Minha solução foi adicionar um div para chamar uma função se o item fosse o último em uma repetição.


2

eu gostaria de adicionar outra resposta, já que as respostas anteriores consideram que o código necessário para executar o ngRepeat é um código angular, que nesse caso todas as respostas acima fornecem uma solução ótima e simples, algumas mais genéricas que outras, e caso seja importante o estágio do ciclo de vida do resumo, você pode dar uma olhada no blog de Ben Nadel , com a exceção de usar $ parse em vez de $ eval.

mas na minha experiência, como afirma o OP, geralmente está executando alguns plug-ins ou métodos JQuery no DOM finalmente compilado, que nesse caso eu achei que a solução mais simples é criar uma diretiva com um setTimeout, já que a função setTimeout é pressionada até o final da fila do navegador, é sempre logo depois que tudo é feito em angular, geralmente ngReapet, que continua após a função postLinking dos pais

angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
  return function(scope, element, attrs) {        
    setTimeout(function doWork(){
      //jquery code and plugins
    }, 0);        
  };
});

para quem quer saber que, nesse caso, por que não usar $ timeout, isso causa outro ciclo de digestão que é completamente desnecessário


1

Eu tive que renderizar fórmulas usando MathJax após o fim da repetição de ng, nenhuma das respostas acima resolveu meu problema, então fiz como abaixo. Não é uma solução agradável , mas funcionou para mim ...

<div ng-repeat="formula in controller.formulas">
    <div>{{formula.string}}</div>
    {{$last ? controller.render_formulas() : ""}}
</div>

0

Encontrei uma resposta aqui bem praticada, mas ainda era necessário adicionar um atraso

Crie a seguinte diretiva:

angular.module('MyApp').directive('emitLastRepeaterElement', function() {
return function(scope) {
    if (scope.$last){
        scope.$emit('LastRepeaterElement');
    }
}; });

Adicione-o ao seu repetidor como um atributo, assim:

<div ng-repeat="item in items" emit-last-repeater-element></div>

De acordo com Radu:

$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {mycode});

Para mim, funciona, mas ainda preciso adicionar um setTimeout

$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {
setTimeout(function() { 
    mycode
}, 100); });

-4

Se você simplesmente deseja alterar o nome da classe para que seja renderizado de maneira diferente, o código abaixo faria o truque.

<div>
<div ng-show="loginsuccess" ng-repeat="i in itemList">
    <div id="{{i.status}}" class="{{i.status}}">
        <div class="listitems">{{i.item}}</div>
        <div class="listitems">{{i.qty}}</div>
        <div class="listitems">{{i.date}}</div>
        <div class="listbutton">
            <button ng-click="UpdateStatus(i.$id)" class="btn"><span>Done</span></button>
            <button ng-click="changeClass()" class="btn"><span>Remove</span></button>
        </div>
    <hr>
</div>

Esse código funcionou para mim quando eu tinha um requisito semelhante para renderizar o item comprado na minha lista de compras na fonte Strick trough.

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.