existe um retorno de chamada pós-renderização para a diretiva Angular JS?

139

Acabei de receber minha diretiva para puxar um modelo para acrescentar ao seu elemento assim:

# CoffeeScript
.directive 'dashboardTable', ->
  controller: lineItemIndexCtrl
  templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
  (scope, element, attrs) ->
    element.parent('table#line_items').dataTable()
    console.log 'Just to make sure this is run'

# HTML
<table id="line_items">
    <tbody dashboard-table>
    </tbody>
</table>

Também estou usando um plugin jQuery chamado DataTables. O uso geral disso é assim: $ ('table # some_id'). DataTable (). Você pode passar os dados JSON para a chamada dataTable () para fornecer os dados da tabela OU pode ter os dados já na página e eles farão o resto. Estou fazendo o último, com as linhas já na página HTML .

Mas o problema é que preciso chamar o dataTable () na tabela # line_items APÓS o DOM estar pronto. Minha diretiva acima chama o método dataTable () ANTES que o modelo seja anexado ao elemento da diretiva. Existe uma maneira de chamar funções após o acréscimo?

Obrigado pela ajuda!

ATUALIZAÇÃO 1 após a resposta de Andy:

Quero ter certeza de que o método link só é chamado DEPOIS de que tudo esteja na página, então alterei a diretiva para um pequeno teste:

# CoffeeScript
#angular.module(...)
.directive 'dashboardTable', ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.find('#sayboo').html('boo')

      controller: lineItemIndexCtrl
      template: "<div id='sayboo'></div>"

    }

E eu realmente vejo "boo" no div # sayboo.

Então eu tento minha chamada de dados jquery

.directive 'dashboardTable',  ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.parent('table').dataTable() # NEW LINE

      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

Sem sorte ai

Então tento adicionar um tempo limite:

.directive 'dashboardTable', ($timeout) ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        $timeout -> # NEW LINE
          element.parent('table').dataTable()
        ,5000
      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

E isso funciona. Então, eu me pergunto o que há de errado na versão não temporizada do código?

Nik So
fonte
1
@ adardesign Não, eu nunca fiz, eu tive que usar um temporizador. Por alguma razão, o retorno de chamada não é aqui, na verdade. Eu tenho uma tabela com 11 colunas e centenas de linhas, então, naturalmente, angular parece uma boa aposta para ligação de dados; mas também preciso usar o plugin jquery Datatables, que é tão simples quanto $ ('table'). datatable (). Usando diretiva ou apenas ter um objeto json burro com todas as linhas e usar ng-repeat para iterar, não consigo obter meu $ (). Datatable () para executar DEPOIS que o elemento html da tabela é renderizado, portanto, meu truque atualmente é temporizar para verificar se $ ( 'tr') comprimento> 3 (b / c de cabeçalho / rodapé).
Nik Então,
2
@adardesign E sim, eu tentei todo o método de compilação, método de compilação retornando um objeto contendo os métodos postLink / preLink, método de compilação retornando apenas uma função (a função de vinculação), método de vinculação (sem o método de compilação porque, tanto quanto posso dizer, se você tem um método de compilação que retorna um método de vinculação, a função de vinculação é ignorada). Nenhuma funcionou; portanto, é necessário confiar no bom e antigo $ timeout. Atualizarei este post se eu encontrar algo que funcione melhor ou simplesmente quando descobrir que o retorno de chamada realmente funciona como retorno de chamada
Nik So

Respostas:

215

Se o segundo parâmetro, "atraso" não for fornecido, o comportamento padrão é executar a função após o DOM concluir a renderização. Então, em vez de setTimeout, use $ timeout:

$timeout(function () {
    //DOM has finished rendering
});
parlamento
fonte
8
Por que isso não é explicado nos documentos ?
Gaui
23
Você está certo, minha resposta é um pouco enganadora, porque tentei simplificar. A resposta completa é que esse efeito não é resultado do Angular, mas do navegador. $timeout(fn)em última análise, chama o setTimeout(fn, 0)que interrompe a execução do Javascript e permite que o navegador renderize o conteúdo primeiro, antes de continuar a execução desse Javascript.
parlamento
7
Pense no navegador como enfileirar determinadas tarefas, como "execução de javascript" e "renderização DOM" separadamente, e o que setTimeout (fn, 0) leva a "execução javascript" atualmente em execução para o fundo da fila, após renderizar .
parliament
2
@GabLeRoux sim, isso terá o mesmo efeito, exceto que $ timeout tem o benefício adicional de chamar $ scope. $ Apply () após a execução. Com _.defer (), você precisará chamá-lo manualmente se myFunction alterar variáveis ​​no escopo.
parliament
2
Estou tendo um cenário em que isso não ajuda, na página1 ng-repeat processa vários elementos, depois vou para a página2 e depois volto para a página1 e tento obter o máximo dos elementos ng-repeat ... retorna a altura errada. Se eu esgotar o tempo limite por 1000ms, funciona.
yodalr
14

Eu tive o mesmo problema e acredito que a resposta realmente é não. Veja o comentário de Miško e algumas discussões no grupo .

