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

ChruS
fonte

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!

Tiago Roldão
fonte
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.

karlgold
fonte
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()">
Vikas Bansal
fonte
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...
        }

    })
Joseph Oster
fonte
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.

dshap
fonte
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 ...

deostroll
fonte
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.

Pavel Horal
fonte
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.

Cody
fonte
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!");
      }
    }
  };
});
JoshuaDavid
fonte
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.

Mvochoa
fonte
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.

scotty3
fonte
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

bresleveloper
fonte
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>
João Paulo
fonte
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); });
Blomersi
fonte
-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.

user4808836
fonte