Diretivas angulares - quando e como usar compilação, controlador, pré-link e pós-link [fechado]

451

Ao escrever uma diretiva Angular, é possível usar qualquer uma das seguintes funções para manipular o comportamento do DOM, o conteúdo e a aparência do elemento no qual a diretiva é declarada:

  • compilar
  • controlador
  • pré-link
  • pós-link

Parece haver alguma confusão sobre qual função deve ser usada. Esta pergunta abrange:

Princípios básicos da diretiva

Natureza da função, faça e não faça

Perguntas relacionadas:

Izhaki
fonte
27
O que o que?
haimlit
2
@Ian See: Sobrecarga do operador . Essencialmente, isso é destinado ao wiki da comunidade. Muitas respostas para as perguntas relacionadas são parciais, não fornecendo a imagem completa.
Izhaki 07/07
8
Esse é um ótimo conteúdo, mas solicitamos que tudo aqui seja mantido no formato de perguntas e respostas. Talvez você queira dividir isso em várias perguntas discretas e depois vinculá-las a partir da tag wiki?
Flexo
57
Mesmo que este post seja fora do tópico e em formato de blog, foi mais útil para fornecer uma explicação detalhada das diretivas Angular. Por favor, não exclua este post, administradores!
Exegesis 01/10
12
Honestamente, eu nem me preocupo com os documentos originais. Uma postagem de stackoverflow ou um blog geralmente me leva em questão de segundos, em comparação com os 15 a 30 minutos de arrancar o cabelo tentando entender os documentos originais.
David

Respostas:

168

Em que ordem as funções de diretiva são executadas?

Para uma única diretiva

Com base na seguinte plunk , considere a seguinte marcação HTML:

<body>
    <div log='some-div'></div>
</body>

Com a seguinte declaração de diretiva:

myApp.directive('log', function() {

    return {
        controller: function( $scope, $element, $attrs, $transclude ) {
            console.log( $attrs.log + ' (controller)' );
        },
        compile: function compile( tElement, tAttributes ) {
            console.log( tAttributes.log + ' (compile)'  );
            return {
                pre: function preLink( scope, element, attributes ) {
                    console.log( attributes.log + ' (pre-link)'  );
                },
                post: function postLink( scope, element, attributes ) {
                    console.log( attributes.log + ' (post-link)'  );
                }
            };
         }
     };  

});

A saída do console será:

some-div (compile)
some-div (controller)
some-div (pre-link)
some-div (post-link)

Podemos ver que compileé executado primeiro, depois controller, depois pre-linke último post-link.

Para diretivas aninhadas

Nota: O seguinte não se aplica a diretivas que processam seus filhos em sua função de link. Algumas diretivas angulares fazem isso (como ngIf, ngRepeat ou qualquer diretiva com transclude). Essas diretivas nativamente terão sua linkfunção chamada antes que suas diretivas filho compilesejam chamadas.

A marcação HTML original geralmente é feita de elementos aninhados, cada um com sua própria diretiva. Como na seguinte marcação (veja plunk ):

<body>
    <div log='parent'>
        <div log='..first-child'></div>
        <div log='..second-child'></div>
    </div>
</body>

A saída do console ficará assim:

// The compile phase
parent (compile)
..first-child (compile)
..second-child (compile)

// The link phase   
parent (controller)
parent (pre-link)
..first-child (controller)
..first-child (pre-link)
..first-child (post-link)
..second-child (controller)
..second-child (pre-link)
..second-child (post-link)
parent (post-link)

Podemos distinguir duas fases aqui - a fase de compilação e a fase de link .

A fase de compilação

Quando o DOM é carregado, o Angular inicia a fase de compilação, onde percorre a marcação de cima para baixo e chama compiletodas as diretivas. Graficamente, poderíamos expressá-lo assim:

Uma imagem que ilustra o loop de compilação para crianças

Talvez seja importante mencionar que, nesse estágio, os modelos que a função de compilação obtém são os modelos de origem (não o modelo de instância).

