Agregar directivas de directiva en AngularJS

197

Estoy tratando de construir una directiva que se encargue de agregar más directivas al elemento en el que se declara. Por ejemplo, quiero construir una directiva que se encargue de agregar datepicker, datepicker-languagey ng-required="true".

Si trato de agregar esos atributos y luego usarlos $compile, obviamente genero un bucle infinito, así que estoy verificando si ya he agregado los atributos necesarios:

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

Por supuesto, si no conozco $compileel elemento, los atributos se establecerán pero la directiva no se iniciará.

¿Es correcto este enfoque o lo estoy haciendo mal? ¿Hay una mejor manera de lograr el mismo comportamiento?

UDPATE : dado que esa $compilees la única forma de lograr esto, ¿hay alguna forma de omitir el primer pase de compilación (el elemento puede contener varios elementos secundarios )? Tal vez por el establecimiento terminal:true?

ACTUALIZACIÓN 2 : He intentado poner la directiva en un selectelemento y, como se esperaba, la compilación se ejecuta dos veces, lo que significa que hay el doble de la cantidad esperada de options.

frapontillo
fuente

Respuestas:

260

En los casos en que tenga varias directivas sobre un único elemento DOM y donde el orden en el que se aplican sea importante, puede usar la prioritypropiedad para ordenar su aplicación. Los números más altos se ejecutan primero. La prioridad predeterminada es 0 si no especifica una.

EDITAR : después de la discusión, aquí está la solución de trabajo completa. La clave era eliminar el atributo : element.removeAttr("common-things");y también element.removeAttr("data-common-things");(en caso de que los usuarios especifiquen data-common-thingsen el 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);
          }
        };
      }
    };
  });

El programa de trabajo está disponible en: http://plnkr.co/edit/Q13bUt?p=preview

O:

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

MANIFESTACIÓN

Explicación de por qué tenemos que configurar terminal: truey priority: 1000(un número alto):

Cuando el DOM está listo, angular lo recorre para identificar todas las directivas registradas y compilar las directivas una por una en función de priority si estas directivas están en el mismo elemento . Establecemos la prioridad de nuestra directiva personalizada en un número alto para asegurarnos de que se compile primero y con terminal: truelas otras directivas se omitirán después de que se compile esta directiva.

Cuando se compila nuestra directiva personalizada, modificará el elemento agregando directivas y eliminándose a sí mismo y usará el servicio $ compile para compilar todas las directivas (incluidas las que se omitieron) .

Si no configuramos terminal:truey priority: 1000, existe la posibilidad de que algunas directivas se compilen antes que nuestra directiva personalizada. Y cuando nuestra directiva personalizada usa $ compile para compilar el elemento => compila nuevamente las directivas ya compiladas. Esto provocará un comportamiento impredecible, especialmente si las directivas compiladas antes de nuestra directiva personalizada ya han transformado el DOM.

Para obtener más información sobre prioridad y terminal, consulte ¿Cómo entender el `terminal` de la directiva?

Un ejemplo de una directiva que también modifica la plantilla es ng-repeat(prioridad = 1000), cuando ng-repeatse compila, ng-repeat haga copias del elemento de la plantilla antes de que se apliquen otras directivas .

Gracias al comentario de @ Izhaki, aquí está la referencia al ngRepeatcódigo fuente: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

Khanh TO
fuente
55
Me arroja una excepción de desbordamiento de pila: a RangeError: Maximum call stack size exceededmedida que continúa compilando para siempre.
frapontillo
3
@frapontillo: en su caso, intente agregar element.removeAttr("common-datepicker");para evitar bucles indefinidos.
Khanh A
44
Ok, he sido capaz de solucionar el problema, usted tiene que fijar replace: false, terminal: true, priority: 1000; luego establezca los atributos deseados en la compilefunción y elimine nuestro atributo de directiva. Finalmente, en la postfunción devuelta por compile, call $compile(element)(scope). El elemento se compilará regularmente sin la directiva personalizada pero con los atributos agregados. Lo que estaba tratando de lograr era no eliminar la directiva personalizada y manejar todo esto en un solo proceso: parece que esto no se puede hacer. Consulte el plnkr actualizado: plnkr.co/edit/Q13bUt?p=preview .
frapontillo
2
Tenga en cuenta que si necesita utilizar el parámetro de objeto de atributos de las funciones de compilación o enlace, sepa que la directiva responsable de interpolar valores de atributo tiene prioridad 100, y su directiva debe tener una prioridad menor que esta, de lo contrario solo obtendrá valores de cadena de los atributos debido a que el directorio es terminal. Ver (ver esta solicitud de extracción de Github y este problema relacionado )
Simen Echholt
2
Como alternativa a la eliminación de los common-thingsatributos, puede pasar un parámetro maxPriority al comando de compilación:$compile(element, null, 1000)(scope);
Andreas
10

En realidad, puede manejar todo esto con solo una simple etiqueta de plantilla. Consulte http://jsfiddle.net/m4ve9/ para ver un ejemplo. Tenga en cuenta que en realidad no necesitaba una propiedad de compilación o enlace en la definición de super-directiva.

Durante el proceso de compilación, Angular extrae los valores de la plantilla antes de la compilación, por lo que puede adjuntar cualquier directiva adicional allí y Angular se encargará de usted.

Si se trata de una súper directiva que necesita preservar el contenido interno original, puede usar transclude : truey reemplazar el interior con<ng-transclude></ng-transclude>

