Como posso adicionar dinamicamente uma diretiva no AngularJS?

212

Eu tenho uma versão muito resumida do que estou fazendo que esclarece o problema.

Eu tenho um simples directive. Sempre que você clica em um elemento, ele adiciona outro. No entanto, ele precisa ser compilado primeiro para renderizá-lo corretamente.

Minha pesquisa me levou a $compile. Mas todos os exemplos usam uma estrutura complicada que eu realmente não sei como aplicar aqui.

Os violinos estão aqui: http://jsfiddle.net/paulocoelho/fBjbP/1/

E o JS está aqui:

var module = angular.module('testApp', [])
    .directive('test', function () {
    return {
        restrict: 'E',
        template: '<p>{{text}}</p>',
        scope: {
            text: '@text'
        },
        link:function(scope,element){
            $( element ).click(function(){
                // TODO: This does not do what it's supposed to :(
                $(this).parent().append("<test text='n'></test>");
            });
        }
    };
});

Solução de Josh David Miller: http://jsfiddle.net/paulocoelho/fBjbP/2/

PCoelho
fonte

Respostas:

259

Você tem muitos jQuery inúteis, mas o serviço $ compile é realmente super simples neste caso:

.directive( 'test', function ( $compile ) {
  return {
    restrict: 'E',
    scope: { text: '@' },
    template: '<p ng-click="add()">{{text}}</p>',
    controller: function ( $scope, $element ) {
      $scope.add = function () {
        var el = $compile( "<test text='n'></test>" )( $scope );
        $element.parent().append( el );
      };
    }
  };
});

Você notará que refatorei sua diretiva também para seguir algumas práticas recomendadas. Deixe-me saber se você tiver dúvidas sobre qualquer um deles.

Josh David Miller
fonte
34
Impressionante. Funciona. Veja, esses exemplos simples e básicos são os que devem ser mostrados nos documentos dos angulares. Eles começam com exemplos complicados.
PCoelho 7/03/13
1
Obrigado, Josh, isso foi realmente útil. Eu criei uma ferramenta no Plnkr que estamos usando em um novo CoderDojo para ajudar as crianças a aprender a codificar, e eu apenas a estendi para que agora eu possa usar diretivas Angular Bootstrap como datepicker, alert, tabs, etc. e agora é só trabalhar no Chrome porém: embed.plnkr.co/WI16H7Rsa5adejXSmyNj/preview
JoshGough
3
Josh - qual é a maneira mais fácil de fazer isso sem usar $compile? Obrigado pela sua resposta, a propósito!
Doubleswirve
3
@doubleswirve Nesse caso, seria muito mais fácil usar o ngRepeat. :-) Mas suponho que você queira adicionar novas diretivas dinamicamente à página; nesse caso, a resposta é não - não há uma maneira mais simples, porque o $compileserviço é o que liga as diretivas e as conecta ao ciclo de eventos. Não há como contornar $compileuma situação como essa, mas na maioria dos casos outra diretiva como o ngRepeat pode realizar o mesmo trabalho (portanto, o ngRepeat está fazendo a compilação para nós). Você tem um caso de uso específico?
Josh David Miller
2
A compilação não deveria acontecer no estágio de pré-ligação? Eu acho que o controlador deve conter apenas código não-DOM, testável por unidade, mas eu sou novo no conceito de link / controlador, por isso não tenho certeza. Além disso, uma alternativa básica é ng-include + parcial + ng-controller, pois atuará como uma diretiva com escopo herdado .
Marcus Rådell
77

Além do exemplo perfeito de Riceball LEE de adicionar uma nova diretiva de elemento

newElement = $compile("<div my-directive='n'></div>")($scope)
$element.parent().append(newElement)

A adição de uma nova diretiva de atributo ao elemento existente pode ser feita desta maneira:

Digamos que você deseja adicionar on-the-fly my-directiveao spanelemento.

template: '<div>Hello <span>World</span></div>'

link: ($scope, $element, $attrs) ->

  span = $element.find('span').clone()
  span.attr('my-directive', 'my-directive')
  span = $compile(span)($scope)
  $element.find('span').replaceWith span

Espero que ajude.

