AngularJS: Como executar código adicional após o AngularJS renderizar um modelo?

182

Eu tenho um modelo angular no DOM. Quando meu controlador obtém novos dados de um serviço, ele atualiza o modelo no escopo $ e renderiza novamente o modelo. Tudo de bom até agora.

O problema é que eu também preciso fazer um trabalho extra depois que o modelo foi renderizado novamente e estiver no DOM (neste caso, um plugin jQuery).

Parece que deve haver um evento para ouvir, como o AfterRender, mas não consigo encontrar nada disso. Talvez uma diretiva fosse um caminho a percorrer, mas parecia disparar muito cedo também.

Aqui está um jsFiddle descrevendo meu problema: Fiddle-AngularIssue

== ATUALIZAÇÃO ==

Com base em comentários úteis, mudei para uma diretiva para lidar com a manipulação do DOM e implementei um modelo $ watch dentro da diretiva. No entanto, ainda estou tendo o mesmo problema básico; o código dentro do evento $ watch é acionado antes que o modelo seja compilado e inserido no DOM; portanto, o plugin jquery está sempre avaliando uma tabela vazia.

Curiosamente, se eu remover a chamada assíncrona, tudo funcionará bem, então esse é um passo na direção certa.

Aqui está o meu Fiddle atualizado para refletir essas alterações: http://jsfiddle.net/uNREn/12/

gorebash
fonte
Isso parece semelhante a uma pergunta que eu tinha. stackoverflow.com/questions/11444494/… . Talvez algo lá possa ajudar.
dnc253

Respostas:

39

Esta postagem é antiga, mas altero seu código para:

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

veja jsfiddle .

Espero que ajude você

dipold
fonte
Obrigado, isso é muito útil. Essa é a melhor solução para mim, porque ela não requer manipulação de domínio do controlador e ainda funcionará quando os dados forem atualizados de uma resposta de serviço verdadeiramente assíncrona (e eu não preciso usar $ evalAsync no meu controlador).
gorebash
9
Isso foi preterido? Quando eu tento isso, ele realmente chama pouco antes do DOM ser renderizado. É dependente de uma sombra dom ou algo assim?
Shayne 23/09
Sou relativamente novo no Angular e não consigo ver como implementar essa resposta no meu caso específico. Estive procurando várias respostas e suposições, mas isso não está funcionando. Não sei ao certo qual deve ser a função 'watch'. Nosso aplicativo angular possui várias diretivas diferentes, que variam de página para página, então como eu sei o que 'assistir'? Certamente, há uma maneira simples de disparar algum código quando uma página terminar de ser renderizada?
moosefetcher
63

Primeiro, o lugar certo para mexer na renderização são as diretivas. Meu conselho seria envolver o DOM manipulando plugins jQuery por diretivas como esta.

Eu tive o mesmo problema e criei esse trecho. Ele usa $watche $evalAsyncpara garantir que seu código seja executado depois que diretivas como ng-repeatforam resolvidas e modelos como {{ value }}renderizados.

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

Espero que isso possa ajudá-lo a evitar alguma luta.

danijar
fonte
Isso pode estar afirmando o óbvio, mas se isso não funcionar, verifique as operações assíncronas nas funções / controladores de link. Eu não acho que $ evalAsync possa levar isso em consideração.
LOAS
Vale a pena notar que você também pode combinar uma linkfunção com a templateUrl.
Wtower
35

Seguindo o conselho de Misko, se você deseja uma operação assíncrona, em vez de $ timeout () (que não funciona)

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

use $ evalAsync () (que funciona)

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

Violino . Também adicionei um link "remover linha de dados" que modificará $ scope.assignments, simulando uma alteração nos dados / modelo - para mostrar que a alteração dos dados funciona.

A seção Tempo de execução da página Visão geral conceitual explica que evalAsync deve ser usado quando você precisar que algo ocorra fora do quadro de pilha atual, mas antes que o navegador seja processado. (Acho que aqui ... "o quadro atual da pilha" provavelmente inclui atualizações angulares do DOM.) Use $ timeout se precisar que algo ocorra após a renderização do navegador.

No entanto, como você já descobriu, acho que não há necessidade de operação assíncrona aqui.

Mark Rajcok
fonte
4
$scope.$evalAsync($scope.assignmentsLoaded(data));não faz nenhum sentido IMO. O argumento para $evalAsync()deve ser uma função. No seu exemplo, você está chamando a função no momento em que uma função deve ser registrada para execução posterior.
hgoebl
@hgoebl, o argumento para $ evalAsync pode ser uma função ou uma expressão. Nesse caso, usei uma expressão. Mas você está certo, a expressão é executada imediatamente. Modifiquei a resposta para envolvê-la em uma função para execução posterior. Obrigado por capturar isso.
Mark Rajcok
26