A fase do link

As instâncias do DOM geralmente são simplesmente o resultado de um modelo de origem sendo renderizado no DOM, mas podem ser criados ng-repeatou introduzidos em tempo real.

Sempre que uma nova instância de um elemento com uma diretiva é renderizada no DOM, a fase do link é iniciada.

Nesta fase, Angular chama controller, pre-linkitera filhos e chama post-linktodas as diretivas, como:

Uma ilustração demonstrando as etapas da fase do link

Izhaki
fonte
5
@lzhaki O fluxograma parece bom. Importa-se de compartilhar o nome da ferramenta de gráficos? :)
merlin
1
@merlin Eu usei o OmniGraffle (mas poderia ter usado o illustrator ou o inkscape - além da velocidade, não há nada que o OmniGraffle faça melhor do que outras ferramentas de gráficos no que diz respeito a esta ilustração).
21914 Izhaki
2
@ Plunker de Anant desapareceu então aqui está um novo: plnkr.co/edit/kZZks8HN0iFIY8ZaKJkA?p=preview Abrir o console JS para ver as instruções de log
POR QUE isso não é verdade quando ng-repeat é usado para diretivas filhos ??? Veja o plunk: plnkr.co/edit/HcH4r6GV5jAFC3yOZknc?p=preview
Luckylooke
@Luckylooke Seu plunk não tem filhos com diretiva sob ng-repeat (ou seja, o que está sendo repetido é um modelo com uma diretiva. Se isso acontecer, você verá que sua compilação é chamada apenas após o link de ng-repeat.
Izhaki
90

O que mais acontece entre essas chamadas de função?

As várias funções directiva são executados a partir de dentro de duas outras funções angulares chamados $compile(onde a directiva da compilefor executado) e uma função de chamada interno nodeLinkFn(onde o da directiva controller, preLinke postLinksão executadas). Várias coisas acontecem dentro da função angular antes e depois das funções diretivas serem chamadas. Talvez o mais notável seja a recursão infantil. A ilustração simplificada a seguir mostra as principais etapas nas fases de compilação e link:

Uma ilustração mostrando as fases de compilação e link angular

Para demonstrar essas etapas, vamos usar a seguinte marcação HTML:

<div ng-repeat="i in [0,1,2]">
    <my-element>
        <div>Inner content</div>
    </my-element>
</div>

Com a seguinte diretiva:

myApp.directive( 'myElement', function() {
    return {
        restrict:   'EA',
        transclude: true,
        template:   '<div>{{label}}<div ng-transclude></div></div>'
    }
});

Compilar

A compileAPI tem a seguinte aparência:

compile: function compile( tElement, tAttributes ) { ... }

Geralmente, os parâmetros são prefixados com t para significar que os elementos e atributos fornecidos são os do modelo de origem, e não os da instância.

Antes da chamada para o compileconteúdo transcluído (se houver), é removido e o modelo é aplicado à marcação. Assim, o elemento fornecido para a compilefunção terá a seguinte aparência:

<my-element>
    <div>
        "{{label}}"
        <div ng-transclude></div>
    </div>
</my-element>

Observe que o conteúdo transcluído não é reinserido neste momento.

Após a chamada para a diretiva de .compile , o Angular percorrerá todos os elementos filhos, incluindo aqueles que podem ter sido introduzidos pela diretiva (os elementos do modelo, por exemplo).

Criação de Instância

No nosso caso, três instâncias do modelo de origem acima serão criadas (por ng-repeat ). Portanto, a sequência a seguir será executada três vezes, uma vez por instância.

Controlador

A controllerAPI envolve:

controller: function( $scope, $element, $attrs, $transclude ) { ... }

Entrando na fase de link, a função de link retornada via $compileagora é fornecida com um escopo.

Primeiro, a função de link cria um escopo filho ( scope: true) ou um escopo isolado ( scope: {...}), se solicitado.

O controlador é então executado, fornecido com o escopo do elemento de instância.

Pré-link

A pre-linkAPI tem a seguinte aparência:

function preLink( scope, element, attributes, controller ) { ... }

Praticamente nada acontece entre a chamada para a diretiva .controllere a.preLink função. A Angular ainda fornece recomendações sobre como cada uma deve ser usada.

Seguindo o .preLink chamada, a função de link percorrerá cada elemento filho - chamando a função de link correta e anexando a ele o escopo atual (que serve como o escopo pai dos elementos filhos).

Post-link

A post-linkAPI é semelhante à da pre-linkfunção:

function postLink( scope, element, attributes, controller ) { ... }

Talvez valha a pena notar que, uma vez .postLinkchamada a função de uma diretiva , o processo de vinculação de todos os seus elementos filhos foi concluído, incluindo todas as .postLinkfunções das crianças .

Isso significa que, quando o tempo .postLinké chamado, as crianças estão 'vivas' e estão prontas. Isso inclui:

  • ligação de dados
  • transclusão aplicada
  • escopo anexado

O modelo nesse estágio ficará assim:

<my-element>
    <div class="ng-binding">
        "{{label}}"
        <div ng-transclude>                
            <div class="ng-scope">Inner content</div>
        </div>
    </div>
</my-element>
Izhaki
fonte
3
Como você criou esse desenho?
Royi Namir
6
@RoyiNamir Omnigraffle.
21815 Izhaki #
43

Como declarar as várias funções?

Compilar, Controlador, Pré-link e Pós-link

Se alguém usar todas as quatro funções, a diretiva seguirá este formato:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.
            return {
                pre: function preLink( scope, element, attributes, controller, transcludeFn ) {
                    // Pre-link code goes here
                },
                post: function postLink( scope, element, attributes, controller, transcludeFn ) {
                    // Post-link code goes here
                }
            };
        }
    };  
});