morto
fonte
3
Não se esqueça de remover a diretiva original para evitar erro de tamanho máximo da pilha de chamadas.
SRachamim
Olá, você poderia fornecer idéias sobre minha nova API proposta para tornar a adição de diretivas programaticamente um processo mais simples? github.com/angular/angular.js/issues/6950 Obrigado!
trusktr
Eu gostaria que em 2015 não tivéssemos limites no tamanho da pilha de chamadas. :(
psycho brm
3
O Maximum call stack size exceedederro sempre acontece devido a uma recursão infinita. Nunca vi um caso em que o aumento do tamanho da pilha o resolvesse.
Gunchars 31/07
Problema semelhante estou enfrentando, você pode me ajudar aqui stackoverflow.com/questions/38821980/...
pandu das
45

A adição dinâmica de diretivas em angularjs possui dois estilos:

Adicionar uma diretiva angularjs a outra diretiva

  • inserindo um novo elemento (diretiva)
  • inserindo um novo atributo (diretiva) no elemento

inserindo um novo elemento (diretiva)

é simples. E você pode usar em "link" ou "compilar".

var newElement = $compile( "<div my-diretive='n'></div>" )( $scope );
$element.parent().append( newElement );

inserindo um novo atributo no elemento

É difícil e me dá dor de cabeça em dois dias.

Usar "$ compile" irá gerar um erro recursivo crítico !! Talvez deva ignorar a diretiva atual ao recompilar o elemento.

$element.$set("myDirective", "expression");
var newElement = $compile( $element )( $scope ); // critical recursive error.
var newElement = angular.copy(element);          // the same error too.
$element.replaceWith( newElement );

Então, eu tenho que encontrar uma maneira de chamar a função "link" da diretiva. É muito difícil obter os métodos úteis que estão ocultos profundamente nos fechamentos.

compile: (tElement, tAttrs, transclude) ->
   links = []
   myDirectiveLink = $injector.get('myDirective'+'Directive')[0] #this is the way
   links.push myDirectiveLink
   myAnotherDirectiveLink = ($scope, $element, attrs) ->
       #....
   links.push myAnotherDirectiveLink
   return (scope, elm, attrs, ctrl) ->
       for link in links
           link(scope, elm, attrs, ctrl)       

Agora, funciona bem.

Riceball LEE
fonte
1
Gostaria de ver uma demonstração da inserção de um novo atributo no elemento, em baunilha JS, se possível - Estou faltando alguma coisa ... #
221 Patrick #
o exemplo real de inserção de um novo atributo no elemento está aqui (veja meu github): github.com/snowyu/angular-reactable/blob/master/src/…
Riceball LEE
1
Não ajuda honestamente. Isto é como eu acabei resolvendo meu problema, porém: stackoverflow.com/a/20137542/1455709
Patrick
Sim, este caso é a inserção de uma diretiva de atributo em outra diretiva, não o elemento de inserção no modelo.
Riceball LEE
Qual o motivo por trás disso fora do modelo?
Patrick
9
function addAttr(scope, el, attrName, attrValue) {
  el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
user1212212
fonte
5

A resposta aceita por Josh David Miller funciona muito bem se você estiver tentando adicionar dinamicamente uma diretiva que usa uma linha template. No entanto, se a sua diretiva tirar proveito da templateUrlresposta dele, não funcionará. Aqui está o que funcionou para mim:

.directive('helperModal', [, "$compile", "$timeout", function ($compile, $timeout) {
    return {
        restrict: 'E',
        replace: true,
        scope: {}, 
        templateUrl: "app/views/modal.html",
        link: function (scope, element, attrs) {
            scope.modalTitle = attrs.modaltitle;
            scope.modalContentDirective = attrs.modalcontentdirective;
        },
        controller: function ($scope, $element, $attrs) {
            if ($attrs.modalcontentdirective != undefined && $attrs.modalcontentdirective != '') {
                var el = $compile($attrs.modalcontentdirective)($scope);
                $timeout(function () {
                    $scope.$digest();
                    $element.find('.modal-body').append(el);
                }, 0);
            }
        }
    }
}]);
ferics2
fonte
5

Josh David Miller está correto.

PCoelho, Caso esteja se perguntando o que $compileacontece nos bastidores e como a saída HTML é gerada a partir da diretiva, dê uma olhada abaixo

O $compileserviço compila o fragmento de HTML ( "< test text='n' >< / test >") que inclui a diretiva ("teste" como um elemento) e produz uma função. Essa função pode ser executada com um escopo para obter a "saída HTML de uma diretiva".

var compileFunction = $compile("< test text='n' > < / test >");
var HtmlOutputFromDirective = compileFunction($scope);

Mais detalhes com exemplos de código completo aqui: http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs

Danial Lokman
fonte
4

Inspirado em muitas das respostas anteriores, criei a seguinte diretiva "stroman" que se substituirá a qualquer outra diretiva.

app.directive('stroman', function($compile) {
  return {
    link: function(scope, el, attrName) {
      var newElem = angular.element('<div></div>');
      // Copying all of the attributes
      for (let prop in attrName.$attr) {
        newElem.attr(prop, attrName[prop]);
      }
      el.replaceWith($compile(newElem)(scope)); // Replacing
    }
  };
});

Importante: Registre as diretivas que você deseja usar restrict: 'C'. Como isso:

app.directive('my-directive', function() {
  return {
    restrict: 'C',
    template: 'Hi there',
  };
});

Você pode usar assim:

<stroman class="my-directive other-class" randomProperty="8"></stroman>

Para conseguir esta:

<div class="my-directive other-class" randomProperty="8">Hi there</div>

Protip. Se você não quiser usar diretivas baseadas em classes, poderá mudar '<div></div>'para o que quiser. Por exemplo, tenha um atributo fixo que contenha o nome da diretiva desejada em vez de class.

Gábor Imre
fonte
Problema semelhante estou enfrentando, você pode me ajudar aqui stackoverflow.com/questions/38821980/...
pandu das
AMD. Demorou 2 dias para encontrar este $ compilação ... obrigado amigos .. ele funciona melhor ... AJS você rock ....
Srinivasan