Adicionar diretivas da diretiva em AngularJS

Estou tentando construir uma diretiva que cuida de adicionar mais diretivas ao elemento em que ela está declarada. Por exemplo, eu quero construir uma diretiva que cuida da adição de datepicker , datepicker-language e ng-required="true" .

Se eu tentar adicionar esses atributos e então usar $compile eu obviamente gero um loop infinito, então estou verificando se já adicionei os atributos necessários:

 angular.module('app') .directive('superDirective', function ($compile, $injector) { return { restrict: 'A', replace: true, link: function compile(scope, element, attrs) { if (element.attr('datepicker')) { // check return; } element.attr('datepicker', 'someValue'); element.attr('datepicker-language', 'en'); // some more $compile(element)(scope); } }; }); 

É claro que, se eu não $compile o elemento, os atributos serão definidos, mas a diretiva não será inicializada.

Esta abordagem é correta ou estou fazendo errado? Existe uma maneira melhor de alcançar o mesmo comportamento?

UDPATE : dado o fato de que $compile é a única maneira de conseguir isso, existe uma maneira de pular o primeiro passo de compilation (o elemento pode conter vários filhos)? Talvez configurando o terminal:true ?

ATUALIZAÇÃO 2 : Eu tentei colocar a diretiva em um elemento select e, como esperado, a compilation é executada duas vezes, o que significa que há duas vezes o número de option esperadas.

Nos casos em que você tem várias diretivas em um único elemento DOM e onde a ordem em que elas são aplicadas é importante, você pode usar a propriedade priority para solicitar o aplicativo. Números mais altos são executados primeiro. A prioridade padrão é 0 se você não especificar um.

EDIT : após a discussão, aqui está a solução completa de trabalho. A chave era remover o atributo : element.removeAttr("common-things"); e também element.removeAttr("data-common-things"); (caso os usuários especifiquem data-common-things aos data-common-things no html)

 angular.module('app') .directive('commonThings', function ($compile) { return { restrict: 'A', replace: false, terminal: true, //this setting is important, see explanation below priority: 1000, //this setting is important, see explanation below compile: function compile(element, attrs) { element.attr('tooltip', '{{dt()}}'); element.attr('tooltip-placement', 'bottom'); element.removeAttr("common-things"); //remove the attribute to avoid indefinite loop element.removeAttr("data-common-things"); //also remove the same attribute with data- prefix in case users specify data-common-things in the html return { pre: function preLink(scope, iElement, iAttrs, controller) { }, post: function postLink(scope, iElement, iAttrs, controller) { $compile(iElement)(scope); } }; } }; }); 

Plunker de trabalho está disponível em: http://plnkr.co/edit/Q13bUt?p=preview

Ou:

 angular.module('app') .directive('commonThings', function ($compile) { return { restrict: 'A', replace: false, terminal: true, priority: 1000, link: function link(scope,element, attrs) { element.attr('tooltip', '{{dt()}}'); element.attr('tooltip-placement', 'bottom'); element.removeAttr("common-things"); //remove the attribute to avoid indefinite loop element.removeAttr("data-common-things"); //also remove the same attribute with data- prefix in case users specify data-common-things in the html $compile(element)(scope); } }; }); 

DEMO

Explicação porque temos que definir terminal: true e priority: 1000 (um número alto):

Quando o DOM está pronto, o angular percorre o DOM para identificar todas as diretivas registradas e compilar as diretivas uma a uma com base na priority se essas diretivas estiverem no mesmo elemento . Nós definimos a prioridade de nossa diretiva personalizada para um número alto para garantir que ela seja compilada primeiro e com terminal: true , as outras diretivas serão ignoradas após a compilation desta diretiva.

Quando nossa diretiva personalizada é compilada, ela modificará o elemento adicionando diretivas e removendo a si mesmo e usando o $ compile service para compilar todas as diretivas (incluindo aquelas que foram ignoradas) .

Se não definirmos terminal:true e priority: 1000 , há uma chance de que algumas diretivas sejam compiladas antes de nossa diretiva personalizada. E quando nossa diretiva personalizada usa $ compile para compilar o elemento => compile novamente as diretivas já compiladas. Isso causará um comportamento imprevisível, especialmente se as diretivas compiladas antes de nossa diretiva personalizada já tiverem transformado o DOM.

Para mais informações sobre prioridade e terminal, confira Como entender o `terminal` da diretiva?

Um exemplo de uma diretiva que também modifica o modelo é ng-repeat (priority = 1000), quando ng-repeat é compilado, ng-repeat faz cópias do elemento template antes que outras diretivas sejam aplicadas .

Graças ao comentário do @ Izhaki, aqui está a referência ao código-fonte ngRepeat : https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

Você pode realmente lidar com tudo isso com apenas uma tag de modelo simples. Veja http://jsfiddle.net/m4ve9/ para um exemplo. Note que eu realmente não precisei de uma propriedade de compilation ou link na definição super-diretiva.

Durante o processo de compilation, o Angular puxa os valores do modelo antes de compilar, para que você possa append outras diretivas e o Angular cuidará disso para você.

Se esta for uma diretiva super que precisa preservar o conteúdo interno original, você pode usar transclude : true e replace o interior por

Espero que ajude, deixe-me saber se alguma coisa não está clara

Alex

Aqui está uma solução que move as diretivas que precisam ser adicionadas dinamicamente, na visão e também adiciona alguma lógica condicional (básica) opcional. Isso mantém a diretiva limpa sem nenhuma lógica codificada.

A diretiva toma uma matriz de objects, cada object contém o nome da diretiva a ser adicionada e o valor a ser passado (se houver).

Eu estava lutando para pensar em um caso de uso para uma diretiva como essa até que achei que seria útil adicionar alguma lógica condicional que apenas adiciona uma diretiva com base em alguma condição (embora a resposta abaixo ainda seja planejada). Eu adicionei uma propriedade if opcional que deve conter um valor de bool, expressão ou function (por exemplo, definido em seu controlador) que determina se a diretiva deve ser adicionada ou não.

Também estou usando attrs.$attr.dynamicDirectives para obter a declaração de atributo exata usada para adicionar a diretiva (por exemplo, data-dynamic-directive , dynamic-directive ) sem precisar codificar os valores de string para codificar.

Demonstração Plunker

 angular.module('plunker', ['ui.bootstrap']) .controller('DatepickerDemoCtrl', ['$scope', function($scope) { $scope.dt = function() { return new Date(); }; $scope.selects = [1, 2, 3, 4]; $scope.el = 2; // For use with our dynamic-directive $scope.selectIsRequired = true; $scope.addTooltip = function() { return true; }; } ]) .directive('dynamicDirectives', ['$compile', function($compile) { var addDirectiveToElement = function(scope, element, dir) { var propName; if (dir.if) { propName = Object.keys(dir)[1]; var addDirective = scope.$eval(dir.if); if (addDirective) { element.attr(propName, dir[propName]); } } else { // No condition, just add directive propName = Object.keys(dir)[0]; element.attr(propName, dir[propName]); } }; var linker = function(scope, element, attrs) { var directives = scope.$eval(attrs.dynamicDirectives); if (!directives || !angular.isArray(directives)) { return $compile(element)(scope); } // Add all directives in the array angular.forEach(directives, function(dir){ addDirectiveToElement(scope, element, dir); }); // Remove attribute used to add this directive element.removeAttr(attrs.$attr.dynamicDirectives); // Compile element to run other directives $compile(element)(scope); }; return { priority: 1001, // Run before other directives eg ng-repeat terminal: true, // Stop other directives running link: linker }; } ]); 
          

Eu queria adicionar a minha solução, já que a que aceitou não funcionou bem para mim.

Eu precisava adicionar uma diretiva, mas também manter o meu no elemento.

Neste exemplo, estou adicionando uma diretiva simples no estilo ng ao elemento. Para evitar loops infinitos de compilation e permitir que eu mantivesse minha diretiva, adicionei uma verificação para ver se o que eu adicionei estava presente antes de recompilar o elemento.

 angular.module('some.directive', []) .directive('someDirective', ['$compile',function($compile){ return { priority: 1001, controller: ['$scope', '$element', '$attrs', '$transclude' ,function($scope, $element, $attrs, $transclude) { // controller code here }], compile: function(element, attributes){ var compile = false; //check to see if the target directive was already added if(!element.attr('ng-style')){ //add the target directive element.attr('ng-style', "{'width':'200px'}"); compile = true; } return { pre: function preLink(scope, iElement, iAttrs, controller) { }, post: function postLink(scope, iElement, iAttrs, controller) { if(compile){ $compile(iElement)(scope); } } }; } }; }]); 

Tente armazenar o estado em um atributo no próprio elemento, como superDirectiveStatus="true"

Por exemplo:

 angular.module('app') .directive('superDirective', function ($compile, $injector) { return { restrict: 'A', replace: true, link: function compile(scope, element, attrs) { if (element.attr('datepicker')) { // check return; } var status = element.attr('superDirectiveStatus'); if( status !== "true" ){ element.attr('datepicker', 'someValue'); element.attr('datepicker-language', 'en'); // some more element.attr('superDirectiveStatus','true'); $compile(element)(scope); } } }; }); 

Espero que isso ajude você.

Houve uma mudança de 1.3.x para 1.4.x.

Em Angular 1.3.x isso funcionou:

 var dir: ng.IDirective = { restrict: "A", require: ["select", "ngModel"], compile: compile, }; function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) { tElement.append(""); return function postLink(scope: DirectiveScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) { attributes["ngOptions"] = "a.ID as a.Bezeichnung for a in akademischetitel"; scope.akademischetitel = AkademischerTitel.query(); } } 

Agora em Angular 1.4.x nós temos que fazer isso:

 var dir: ng.IDirective = { restrict: "A", compile: compile, terminal: true, priority: 10, }; function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) { tElement.append(""); tElement.removeAttr("tq-akademischer-titel-select"); tElement.attr("ng-options", "a.ID as a.Bezeichnung for a in akademischetitel"); return function postLink(scope: DirectiveScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) { $compile(element)(scope); scope.akademischetitel = AkademischerTitel.query(); } } 

(Da resposta aceita: https://stackoverflow.com/a/19228302/605586 do Khanh TO).

Uma solução simples que poderia funcionar em alguns casos é criar e $ compilar um wrapper e, em seguida, append seu elemento original a ele.

Algo como…

 link: function(scope, elem, attr){ var wrapper = angular.element('
'); elem.before(wrapper); $compile(wrapper)(scope); wrapper.append(elem); }

Essa solução tem a vantagem de manter as coisas simples, não recompilando o elemento original.

Isso não funcionaria se alguma das diretivas adicionadas require alguma das diretivas do elemento original ou se o elemento original tivesse um posicionamento absoluto.