Observe que a compilação retorna um objeto que contém as funções de pré-link e pós-link; no jargão angular, dizemos que a função de compilação retorna uma função de modelo .

Compilar, Controlar e Pós-Link

Se pre-linknão for necessário, a função de compilação pode simplesmente retornar a função de pós-link em vez de um objeto de definição, assim:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.
            return function postLink( scope, element, attributes, controller, transcludeFn ) {
                    // Post-link code goes here                 
            };
        }
    };  
});

Às vezes, deseja-se adicionar um compilemétodo, após a linkdefinição do método (post) . Para isso, pode-se usar:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.

            return this.link;
        },
        link: function( scope, element, attributes, controller, transcludeFn ) {
            // Post-link code goes here
        }

    };  
});

Controlador e pós-link

Se nenhuma função de compilação for necessária, é possível pular completamente sua declaração e fornecer a função pós-link sob a linkpropriedade do objeto de configuração da diretiva:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        link: function postLink( scope, element, attributes, controller, transcludeFn ) {
                // Post-link code goes here                 
        },          
    };  
});

Sem controlador

Em qualquer um dos exemplos acima, pode-se simplesmente remover a controllerfunção, se não for necessário. Por exemplo, se apenas uma post-linkfunção é necessária, pode-se usar:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        link: function postLink( scope, element, attributes, controller, transcludeFn ) {
                // Post-link code goes here                 
        },          
    };  
});
Izhaki
fonte
31

Qual é a diferença entre um modelo de origem e um modelo de instância ?

O fato de Angular permitir a manipulação de DOM significa que a marcação de entrada no processo de compilação às vezes difere da saída. Particularmente, algumas marcações de entrada podem ser clonadas algumas vezes (como com ng-repeat) antes de serem renderizadas no DOM.

A terminologia angular é um pouco inconsistente, mas ainda distingue entre dois tipos de marcações:

  • Modelo de origem - a marcação a ser clonada, se necessário. Se clonada, essa marcação não será renderizada no DOM.
  • Modelo de instância - a marcação real a ser renderizada no DOM. Se a clonagem estiver envolvida, cada instância será um clone.

A seguinte marcação demonstra isso:

