Додайте директиви з директиви в AngularJS


197

Я намагаюся створити директиву, яка б додала більше директив до елемента, про який заявлено. Наприклад, я хочу побудувати директиву , яка піклується про додавання datepicker, datepicker-languageі ng-required="true".

Якщо я спробую додати ці атрибути, а потім використовувати, $compileя, очевидно, генерую нескінченний цикл, тому я перевіряю, чи вже додав потрібні атрибути:

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

Звичайно, якщо я не $compileелемент, атрибути будуть встановлені, але директива не буде завантажена.

Чи правильний такий підхід чи я роблю його неправильно? Чи є кращий спосіб досягти такої ж поведінки?

UDPATE : враховуючи той факт, що $compileце єдиний спосіб досягти цього, чи є спосіб пропустити перший пропуск компіляції (елемент може містити кілька дітей)? Може, встановивши terminal:true?

ОНОВЛЕННЯ 2 : Я спробував ввести директиву в selectелемент і, як очікувалося, компіляція працює вдвічі, а це означає, що вдвічі більше очікуваних options.

Відповіді:


260

У випадках, коли у вас є кілька директив щодо одного елемента DOM, і де має значення порядок їх застосування, ви можете скористатися priorityвластивістю, щоб замовити їх застосування. Вищі числа запускаються першими. Пріоритет за замовчуванням - 0, якщо ви не вказали його.

EDIT : після обговорення ось повне робоче рішення. Ключем було видалення атрибута :, element.removeAttr("common-things");а також element.removeAttr("data-common-things");(у випадку, якщо користувачі вказують data-common-thingsу 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);
          }
        };
      }
    };
  });

Робочий плункер доступний за посиланням: http://plnkr.co/edit/Q13bUt?p=preview

Або:

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

Пояснення, чому нам потрібно встановити terminal: trueі priority: 1000(велике число):

Коли DOM готовий, кутовий прогулянка DOM для виявлення всіх зареєстрованих директив і складання директив по черзі на основі, priority якщо ці директиви знаходяться на одному елементі . Ми встановлюємо пріоритет нашої спеціальної директиви на велику кількість, щоб переконатися, що вона буде скомпільована першою, а разом з цим terminal: true, інші директиви будуть пропущені після компіляції цієї директиви.

Коли наша спеціальна директива буде скомпільована, вона буде змінювати елемент, додаючи директиви та видаляючи себе, і використовує службу $ compile для компіляції всіх директив (включаючи ті, що були пропущені) .

Якщо ми не встановимо terminal:trueі priority: 1000, є ймовірність, що деякі директиви будуть складені раніше нашою спеціальною директивою. І коли наша спеціальна директива використовує $ compile для компіляції елемента => знову компілювати вже складені директиви. Це спричинить непередбачувану поведінку, особливо якщо директиви, складені до нашої спеціальної директиви, вже перетворили DOM.

Для отримання додаткової інформації про пріоритет та термінал, дізнайтеся, як зрозуміти `термінал` директиви?

Прикладом директиви, яка також змінює шаблон, є ng-repeat(пріоритет = 1000), коли ng-repeatкомпілюється, ng-repeat зробіть копії елемента шаблону до того, як будуть застосовані інші директиви .

Завдяки коментарю @ Іжакі, ось посилання на ngRepeatвихідний код: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js


5
Це викидає для мене виняток переповнення стека: так RangeError: Maximum call stack size exceededяк він продовжує збиратись назавжди.
frapontillo

3
@frapontillo: у вашому випадку спробуйте додати, element.removeAttr("common-datepicker");щоб уникнути невизначеного циклу.
Хан ДО

4
Добре, я був в змозі розібратися, ви повинні встановити replace: false, terminal: true, priority: 1000; потім встановіть потрібні атрибути у compileфункції та видаліть наш атрибут директиви. Нарешті, у postфункції, що повертається compile, викликати $compile(element)(scope). Елемент буде регулярно компілюватися без спеціальної директиви, але з доданими атрибутами. Те, що я намагався досягти, - це не видаляти користувальницьку директиву та обробляти все це за один процес: це, здається, не можна зробити. Будь ласка, зверніться до оновленого PLNKR: plnkr.co/edit/Q13bUt?p=preview .
frapontillo

2
Зауважте, що якщо вам потрібно використовувати параметр об'єкта атрибутів функцій компіляції або зв’язку, знайте, що директива, відповідальна за інтерполяцію значень атрибутів, має пріоритет 100, і ваша директива повинна мати нижчий пріоритет, ніж цей, інакше ви отримаєте лише рядкові значення атрибутів через те, що каталог є термінальним. Дивіться (див. Цей запит на витяг github та пов’язану з цим проблему )
Сімен Ехольт

2
як альтернативу видаленню common-thingsатрибутів ви можете передати параметр maxPriority команді компіляції:$compile(element, null, 1000)(scope);
Andreas

