javascript - personalizadas - que es una directiva en angular 4




Agregar directivas de la directiva en AngularJS (5)

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

Si trato de agregar esos atributos y luego uso $compile , obviamente genero un bucle infinito, así que estoy comprobando 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 $compile el 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 $compile es 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 estableciendo terminal:true ¿ terminal:true ?

ACTUALIZACIÓN 2 : He intentado colocar la directiva en un elemento de select y, como se esperaba, la compilación se ejecuta dos veces, lo que significa que hay el doble de la cantidad de option esperadas.


Aquí hay una solución que mueve las directivas que deben agregarse dinámicamente, a la vista y también agrega alguna lógica condicional opcional (básica). 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).

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

También estoy usando attrs.$attr.dynamicDirectives para obtener la declaración del atributo exacto que se usa para agregar la directiva (por ejemplo, directiva de data-dynamic-directive dynamic-directive ) sin verificar los valores de la cadena de codificación.

Demo 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 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>


En los casos en los que tenga varias directivas en un solo elemento DOM y el orden en el que se aplican, puede usar la propiedad de priority para ordenar su aplicación. Los números más altos corren primero. La prioridad predeterminada es 0 si no especifica uno.

EDIT : 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-things en 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 plunker 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);
      }
    };
  });

DEMO

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

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

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

Si no establecemos terminal:true y priority: 1000 , existe la posibilidad de que algunas directivas se compilen antes de nuestra directiva personalizada. Y cuando nuestra directiva personalizada usa $ compile para compilar el elemento => compile nuevamente las directivas ya compiladas. Esto causará 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 la prioridad y el 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 se compila ng-repeat , ng-repeat hace copias del elemento de la plantilla antes de que se apliquen otras directivas .

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


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://.com/a/19228302/605586 de Khanh TO).


Intente almacenar el estado en un atributo en el propio elemento, 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.


Una solución simple que podría funcionar en algunos casos es crear y compilar un envoltorio 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 require alguna de las directivas del elemento original o si el elemento original tiene una posición absoluta.





angularjs-directive