Dodaj dyrektywy z dyrektywy w AngularJS

197

Próbuję zbudować dyrektywę, która zajmuje się dodawaniem kolejnych dyrektyw do elementu, który jest zadeklarowany. Na przykład, chcę zbudować dyrektywę, która dba o dodanie datepicker, datepicker-languagei ng-required="true".

Jeśli spróbuję dodać te atrybuty, a następnie użyję $compile, oczywiście generuję nieskończoną pętlę, więc sprawdzam, czy już dodałem potrzebne atrybuty:

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

Oczywiście, jeśli nie $compileelement, atrybuty zostaną ustawione, ale dyrektywa nie zostanie załadowana.

Czy to podejście jest prawidłowe, czy robię to źle? Czy istnieje lepszy sposób na osiągnięcie tego samego zachowania?

UDPATE : biorąc pod uwagę fakt, że $compilejest to jedyny sposób na osiągnięcie tego, czy istnieje sposób na pominięcie pierwszego przejścia kompilacji (element może zawierać kilka elementów podrzędnych)? Może przez ustawienie terminal:true?

AKTUALIZACJA 2 : Próbowałem umieścić dyrektywę w selectelemencie i, zgodnie z oczekiwaniami, kompilacja działa dwa razy, co oznacza, że ​​jest dwa razy więcej oczekiwanych options.

frapontillo
źródło

Odpowiedzi:

260

W przypadku, gdy masz wiele dyrektyw na jednym elemencie DOM i gdy kolejność, w jakiej są stosowane, ma znaczenie, możesz użyć prioritywłaściwości do zamówienia ich aplikacji. Wyższe liczby są uruchamiane jako pierwsze. Domyślny priorytet to 0, jeśli go nie określisz.

EDYCJA : po dyskusji, oto kompletne działające rozwiązanie. Kluczem było usunąć atrybut : element.removeAttr("common-things");, a także element.removeAttr("data-common-things");(w przypadku użytkowników określić data-common-thingsw 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);
          }
        };
      }
    };
  });

Działający plunker jest dostępny pod adresem : http://plnkr.co/edit/Q13bUt?p=preview

Lub:

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

PRÓBNY

Wyjaśnienie, dlaczego musimy ustawić terminal: truei priority: 1000(dużą liczbę):

Gdy DOM jest gotowy, kątowy prowadzi DOM w celu zidentyfikowania wszystkich zarejestrowanych dyrektyw i kompilowania dyrektyw jeden po drugim w oparciu o to, priority czy te dyrektywy dotyczą tego samego elementu . Możemy ustawić priorytet nasz zwyczaj dyrektywy do dużej liczby, aby zapewnić, że zostanie skompilowany pierwszy i terminal: true, pozostałe dyrektywy zostaną pominięte po tej dyrektywy jest skompilowany.

Kiedy nasza niestandardowa dyrektywa zostanie skompilowana, zmodyfikuje element, dodając dyrektywy i usuwając się, i używa usługi $ compile do skompilowania wszystkich dyrektyw (w tym tych, które zostały pominięte) .

Jeśli nie ustawimy terminal:truei priority: 1000, istnieje szansa, że ​​niektóre dyrektywy zostaną skompilowane przed naszą niestandardową dyrektywą. A kiedy nasza niestandardowa dyrektywa używa $ compile do skompilowania elementu => ponownie skompiluj już skompilowane dyrektywy. Spowoduje to nieprzewidziane zachowanie, szczególnie jeśli dyrektywy skompilowane przed naszą niestandardową dyrektywą już przekształciły DOM.

Aby uzyskać więcej informacji o priorytecie i terminalu, sprawdź Jak zrozumieć „terminal” dyrektywy?

Przykładem dyrektywy, która również modyfikuje szablon jest ng-repeat(priorytet = 1000), kiedy ng-repeatjest kompilowany, ng-repeat wykonaj kopię elementu szablonu przed zastosowaniem innych dyrektyw .

Dzięki komentarzowi @ Izhaki tutaj znajduje się odniesienie do ngRepeatkodu źródłowego: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

Khanh TO
źródło
5
Zgłasza mi wyjątek przepełnienia stosu: RangeError: Maximum call stack size exceededkompilacja trwa wiecznie.
frapontillo
3
@frapontillo: w twoim przypadku spróbuj dodać, element.removeAttr("common-datepicker");aby uniknąć nieokreślonej pętli.
Khanh DO
4
Ok, udało mi się rozwiązać to, trzeba ustawić replace: false, terminal: true, priority: 1000; następnie ustaw pożądane atrybuty w compilefunkcji i usuń nasz atrybut dyrektywy. Wreszcie w postfunkcji zwróconej przez compilecall $compile(element)(scope). Element będzie regularnie kompilowany bez niestandardowej dyrektywy, ale z dodanymi atrybutami. Starałem się nie usunąć niestandardowej dyrektywy i poradzić sobie z tym wszystkim w jednym procesie: wydaje się, że nie da się tego zrobić. Proszę odnieść się do zaktualizowanego plnkr: plnkr.co/edit/Q13bUt?p=preview .
frapontillo
2
Zauważ, że jeśli potrzebujesz użyć parametru obiektu atrybutów funkcji kompilacji lub łącza, wiedz, że dyrektywa odpowiedzialna za interpolację wartości atrybutów ma priorytet 100, a twoja dyrektywa musi mieć niższy priorytet, w przeciwnym razie otrzymasz tylko wartości łańcuchowe atrybutów ze względu na to, że katalog jest terminalem. Zobacz (zobacz to żądanie ściągnięcia github i ten powiązany problem )
Simen Echholt
2
zamiast usuwania common-thingsatrybutów można przekazać parametr maxPriority do polecenia kompilacji:$compile(element, null, 1000)(scope);
Andreas
10