O Angular pode rastrear que todas as chamadas de função que ele faz para manipular o DOM estão concluídas, mas como essas funções podem acionar uma lógica assíncrona que ainda atualiza o DOM após o retorno, não se espera que o Angular saiba sobre isso. Qualquer retorno de chamada que a Angular fornece pode funcionar algumas vezes, mas não seria seguro confiar nela.

Resolvemos isso heuristicamente com um setTimeout, como você fez.

(Lembre-se de que nem todos concordam comigo - você deve ler os comentários nos links acima e ver o que pensa.)

Roy Truelove
fonte
7

Você pode usar a função 'link', também conhecida como postLink, que é executada após a inserção do modelo.

app.directive('myDirective', function() {
  return {
    link: function(scope, elm, attrs) { /*I run after template is put in */ },
    template: '<b>Hello</b>'
  }
});

Leia isto se você planeja fazer diretrizes, é uma grande ajuda: http://docs.angularjs.org/guide/directive

Andrew Joslin
fonte
Olá Andy, muito obrigado por responder; Tentei a função de link, mas não me importaria em tentar novamente exatamente como você codifica; Passei os últimos 1,5 dias lendo essa página de diretiva; e vendo também os exemplos no site da angular. Tentará seu código agora.
Nik Então,
Ah, agora vejo que você estava tentando criar um link, mas estava fazendo errado. Se você apenas retornar uma função, ela será assumida como link. Se você retornar um objeto, precisará devolvê-lo com a chave como 'link'. Você também pode retornar uma função de vinculação da sua função de compilação.
Andrew Joslin
Oi Andy, recuperei meus resultados; Eu quase perdi minha sanidade, porque realmente fiz basicamente qual é a sua resposta aqui. Por favor, veja a minha atualização
Nik So
Humm, tente algo como: <table id = "bob"> <tbody dashboard-table = "# bob"> </tbody> </table> Em seguida, no seu link, faça $ (attrs.dashboardTable) .dataTable () para verifique se está sendo selecionado corretamente. Ou eu acho que você já tentou isso .. Não tenho certeza se o link não está funcionando.
Andrew Joslin
Este método funcionou para mim, eu queria mover elementos dentro da dom post rendering do modelo para o meu requisito, fiz isso na função de
link.Obrigado
7

Embora minha resposta não esteja relacionada às tabelas de dados, ela aborda a questão da manipulação do DOM e, por exemplo, a inicialização do plugin jQuery para diretivas usadas em elementos com conteúdo atualizado de maneira assíncrona.

Em vez de implementar um tempo limite, pode-se adicionar um relógio que ouvirá alterações no conteúdo (ou até gatilhos externos adicionais).

No meu caso, usei essa solução alternativa para inicializar um plug-in jQuery depois que o ng-repeat foi feito, o que criou meu DOM interno - em outro caso, usei-o apenas para manipular o DOM depois que a propriedade do escopo foi alterada no controlador. Aqui está como eu fiz ...

HTML:

<div my-directive my-directive-watch="!!myContent">{{myContent}}</div>

JS:

app.directive('myDirective', [ function(){
    return {
        restrict : 'A',
        scope : {
            myDirectiveWatch : '='
        },
        compile : function(){
            return {
                post : function(scope, element, attributes){

                    scope.$watch('myDirectiveWatch', function(newVal, oldVal){
                        if (newVal !== oldVal) {
                            // Do stuff ...
                        }
                    });

                }
            }
        }
    }
}]);

Nota: Em vez de apenas converter a variável myContent para boolear no atributo my-Directive-watch, pode-se imaginar qualquer expressão arbitrária lá.

Nota: O isolamento do escopo, como no exemplo acima, pode ser feito apenas uma vez por elemento - tentar fazer isso com várias diretivas no mesmo elemento resultará em um erro $ compile: multidir - consulte: https://docs.angularjs.org / error / $ compile / multidir

conceptdeluxe
fonte
7

Pode chegar tarde para responder a esta pergunta. Mas ainda assim alguém pode se beneficiar da minha resposta.

Eu tive um problema semelhante e, no meu caso, não posso alterar a diretiva, pois é uma biblioteca e alterar um código da biblioteca não é uma boa prática. Então, o que eu fiz foi usar uma variável para aguardar o carregamento da página e usar ng-if dentro do meu html para esperar renderizar o elemento específico.

No meu controlador:

$scope.render=false;

//this will fire after load the the page

angular.element(document).ready(function() {
    $scope.render=true;
});

No meu html (no meu caso, o componente html é uma tela)

<canvas ng-if="render"> </canvas>
Madura Pradeep
fonte
3

Eu tive o mesmo problema, mas usando Angular + DataTable com um fnDrawCallback+ agrupamento de linhas + $ diretivas aninhadas compiladas. Coloquei o $ timeout na minha fnDrawCallbackfunção para corrigir a renderização da paginação.

Antes do exemplo, com base na fonte row_grouping:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  for(var i=0; i<nTrs.length; i++){
     //1. group rows per row_grouping example
     //2. $compile html templates to hook datatable into Angular lifecycle
  }
}