Espero que ayude, avíseme si algo no está claro

Alex

mrvdot
fuente
Gracias Alex, el problema de este enfoque es que no puedo suponer cuál será la etiqueta. En el ejemplo, era un selector de fechas, es decir, una inputetiqueta, pero me gustaría que funcione para cualquier elemento, como divs o selects.
frapontillo
1
Ah, sí, me perdí eso. En ese caso, recomendaría seguir con un div y solo asegurarme de que sus otras directivas puedan funcionar en eso. No es la respuesta más clara, pero se ajusta mejor a la metodología angular. Para cuando el proceso de arranque haya comenzado a compilar un nodo HTML, ya ha recopilado todas las directivas en el nodo para la compilación, por lo que agregar uno nuevo no será notado por el proceso de arranque original. Dependiendo de sus necesidades, puede encontrar que envolver todo en un div y trabajar dentro de él le brinda más flexibilidad, pero también limita dónde puede colocar su elemento.
mrvdot
3
@frapontillo Puede usar una plantilla como una función elementy pasarla attrs. Me llevó años resolverlo, y no lo he visto usado en ningún lado, pero parece funcionar bien: stackoverflow.com/a/20137542/1455709
Patrick
6

Aquí hay una solución que mueve las directivas que deben agregarse dinámicamente a la vista y también agrega lógica condicional (básica) opcional. Esto mantiene la directiva limpia sin lógica codificada.

La directiva toma una matriz de objetos, cada objeto contiene el nombre de la directiva que se agregará y el valor que se le pasará (si corresponde).

Estaba luchando por pensar en un caso de uso para una directiva como esta hasta que pensé que podría ser útil agregar alguna lógica condicional que solo agregue una directiva basada en alguna condición (aunque la respuesta a continuación todavía es artificial). Agregué una ifpropiedad opcional que debería contener un valor bool, expresión o función (por ejemplo, definida en su controlador) que determina si la directiva debe agregarse o no.

También estoy usando attrs.$attr.dynamicDirectivespara obtener la declaración de atributo exacta utilizada para agregar la directiva (por ejemplo data-dynamic-directive, dynamic-directive) sin codificar los valores de cadena para verificar.

Plunker Demo

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 e.g.  ng-repeat
                terminal: true, // Stop other directives running
                link: linker
            };
        }
    ]);
<!doctype html>
<html ng-app="plunker">

<head>
    <script src="//code.angularjs.org/1.2.20/angular.js"></script>
    <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.6.0.js"></script>
    <script src="example.js"></script>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
</head>

<body>

    <div data-ng-controller="DatepickerDemoCtrl">

        <select data-ng-options="s for s in selects" data-ng-model="el" 
            data-dynamic-directives="[
                { 'if' : 'selectIsRequired', 'ng-required' : '{{selectIsRequired}}' },
                { 'tooltip-placement' : 'bottom' },
                { 'if' : 'addTooltip()', 'tooltip' : '{{ dt() }}' }
            ]">
            <option value=""></option>
        </select>

    </div>
</body>

</html>

GFoley83
fuente
Usado en otra plantilla directiva. Funciona bien y me ahorra tiempo. Solo gracias.
jcstritt
4

Quería agregar mi solución ya que la aceptada no funcionó para mí.

Necesitaba agregar una directiva pero también mantener la mía en el elemento.

En este ejemplo, estoy agregando una simple directiva ng-style al elemento. Para evitar bucles de compilación infinitos y permitirme mantener mi directiva, agregué un cheque para ver si lo que agregué estaba presente antes de volver a compilar el 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);
                    }
                }
            };
        }
    };
}]);
Sean256
fuente
Vale la pena señalar que no puede usar esto con transclude o una plantilla, ya que el compilador intenta volver a aplicarlos en la segunda ronda.
spikyjt
1

Intente almacenar el estado en un atributo en el elemento en sí, como superDirectiveStatus="true"

Por ejemplo:

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 esto te ayude.

Kemal Dağ
fuente
Gracias, el concepto básico sigue siendo el mismo :). Estoy tratando de encontrar una manera de omitir el primer pase de compilación. He actualizado la pregunta original.
frapontillo
La doble compilación rompe las cosas de una manera horrible.
frapontillo
1

Hubo un cambio de 1.3.x a 1.4.x.

En Angular 1.3.x esto funcionó:

var dir: ng.IDirective = {
    restrict: "A",
    require: ["select", "ngModel"],
    compile: compile,
};

function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) {
    tElement.append("<option value=''>--- Kein ---</option>");

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

Ahora en Angular 1.4.x tenemos que hacer esto:

var dir: ng.IDirective = {
    restrict: "A",
    compile: compile,
    terminal: true,
    priority: 10,
};

function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) {
    tElement.append("<option value=''>--- Kein ---</option>");
    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();
    }
}

(De la respuesta aceptada: https://stackoverflow.com/a/19228302/605586 de Khanh TO).

Thomas
fuente
0

Una solución simple que podría funcionar en algunos casos es crear y $ compilar un contenedor y luego agregarle su elemento original.

Algo como...

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

Esta solución tiene la ventaja de que simplifica las cosas al no volver a compilar el elemento original.

Esto no funcionaría si alguna de las directivas agregadas es requirealguna de las directivas del elemento original o si el elemento original tiene un posicionamiento absoluto.

plong0
fuente