Możesz poradzić sobie z tym wszystkim za pomocą prostego znacznika szablonu. Zobacz http://jsfiddle.net/m4ve9/ dla przykładu. Zauważ, że tak naprawdę nie potrzebowałem właściwości kompilacji ani łącza w definicji super-dyrektywy.

Podczas procesu kompilacji, Angular pobiera wartości szablonu przed kompilacją, więc możesz dołączyć tam wszystkie dalsze dyrektywy, a Angular zajmie się tym za Ciebie.

Jeśli jest to super dyrektywa, która musi zachować oryginalną treść wewnętrzną, możesz użyć transclude : truei zastąpić wnętrze<ng-transclude></ng-transclude>

Mam nadzieję, że to pomoże, daj mi znać, jeśli coś jest niejasne

Alex

mrvdot
źródło
Dziękuję Alex, problem z tym podejściem polega na tym, że nie mogę założyć, jaki będzie tag. W tym przykładzie był to datownik, tj. inputTag, ale chciałbym, aby działał dla dowolnego elementu, takiego jak divs lub selects.
frapontillo
1
Ach, tak, tęskniłem za tym. W takim przypadku zaleciłbym pozostanie przy div i upewnienie się, że inne dyrektywy mogą nad tym popracować. To nie jest najczystsza z odpowiedzi, ale najlepiej pasuje do metodologii Angular. Kiedy proces ładowania początkowego zaczął kompilować węzeł HTML, zebrał już wszystkie dyrektywy w węźle do kompilacji, więc dodanie nowego nie zostanie zauważone przez oryginalny proces ładowania początkowego. W zależności od potrzeb możesz znaleźć zawijanie wszystkiego w div i pracę wewnątrz, co daje większą elastyczność, ale także ogranicza miejsce, w którym możesz umieścić swój element.
mrvdot
3
@frapontillo Można użyć szablonu jako funkcję z elementi attrsprzeszedł zajęło mi wieki, że z pracy, a ja nie widziałem go stosować wszędzie - ale wydaje się działać prawidłowo. stackoverflow.com/a/20137542/1455709
Patrick,
6

Oto rozwiązanie, które przenosi dyrektywy, które należy dynamicznie dodawać, do widoku, a także dodaje opcjonalną (podstawową) logikę warunkową. To utrzymuje dyrektywę w czystości bez logiki na stałe.

Dyrektywa przyjmuje tablicę obiektów, każdy obiekt zawiera nazwę dyrektywy, która ma zostać dodana, i wartość, którą należy do niej przekazać (jeśli istnieje).

Próbowałem wymyślić przypadek użycia takiej dyrektywy, dopóki nie pomyślałem, że użyteczne może być dodanie logiki warunkowej, która dodaje tylko dyrektywę opartą na pewnych warunkach (choć odpowiedź poniżej jest nadal wymyślona). Dodałem opcjonalną ifwłaściwość, która powinna zawierać wartość bool, wyrażenie lub funkcję (np. Zdefiniowaną w twoim kontrolerze), która określa, czy dyrektywa powinna zostać dodana czy nie.

Używam również attrs.$attr.dynamicDirectivesuzyskać deklarację atrybutu dokładnie używany dodać dyrektywy (np data-dynamic-directive, dynamic-directive) bez twardych kodowania ciągów znaków w celu sprawdzenia.

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
źródło
Używany w innym szablonie dyrektywy. Działa dobrze i oszczędzam czas. Po prostu dziękuję.
jcstritt
4

Chciałem dodać moje rozwiązanie, ponieważ zaakceptowane nie do końca działało.

Musiałem dodać dyrektywę, ale również utrzymywać mój na temat tego elementu.

W tym przykładzie dodałem prostą dyrektywę w stylu ng do elementu. Aby zapobiec nieskończonym pętlom kompilacji i pozwolić mi zachować moją dyrektywę, dodałem opcję sprawdzania, czy to, co dodałem, było obecne przed ponowną kompilacją elementu.

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
źródło
Warto zauważyć, że nie można tego użyć z transkluzją lub szablonem, ponieważ kompilator próbuje je ponownie zastosować w drugiej rundzie.
spikyjt
1

Spróbuj zapisać stan w atrybucie samego elementu, na przykład superDirectiveStatus="true"

Na przykład:

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

        }

      }
    };
  });

Mam nadzieję, że to Ci pomoże.

Kemal Dağ
źródło
Dzięki, podstawowa koncepcja pozostaje taka sama :). Próbuję znaleźć sposób na pominięcie pierwszego przejścia kompilacji. Zaktualizowałem oryginalne pytanie.
frapontillo
Podwójna kompilacja psuje wszystko w okropny sposób.
frapontillo
1

Nastąpiła zmiana z 1.3.x na 1.4.x.

W Angular 1.3.x działało to:

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

Teraz w Angular 1.4.x musimy to zrobić:

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

(Z zaakceptowanej odpowiedzi: https://stackoverflow.com/a/19228302/605586 od Khanh TO).

Tomasz
źródło
0

Prostym rozwiązaniem, które może działać w niektórych przypadkach, jest utworzenie i $ skompilowanie opakowania, a następnie dołączenie do niego oryginalnego elementu.

Coś jak...

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

Zaletą tego rozwiązania jest to, że nie kompiluje oryginalnego elementu.

Nie działałoby to, jeśli którakolwiek z dyrektyw dodanych w requirektórejkolwiek z dyrektyw oryginalnego elementu lub jeśli element oryginalny ma bezwzględne pozycjonowanie.

plong0
źródło