Dodawanie dyrektyw z dyrektywy w AngularJS

Próbuję zbudować dyrektywę, która zajmie się dodaniem kolejnych dyrektyw do elementu, na którym jest zadeklarowana. Na przykład chcę zbudować dyrektywę, która zajmie się dodawaniem datepicker, datepicker-language i ng-required="true".

Jeśli próbuję dodać te atrybuty, a następnie użyć $compile oczywiście generuję nieskończoną pętlę, więc sprawdzam, czy dodałem już 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 $compile elementu, atrybuty zostaną ustawione, ale dyrektywa nie będzie bootstrapped.

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

UDPATE: biorąc pod uwagę fakt, że $compile jest jedynym sposobem, aby to osiągnąć, czy istnieje sposób na pominięcie pierwszego przejścia kompilacji(element może zawierać kilka dzieci)? Może ustawiając terminal:true?

UPDATE 2 : próbowałem umieścić dyrektywę w elemencie select i zgodnie z oczekiwaniami kompilacja działa dwa razy, co oznacza istnieje dwa razy więcej oczekiwanych option s.

Author: frapontillo, 2013-10-07

7 answers

W przypadkach, gdy masz wiele dyrektyw na jednym elemencie DOM i gdy kolejność ich stosowania ma znaczenie, możesz użyć właściwości priority, aby zamówić ich podanie. Wyższe numery działają jako pierwsze. Domyślny priorytet to 0, jeśli go nie podasz.

EDIT: po dyskusji, oto kompletne rozwiązanie robocze. Kluczem było usunięcie atrybutu: element.removeAttr("common-things");, a także element.removeAttr("data-common-things"); (w przypadku, gdy użytkownicy określają data-common-things w 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);
          }
        };
      }
    };
  });

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

DEMO

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

Gdy DOM jest gotowy, angular przechyla się po DOM, aby zidentyfikować wszystkie zarejestrowane dyrektywy i skompilować dyrektywy jeden po drugim na podstawie priority jeśli te dyrektywy dotyczą tego samego elementu . Ustalamy priorytet naszej dyrektywy niestandardowej do dużej liczby, aby zapewnić, że zostanie skompilowana jako pierwsza, a z terminal: true, Pozostałe dyrektywy zostaną pominięte po skompilowaniu tej dyrektywy.

Kiedy nasza custom directive zostanie skompilowana, zmodyfikuje element dodając dyrektywy i usuwając się z niego oraz użyje usługi $ compile do kompilacji wszystkich dyrektyw (łącznie z tymi, które zostały pominięte) .

Jeśli nie ustawimy terminal:true i priority: 1000, istnieje szansa, że niektóre dyrektywy zostaną skompilowane przed {[20] } naszą zwyczajową dyrektywą. A gdy nasza niestandardowa dyrektywa używa $compile do kompilacji elementu => skompilować ponownie już skompilowane dyrektywy. Spowoduje to nieprzewidywalne zachowanie, zwłaszcza jeśli dyrektywy skompilowane przed naszą niestandardową dyrektywą już zmieniły DOM.

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

Przykład dyrektywy, która również modyfikuje szablon jest ng-repeat (priorytet = 1000), gdy ng-repeat jest skompilowany, ng-repeat wykonaj kopie elementu szablonu przed zastosowaniem innych dyrektyw .

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

 256
Author: Khanh TO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:16

Możesz sobie z tym wszystkim poradzić za pomocą prostego tagu szablonu. Zobacz http://jsfiddle.net/m4ve9 / na przykład. Zauważ, że w rzeczywistości nie potrzebowałem właściwości compile lub link w definicji super-dyrektywy.

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

Jeśli jest to super dyrektywa, która musi zachować oryginał zawartość wewnętrzną, można użyć transclude : true i zastąpić wewnątrz <ng-transclude></ng-transclude>

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

Alex

 10
Author: mrvdot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-09 15:33:07

Oto rozwiązanie, które przenosi dyrektywy, które muszą być dodane dynamicznie do widoku, a także dodaje opcjonalną (podstawową) logikę warunkową. Dzięki temu dyrektywa jest czysta bez zakodowanej logiki.

Dyrektywa pobiera tablicę obiektów, każdy obiekt zawiera nazwę dyrektywy, która ma zostać dodana, oraz wartość, która ma zostać do niej przekazana (jeśli istnieje).

Starałem się wymyślić przypadek użycia takiej dyrektywy, dopóki nie pomyślałem, że przydałoby się dodać trochę logika warunkowa, która dodaje tylko dyrektywę opartą na pewnym warunku(choć odpowiedź poniżej jest nadal wymyślona). Dodałem opcjonalną właściwość if, która powinna zawierać wartość bool, wyrażenie lub funkcję (np. zdefiniowaną w kontrolerze), która określa, czy dyrektywa powinna być dodana, czy nie.

Używam również attrs.$attr.dynamicDirectives aby uzyskać dokładną deklarację atrybutów użytą do dodania dyrektywy (np. data-dynamic-directive, dynamic-directive) bez kodowania ciągów znaków do 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>
 6
Author: GFoley83,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-29 16:16:10

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

Musiałem dodać dyrektywę, ale również zachować moją na elemencie.

W tym przykładzie dodaję prostą dyrektywę w stylu ng do elementu. Aby zapobiec nieskończonym pętlom kompilacji i pozwolić mi zachować moją dyrektywę dodałem sprawdzenie, czy to, co dodałem, było obecne przed rekompilacją 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);
                    }
                }
            };
        }
    };
}]);
 3
Author: Sean256,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-18 14:57:35

Spróbuj zapisać stan w atrybucie samego elementu, np. 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.
 1
Author: Kemal Dağ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-07 11:51:37

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

In Angular 1.3.X to działało:

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

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

 0
Author: Thomas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:30

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

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

To rozwiązanie ma tę zaletę, że utrzymuje rzeczy proste, nie rekompilując oryginalnego elementu.

To nie zadziała, jeśli którakolwiek z dodanych dyrektyw require którejkolwiek z oryginalnych dyrektyw elementu lub jeśli oryginalny element ma absolutne pozycjonowanie.

 0
Author: plong0,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-26 20:31:28