Aggiungi le direttive dalla direttiva in AngularJS


197

Sto cercando di costruire una direttiva che si occupi di aggiungere più direttive all'elemento su cui è dichiarata. Ad esempio, voglio costruire una direttiva che si occupi di aggiungere datepicker, datepicker-languagee ng-required="true".

Se provo ad aggiungere quegli attributi e poi utilizzo, $compileovviamente, generi un ciclo infinito, quindi sto verificando se ho già aggiunto gli attributi necessari:

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

Naturalmente, se non lo faccio $compile, gli attributi verranno impostati ma la direttiva non verrà avviata.

Questo approccio è corretto o sto sbagliando? Esiste un modo migliore per ottenere lo stesso comportamento?

UDPATE : dato che $compileè l'unico modo per raggiungere questo obiettivo, c'è un modo per saltare il primo passaggio di compilazione (l'elemento può contenere diversi figli)? Forse impostando terminal:true?

AGGIORNAMENTO 2 : Ho provato a inserire la direttiva in un selectelemento e, come previsto, la compilazione viene eseguita due volte, il che significa che c'è il doppio del numero di messaggi previsti option.

Risposte:


260

Nei casi in cui hai più direttive su un singolo elemento DOM e in cui l'ordine in cui sono applicati è importante, puoi utilizzare la priorityproprietà per ordinare la loro applicazione. I numeri più alti vengono eseguiti per primi. La priorità predefinita è 0 se non ne specifichi uno.

EDIT : dopo la discussione, ecco la soluzione di lavoro completa. La chiave era rimuovere l'attributo :, element.removeAttr("common-things");e anche element.removeAttr("data-common-things");(nel caso in cui gli utenti specifichino data-common-thingsnell'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);
          }
        };
      }
    };
  });

Il plunker di lavoro è disponibile all'indirizzo: 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

Spiegazione del motivo per cui dobbiamo impostare terminal: truee priority: 1000(un numero elevato):

Quando il DOM è pronto, Angular accompagna il DOM per identificare tutte le direttive registrate e compilare le direttive una per una in base al fatto priority che queste direttive siano sullo stesso elemento . Impostiamo la priorità della nostra direttiva personalizzata su un numero elevato per garantire che venga compilata per prima e terminal: true, con le altre direttive, verranno ignorate dopo che questa direttiva è stata compilata.

Quando viene compilata la nostra direttiva personalizzata, modificherà l'elemento aggiungendo direttive e rimuovendo se stesso e utilizzando il servizio $ compile per compilare tutte le direttive (comprese quelle che sono state ignorate) .

Se non impostiamo terminal:truee priority: 1000, è possibile che alcune direttive vengano compilate prima della nostra direttiva personalizzata. E quando la nostra direttiva personalizzata usa $ compile per compilare l'elemento => compila di nuovo le direttive già compilate. Ciò causerà un comportamento imprevedibile soprattutto se le direttive compilate prima della nostra direttiva personalizzata hanno già trasformato il DOM.

Per maggiori informazioni su priorità e terminale, controlla Come capire il "terminale" della direttiva?

Un esempio di direttiva che modifica anche il modello è ng-repeat(priorità = 1000), quando ng-repeatviene compilato, ng-repeat crea copie dell'elemento modello prima che vengano applicate altre direttive .

Grazie al commento di @ Izhaki, ecco il riferimento al ngRepeatcodice sorgente: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js


5
Mi fa un'eccezione di overflow dello stack: RangeError: Maximum call stack size exceededmentre continua a compilare per sempre.
frapontillo,

3
@frapontillo: nel tuo caso, prova ad aggiungere element.removeAttr("common-datepicker");per evitare un ciclo indefinito.
Khanh

4
Ok, sono stato in grado di risolvere la questione, è necessario impostare replace: false, terminal: true, priority: 1000; quindi imposta gli attributi desiderati nella compilefunzione e rimuovi il nostro attributo direttiva. Infine, nella postfunzione restituita da compile, chiama $compile(element)(scope). L'elemento verrà compilato regolarmente senza la direttiva personalizzata ma con gli attributi aggiunti. Quello che stavo cercando di ottenere non era rimuovere la direttiva personalizzata e gestire tutto ciò in un unico processo: a quanto pare non è possibile farlo. Fare riferimento al plnkr aggiornato: plnkr.co/edit/Q13bUt?p=preview .
frapontillo,

2
Si noti che se è necessario utilizzare il parametro object di attributo delle funzioni di compilazione o collegamento, sapere che la direttiva responsabile dell'interpolazione dei valori degli attributi ha priorità 100 e che la direttiva deve avere una priorità inferiore a questa, altrimenti si otterrà solo il valori stringa degli attributi dovuti alla directory essendo terminale. Vedi (vedi questa richiesta pull di github e questo problema correlato )
Simen Echholt

2
in alternativa alla rimozione degli common-thingsattributi potresti passare un parametro maxPriority al comando di compilazione:$compile(element, null, 1000)(scope);
Andreas

10