Após o exemplo:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  $timeout(function requiredRenderTimeoutDelay(){
    for(var i=0; i<nTrs.length; i++){
       //1. group rows per row_grouping example
       //2. $compile html templates to hook datatable into Angular lifecycle
    }
  ,50); //end $timeout
}

Mesmo um pequeno atraso de tempo limite foi suficiente para permitir que o Angular processasse minhas diretivas compiladas do Angular.

JJ Zabkar
fonte
Apenas curioso, você tem uma mesa bastante grande com muitas colunas? porque eu achei que eu preciso de um muito chato de milissegundos (> 100), de modo a não deixar que a chamada dataTable () para sufocar
Nik Então,
Descobri que o problema ocorreu na navegação da página DataTable para conjuntos de resultados de 2 linhas a mais de 150 linhas. Portanto, não - não acho que o tamanho da tabela tenha sido o problema, mas talvez o DataTable tenha adicionado sobrecarga de renderização suficiente para eliminar alguns desses milissegundos. Meu foco era fazer com que o agrupamento de linhas funcionasse no DataTable com o mínimo de integração do AngularJS.
JJ Zabkar #
2

Nenhuma das soluções trabalhadas para mim aceita o uso de um tempo limite. Isso ocorre porque eu estava usando um modelo que estava sendo criado dinamicamente durante o postLink.

Observe, no entanto, pode haver um tempo limite de '0', pois o tempo limite adiciona a função que está sendo chamada à fila do navegador, o que ocorrerá após o mecanismo de renderização angular, pois ele já está na fila.

Consulte: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering

Michael Smolenski
fonte
0

Aqui está uma diretiva para que as ações sejam programadas após uma renderização superficial. Por superficial, quero dizer que ele será avaliado depois desse elemento renderizado e que não terá relação com o momento em que seu conteúdo for renderizado. Portanto, se você precisar de algum subelemento executando uma ação pós-renderização, considere usá-la lá:

define(['angular'], function (angular) {
  'use strict';
  return angular.module('app.common.after-render', [])
    .directive('afterRender', [ '$timeout', function($timeout) {
    var def = {
        restrict : 'A', 
        terminal : true,
        transclude : false,
        link : function(scope, element, attrs) {
            if (attrs) { scope.$eval(attrs.afterRender) }
            scope.$emit('onAfterRender')
        }
    };
    return def;
    }]);
});

então você pode fazer:

<div after-render></div>

ou com qualquer expressão útil como:

<div after-render="$emit='onAfterThisConcreteThingRendered'"></div>

Sebastian Sastre
fonte
Isso não ocorre depois que o conteúdo é renderizado. Se eu tivesse uma expressão dentro do elemento <div after-render> {{blah}} </div> neste momento, a expressão ainda não será avaliada. O conteúdo da div ainda está {{blah}} dentro da função de link. Portanto, tecnicamente, você está disparando o evento antes que o conteúdo seja renderizado.
Edward Olamisan
Esta é uma rasa após tornar ação, eu nunca disse que ela seja profunda
Sebastian Sastre
0

Eu consegui isso trabalhando com a seguinte diretiva:

app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});

E no HTML:

<table class="table table-hover dataTable dataTable-columnfilter " datatable-setup="">

resolução de problemas se o acima não funcionar para você.

1) observe que 'datatableSetup' é o equivalente a 'datatable-setup'. Angular altera o formato para estojo de camelo.

2) verifique se o aplicativo está definido antes da diretiva. por exemplo, definição e diretiva simples de aplicativo.

var app = angular.module('app', []);
app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});
Anton
fonte
0

Após o fato de que a ordem de carregamento não pode ser antecipada, uma solução simples pode ser usada.

Vejamos o relacionamento diretivo-usuário da diretiva. Normalmente, o usuário da diretiva fornecerá alguns dados à diretiva ou usará algumas funcionalidades (funções) que a diretiva fornece. A diretiva, por outro lado, espera que algumas variáveis ​​sejam definidas em seu escopo.

Se pudermos garantir que todos os jogadores cumpram todos os requisitos de ação antes de tentar executá-las - tudo deve estar bem.

E agora a diretiva:

app.directive('aDirective', function () {
    return {
        scope: {
            input: '=',
            control: '='
        },
        link: function (scope, element) {
            function functionThatNeedsInput(){
                //use scope.input here
            }
            if ( scope.input){ //We already have input 
                functionThatNeedsInput();
            } else {
                scope.control.init = functionThatNeedsInput;
            }
          }

        };
})

e agora o usuário da diretiva html

<a-directive control="control" input="input"></a-directive>

e em algum lugar no controlador do componente que usa a diretiva:

$scope.control = {};
...
$scope.input = 'some data could be async';
if ( $scope.control.functionThatNeedsInput){
    $scope.control.functionThatNeedsInput();
}

É sobre isso. Há muita sobrecarga, mas você pode perder o tempo limite de $. Também assumimos que o componente que usa a diretiva é instanciado antes da diretiva, porque dependemos da variável de controle que existe quando a diretiva é instanciada.

Eli
fonte