Eu descobri que a solução mais simples (barata e alegre) é simplesmente adicionar um espaço vazio com ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing ()" ao final do último elemento renderizado. Esta função será executada quando verificar se o elemento span deve ser exibido. Execute qualquer outro código nesta função.

Sei que essa não é a maneira mais elegante de fazer as coisas, no entanto, funciona para mim ...

Eu tive uma situação semelhante, embora levemente invertida, onde eu precisava remover um indicador de carregamento quando uma animação começou, em dispositivos móveis, o angular estava inicializando muito mais rápido que a animação a ser exibida, e o uso de uma capa de ng era insuficiente, pois o indicador de carregamento era removido bem antes da exibição de dados reais. Nesse caso, acabei de adicionar a função my return 0 ao primeiro elemento renderizado e, nessa função, inverti o var que oculta o indicador de carregamento. (é claro que adicionei um ng-hide ao indicador de carregamento acionado por esta função.

fuzion9
fonte
3
Este é um truque, com certeza, mas é exatamente o que eu precisava. Forcei meu código a ser executado exatamente após a renderização (ou muito próximo a exatamente). Em um mundo ideal eu não faria isso, mas aqui estamos nós ...
Andrew
Eu precisava que o $anchorScrollserviço interno fosse chamado após o DOM renderizado e nada parecia fazer o truque, exceto isso. Hackish, mas funciona.
Pat Migliaccio 17/02
ng-init é uma alternativa melhor
Flying Gambit
1
ng-init pode nem sempre funcionar. Por exemplo, tenho um ng-repeat que adiciona várias imagens ao dom. Se eu quiser chamar uma função após cada imagem ser adicionada no ng-repeat, tenho que usar o ng-show.
Michael Bremerkamp 28/03
4

Finalmente encontrei a solução, estava usando um serviço REST para atualizar minha coleção. Para converter o jquery da tabela de dados é o seguinte código:

$scope.$watchCollection( 'conferences', function( old, nuew ) {
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () {
                $( '#dataTablex' ).dataTable();
        });
    });
Sergio Ceron
fonte
3

Eu tive que fazer isso com bastante frequência. Eu tenho uma diretiva e preciso fazer algumas coisas de jquery depois que as coisas do modelo são totalmente carregadas no DOM. então eu coloquei minha lógica no link: function da diretiva e envolvo o código em um setTimeout (function () {.....}, 1); o setTimout será acionado após o carregamento do DOM e 1 milissegundo é o menor período de tempo após o carregamento do DOM antes da execução do código. isso parece funcionar para mim, mas desejo que o angular tenha gerado um evento assim que o modelo for carregado, para que as diretivas usadas por esse modelo possam fazer coisas de jquery e acessar elementos DOM. espero que isto ajude.

mgrimmenator
fonte
2

Você também pode criar uma diretiva que execute seu código na função de link.

Veja a resposta do stackoverflow .

Anthony O.
fonte
2

Nem $ scope. $ EvalAsync () ou $ timeout (fn, 0) funcionaram de maneira confiável para mim.

Eu tive que combinar os dois. Fiz uma diretiva e também coloquei uma prioridade maior que o valor padrão para uma boa medida. Aqui está uma diretiva para isso (note que eu uso o ngInject para injetar dependências):

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

Para chamar a diretiva, você faria o seguinte:

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

Se você quiser chamá-lo após a execução de ng-repeat, adicionei um espaço vazio em ng-repeat e 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
fonte
1

Em alguns cenários em que você atualiza um serviço e redireciona para uma nova exibição (página) e, em seguida, sua diretiva é carregada antes da atualização dos serviços, você pode usar $ rootScope. $ Broadcast se seu $ watch ou $ timeout falhar

Visão

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

Controlador

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

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

}]);

Directiva

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

Eu vim com uma solução bastante simples. Não tenho certeza se é a maneira correta de fazê-lo, mas funciona no sentido prático. Vamos assistir diretamente o que queremos que seja renderizado. Por exemplo, em uma diretiva que inclui alguns ng-repeats, eu observaria o comprimento do texto (você pode ter outras coisas!) Dos parágrafos ou o html inteiro. A diretiva será assim:

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

Observe que o código realmente é executado após a renderização ser alterada . Mas acho que, na maioria dos casos, você pode lidar com as situações sempre que houver alterações na renderização. Além disso, você pode comparar esse pcomprimento (ou qualquer outra medida) com o seu modelo se desejar executar seu código apenas uma vez após a conclusão da renderização. Agradeço quaisquer pensamentos / comentários sobre isso.

Ehsan88
fonte
0

Você pode usar o módulo 'jQuery Passthrough' dos utilitários angular-ui . Vinculei com êxito um plug-in do carrossel jQuery touch a algumas imagens que recupero assíncronas de um serviço da web e as renderizo com ng-repeat.

Michael Kork.
fonte