<div ng-repeat="i in [0,1,2]">
    <my-directive>{{i}}</my-directive>
</div>

O html de origem define

    <my-directive>{{i}}</my-directive>

que serve como modelo de origem.

Mas, como está dentro de uma ng-repeatdiretiva, esse modelo de origem será clonado (3 vezes no nosso caso). Esses clones são modelo de instância, cada um aparecerá no DOM e será vinculado ao escopo relevante.

Izhaki
fonte
23

Função de compilação

Cada diretiva compile função de é chamada apenas uma vez, quando o Angular é iniciado.

Oficialmente, este é o local para executar manipulações de modelo (de origem) que não envolvem escopo ou ligação de dados.

Principalmente, isso é feito para fins de otimização; considere a seguinte marcação:

<tr ng-repeat="raw in raws">
    <my-raw></my-raw>
</tr>

A <my-raw>diretiva renderizará um conjunto específico de marcação DOM. Para que possamos:

  • Permita ng-repeatduplicar o modelo de origem ( <my-raw>) e modifique a marcação de cada modelo de instância (fora da compilefunção).
  • Modifique o modelo de origem para envolver a marcação desejada (na compilefunção) e permita ng-repeatduplicá-la.

Se houver 1000 itens na rawscoleção, a última opção poderá ser mais rápida que a anterior.

Faz:

  • Manipule a marcação para que ela sirva como modelo para instâncias (clones).

Não

  • Anexe manipuladores de eventos.
  • Inspecione elementos filho.
  • Configure observações sobre atributos.
  • Configure relógios no escopo.
Izhaki
fonte
20

Função de controlador

A controllerfunção de cada diretiva é chamada sempre que um novo elemento relacionado é instanciado.

Oficialmente, a controllerfunção é onde:

  • Define a lógica do controlador (métodos) que pode ser compartilhada entre os controladores.
  • Inicia variáveis ​​de escopo.

Novamente, é importante lembrar que, se a diretiva envolver um escopo isolado, quaisquer propriedades nela herdadas do escopo pai ainda não estarão disponíveis.

Faz:

  • Definir lógica do controlador
  • Iniciar variáveis ​​de escopo

Não:

  • Inspecione os elementos filhos (eles ainda não podem ser renderizados, vinculados ao escopo etc.).
Izhaki
fonte
Que bom que você mencionou o Controller dentro da diretiva é um ótimo lugar para inicializar o escopo. Tive dificuldade para descobrir isso.
Jsbisht #
1
O controlador NÃO "Inicia o escopo", apenas acessa o escopo já iniciado independentemente dele.
Dmitri Zaitsev
@DmitriZaitsev boa atenção aos detalhes. Eu alterei o texto.
21715 Izhaki #
19

Função pós-link

Quando a post-linkfunção é chamada, todas as etapas anteriores ocorreram - encadernação, transclusão etc.

Normalmente, este é um local para manipular ainda mais o DOM renderizado.

Faz:

  • Manipule elementos DOM (renderizados e, portanto, instanciados).
  • Anexe manipuladores de eventos.
  • Inspecione elementos filho.
  • Configure observações sobre atributos.
  • Configure relógios no escopo.
Izhaki
fonte
9
Caso alguém esteja usando a função de link (sem pré-link ou pós-link), é bom saber que é equivalente ao post-link.
Asaf David
15

Função pré-link

A pre-linkfunção de cada diretiva é chamada sempre que um novo elemento relacionado é instanciado.

Como visto anteriormente na seção de ordem de compilação, as pre-linkfunções são chamadas pai-e-filho, enquanto as post-linkfunções são chamadas child-then-parent.

A pre-linkfunção raramente é usada, mas pode ser útil em cenários especiais; por exemplo, quando um controlador filho se registra no controlador pai, mas o registro deve ser de uma parent-then-childmaneira ( ngModelControllerfaz as coisas dessa maneira).

Não:

  • Inspecione os elementos filhos (eles ainda não podem ser renderizados, vinculados ao escopo etc.).
Izhaki
fonte