Puoi effettivamente gestire tutto questo con un semplice tag modello. Vedi http://jsfiddle.net/m4ve9/ per un esempio. Si noti che in realtà non avevo bisogno di una proprietà di compilazione o collegamento sulla definizione di super-direttiva.

Durante il processo di compilazione, Angular inserisce i valori del modello prima della compilazione, in modo da poter allegare eventuali ulteriori direttive e Angular se ne occuperà per te.

Se si tratta di una super direttiva che deve preservare il contenuto interno originale, è possibile utilizzare transclude : truee sostituire l'interno con<ng-transclude></ng-transclude>

Spero che ti aiuti, fammi sapere se qualcosa non è chiaro

alex


Grazie Alex, il problema di questo approccio è che non posso dare per scontato quale sarà il tag. Nell'esempio era un datepicker, cioè un inputtag, ma mi piacerebbe farlo funzionare per qualsiasi elemento, come ad esempio divs o selects.
frapontillo,

1
Ah, sì, l'ho perso. In tal caso, ti consiglierei di restare con un div e assicurarmi solo che le altre tue direttive possano lavorarci. Non è la più pulita delle risposte, ma si adatta meglio alla metodologia angolare. Quando il processo di bootstrap ha iniziato a compilare un nodo HTML, ha già raccolto tutte le direttive sul nodo per la compilazione, quindi aggiungendone uno nuovo non verrà notato dal processo di bootstrap originale. A seconda delle tue esigenze, potresti trovare avvolgere tutto in un div e lavorare all'interno che ti dà maggiore flessibilità, ma limita anche dove puoi mettere il tuo elemento.
mrvdot,

3
@frapontillo Puoi usare un modello come funzione con elemente attrspassato. Mi ci sono voluti anni per risolverlo, e non l'ho visto usato da nessuna parte - ma sembra funzionare bene: stackoverflow.com/a/20137542/1455709
Patrick,

6

Ecco una soluzione che sposta le direttive che devono essere aggiunte dinamicamente, nella vista e aggiunge anche una logica condizionale (di base) opzionale. Ciò mantiene pulita la direttiva senza una logica codificata.

La direttiva accetta una matrice di oggetti, ogni oggetto contiene il nome della direttiva da aggiungere e il valore da passare ad essa (se presente).

Stavo lottando per pensare a un caso d'uso per una direttiva come questa fino a quando non ho pensato che potesse essere utile aggiungere una logica condizionale che aggiunge solo una direttiva basata su una condizione (sebbene la risposta sotto sia ancora inventata). Ho aggiunto una ifproprietà facoltativa che dovrebbe contenere un valore booleano, un'espressione o una funzione (ad esempio definita nel controller) che determina se aggiungere o meno la direttiva.

Sto anche usando attrs.$attr.dynamicDirectivesper ottenere l'esatta dichiarazione di attributo utilizzata per aggiungere la direttiva (ad esempio data-dynamic-directive, dynamic-directive) senza valori di stringa codificabili per verificare.

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>


Utilizzato in un altro modello di direttiva. Funziona benissimo e risparmia tempo. Solo grazie.
jcstritt,

4

Volevo aggiungere la mia soluzione poiché quella accettata non ha funzionato per me.

Avevo bisogno di aggiungere una direttiva ma anche mantenere la mia sull'elemento.

In questo esempio sto aggiungendo una semplice direttiva in stile ng all'elemento. Per evitare infiniti cicli di compilazione e consentirmi di mantenere la mia direttiva, ho aggiunto un segno di spunta per vedere se ciò che avevo aggiunto era presente prima di ricompilare l'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);
                    }
                }
            };
        }
    };
}]);

Vale la pena notare che non è possibile utilizzare questo con transclude o un modello, poiché il compilatore tenta di riapplicarli nel secondo round.
spikyjt,

1

Prova a memorizzare lo stato in un attributo sull'elemento stesso, ad esempio superDirectiveStatus="true"

Per esempio:

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

        }

      }
    };
  });

Spero che questo ti aiuta.


Grazie, il concetto di base rimane lo stesso :). Sto cercando di capire un modo per saltare il primo passaggio di compilazione. Ho aggiornato la domanda originale.
frapontillo,

La doppia compilation rompe le cose in un modo orribile.
Frapontillo,

1

C'è stato un passaggio da 1.3.x a 1.4.x.

In Angular 1.3.x ha funzionato:

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

Ora in Angular 1.4.x dobbiamo fare questo:

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

(Dalla risposta accettata: https://stackoverflow.com/a/19228302/605586 di Khanh TO).


0

Una soluzione semplice che potrebbe funzionare in alcuni casi è quella di creare e compilare $ un wrapper e quindi aggiungere l'elemento originale ad esso.

Qualcosa di simile a...

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

Questa soluzione ha il vantaggio di mantenere le cose semplici non ricompilando l'elemento originale.

Ciò non funzionerebbe se nessuna delle direttive aggiunte fosse una delle direttive requiredell'elemento originale o se l'elemento originale avesse un posizionamento assoluto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.