10

Насправді ви можете впоратися з усім цим лише за допомогою простого тегу шаблонів. Дивіться http://jsfiddle.net/m4ve9/ приклад на веб-сайті . Зауважте, що я фактично не потребував властивості компіляції чи посилання на визначення супер-директиви.

Під час процесу компіляції Angular перетягує значення шаблонів перед компіляцією, тому ви можете приєднати до нього будь-які подальші директиви, і Angular піклується про нього за вас.

Якщо це супер директива, якій потрібно зберегти початковий внутрішній вміст, ви можете використовувати transclude : true та замінити внутрішню частину<ng-transclude></ng-transclude>

Сподіваюся, що це допоможе, дайте мені знати, якщо щось незрозуміле

Олексій


Дякую, Алекс, проблема такого підходу полягає в тому, що я не можу зробити жодного припущення щодо того, яким буде тег. У прикладі це був вибір дат, тобто inputтег, але я хотів би зробити так, щоб він працював для будь-якого елемента, наприклад divs або selects.
frapontillo

1
Ага, так, я пропустив це. У такому випадку я рекомендую дотримуватися діва та просто переконатися, що інші директиви можуть працювати на цьому. Це не найчистіший варіант відповідей, але найкраще підходить для методології Angular. На той момент, коли процес завантаження почав компілювати HTML-вузол, він вже зібрав усі директиви щодо вузла для компіляції, тому додавання нового там не буде помічено оригінальним процесом завантаження. Залежно від ваших потреб, ви можете знайти обгортку всього в діві і робота в рамках цього дає вам більше гнучкості, але це також обмежує те, де ви можете помістити свій елемент.
mrvdot

3
@frapontillo Ви можете використовувати шаблон як функцію elementі attrsпереходити до нього. Змусив мене віками це працювати, і я його ніде не бачив, але він, здається, працює добре: stackoverflow.com/a/20137542/1455709
Патрік

6

Ось рішення, яке переміщує директиви, які потрібно динамічно додавати, у вигляд, а також додає деякі необов'язкові (основні) умовно-логічні. Це забезпечує директиву чистою без чітко кодованої логіки.

Директива приймає масив об'єктів, кожен об'єкт містить ім'я директиви, яку потрібно додати, і значення для передачі їй (якщо воно є).

Я намагався придумати такий варіант використання такої директиви, поки не подумав, що може бути корисним додати деяку умовну логіку, яка лише додає директиву, яка базується на певній умові (хоча відповідь нижче все ще надумана). Я додав за бажаннямif властивість, яке повинно містити значення bool, вираз чи функцію (наприклад, визначену у вашому контролері), яка визначає, чи слід додавати директиву чи ні.

Я також використовую attrs.$attr.dynamicDirectivesдля отримання точної декларації атрибутів, яка використовується для додавання директиви (наприклад data-dynamic-directive, dynamic-directive) без жорсткого кодування рядкових значень для перевірки.

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>


Використовується в іншому шаблоні директиви. Це добре працює і економить мій час. Просто дякую.
jcstritt

4

Я хотів додати своє рішення, оскільки прийняте не дуже працювало для мене.

Мені потрібно було додати директиву, але також зберегти мою стихію.

У цьому прикладі я додаю просту директиву в стилі ng до елемента. Для запобігання нескінченних циклів компіляції та дозволяючи мені зберігати мою директиву я додав чек, щоб перевірити, чи є те, що я додав, присутнє перед перекомпіляцією елемента.

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

Варто зазначити, що ви не можете використовувати це з transclude або шаблоном, оскільки компілятор намагається повторно застосувати їх у другому раунді.
spikyjt

1

Спробуйте зберегти стан в атрибуті на самому елементі, наприклад superDirectiveStatus="true"

Наприклад:

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

        }

      }
    };
  });

Я сподіваюся, що це вам допоможе.


Дякую, основна концепція залишається такою ж :). Я намагаюся знайти спосіб пропустити перший пропуск компіляції. Я оновив початкове запитання.
frapontillo

Подвійна компіляція жахливо розбиває речі.
frapontillo

1

Відбулася зміна з 1,3.x на 1,4.x.

У Angular 1.3.x це спрацювало:

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

Тепер у Angular 1.4.x ми повинні зробити це:

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

(З прийнятої відповіді: https://stackoverflow.com/a/19228302/605586 від Khanh TO).


0

Просте рішення, яке може спрацювати в деяких випадках - створити та $ скомпонувати обгортку, а потім додати до неї свій оригінальний елемент.

Щось на зразок...

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

Перевага даного рішення полягає в тому, що він робить прості речі, не перекомпілюючи початковий елемент.

Це не працює, якщо будь-яка з доданих директив requireбудь-яка з директив оригінального елемента або якщо оригінальний елемент має абсолютне позиціонування.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.