¿Cómo puedo agregar dinámicamente una directiva en AngularJS?

212

Tengo una versión muy resumida de lo que estoy haciendo que resuelve el problema.

Tengo un simple directive. Cada vez que hace clic en un elemento, agrega otro. Sin embargo, debe compilarse primero para representarlo correctamente.

Mi investigación me llevó a $compile. Pero todos los ejemplos usan una estructura complicada que realmente no sé cómo aplicar aquí.

Los violines están aquí: http://jsfiddle.net/paulocoelho/fBjbP/1/

Y el JS está aquí:

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

Solución de Josh David Miller: http://jsfiddle.net/paulocoelho/fBjbP/2/

PCoelho
fuente

Respuestas:

259

Tiene una gran cantidad de jQuery inútil allí, pero el servicio de compilación $ es realmente super simple en este 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 );
      };
    }
  };
});

Notarás que también modifiqué tu directiva para seguir algunas de las mejores prácticas. Avíseme si tiene preguntas sobre alguno de esos.

Josh David Miller
fuente
34
Increíble. Funciona. Mira, estos ejemplos simples y básicos son los que deberían mostrarse en los documentos de los angulares. Comienzan con ejemplos complicados.
PCoelho
1
Gracias, Josh, esto fue realmente útil. Hice una herramienta en Plnkr que estamos usando en un nuevo CoderDojo para ayudar a los niños a aprender a codificar, y simplemente la extendí para que ahora pueda usar las directivas Angular Bootstrap como datepicker, alert, tabs, etc. Aparentemente escribí algo y ahora solo funciona en Chrome: embed.plnkr.co/WI16H7Rsa5adejXSmyNj/preview
JoshGough
3
Josh: ¿cuál es una manera más fácil de lograr esto sin usar $compile? Gracias por tu respuesta por cierto!
Dobleswirve
3
@doubleswirve En este caso, sería mucho más fácil usar ngRepeat. :-) Pero supongo que te refieres a agregar nuevas directivas dinámicamente a la página, en cuyo caso la respuesta es no: no hay una forma más sencilla porque el $compileservicio es lo que conecta las directivas y las conecta al ciclo de eventos. No hay forma de evitar $compileuna situación como esta, pero en la mayoría de los casos otra directiva como ngRepeat puede realizar el mismo trabajo (por lo que ngRepeat está compilando por nosotros). ¿Tiene un caso de uso específico?
Josh David Miller el
2
¿No debería ocurrir la compilación en la etapa de preenlace? Creo que el controlador solo debe contener código que no sea DOM, comprobable por unidad, pero soy nuevo en el concepto de enlace / controlador, así que no estoy seguro. Además, una alternativa básica es ng-include + partial + ng-controller ya que actuará como una directiva con alcance heredado .
Marcus Rådell
77

Además de perfeccionar el ejemplo de Riceball LEE de agregar una nueva directiva de elemento

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

Se puede agregar una nueva directiva de atributo al elemento existente de esta manera:

Digamos que desea agregar sobre la marcha my-directiveal 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 ayude.

muerto
fuente
3
No olvide eliminar la directiva original para evitar que el tamaño máximo de la pila de llamadas exceda el error.
SRachamim
Hola, ¿podría proporcionar ideas sobre mi nueva API propuesta para hacer que agregar directivas mediante programación sea un proceso más simple? github.com/angular/angular.js/issues/6950 ¡Gracias!
trusktr
Ojalá en 2015 no tuviéramos límites en el tamaño de la pila de llamadas. :(
psycho brm
3
El Maximum call stack size exceedederror siempre ocurre debido a la recursión infinita. Nunca he visto una instancia en la que aumentar el tamaño de la pila lo resolvería.
Gunchars
Problema similar al que me enfrento, ¿me pueden ayudar aquí stackoverflow.com/questions/38821980/…
pandu das
45

Agregar dinámicamente directivas en angularjs tiene dos estilos:

Agregar una directiva angularjs a otra directiva

  • insertar un nuevo elemento (directiva)
  • insertar un nuevo atributo (directiva) al elemento

insertar un nuevo elemento (directiva)

es sencillo. Y se puede usar en "enlace" o "compilar".

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

insertando un nuevo atributo al elemento

Es difícil y me da dolor de cabeza en dos días.

¡Usar "$ compile" generará un error recursivo crítico! Tal vez debería ignorar la directiva actual al volver a compilar el 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 );

Por lo tanto, tengo que encontrar una manera de llamar a la función directiva "enlace". Es muy difícil obtener los métodos útiles que están ocultos profundamente dentro de los cierres.

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)       

Ahora funciona bien.

Riceball LEE
fuente
1
Me encantaría ver una demostración de la inserción de un nuevo atributo a elemento, la vainilla JS si es posible - me falta algo ...
Patrick
El ejemplo real de insertar un nuevo atributo al elemento está aquí (ver mi github): github.com/snowyu/angular-reactable/blob/master/src/…
Riceball LEE
1
No ayuda honestamente. Así es como terminé resolviendo mi problema: stackoverflow.com/a/20137542/1455709
Patrick
Sí, este caso es la inserción de una directiva de atributo en otra directiva, no el elemento de inserción en la plantilla.
Riceball LEE
¿Cuál es el razonamiento detrás de hacerlo fuera de la plantilla?
Patrick
9
function addAttr(scope, el, attrName, attrValue) {
  el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
usuario1212212
fuente
5

La respuesta aceptada por Josh David Miller funciona muy bien si está tratando de agregar dinámicamente una directiva que utiliza una línea template. Sin embargo, si su directiva aprovecha templateUrlsu respuesta, no funcionará. Esto es lo que funcionó para mí:

.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
fuente
5

Josh David Miller está en lo correcto.

PCoelho, en caso de que se pregunte qué $compilehace detrás de escena y cómo se genera la salida HTML de la directiva, eche un vistazo a continuación

El $compileservicio compila el fragmento de HTML ( "< test text='n' >< / test >") que incluye la directiva ("prueba" como elemento) y produce una función. Esta función se puede ejecutar con un alcance para obtener el "resultado HTML de una directiva".

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

Más detalles con ejemplos de código completo aquí: http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs

Danial Lokman
fuente
4

Inspirado en muchas de las respuestas anteriores, se me ocurrió la siguiente directiva "stroman" que se reemplazará con cualquier otra directiva.

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 las directivas con las que desea usar restrict: 'C'. Me gusta esto:

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

Puedes usar así:

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

Para obtener esto:

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

Protip. Si no desea utilizar directivas basadas en clases, puede cambiar '<div></div>'a algo que le guste. Por ejemplo, tener un atributo fijo que contiene el nombre de la directiva deseada en lugar de class.

Gábor Imre
fuente
Problema similar al que me enfrento, ¿me pueden ayudar aquí? stackoverflow.com/questions/38821980/…
pandu das
DIOS MIO. tardó 2 días en encontrar esta compilación de $ ... gracias amigos ... funciona mejor ... AJS you rock ...
Srinivasan