Добавьте директивы из директивы в 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 вдвое больше .

frapontillo
источник

Ответы:

260

В тех случаях, когда у вас есть несколько директив для одного элемента DOM, и когда порядок, в котором они применяются, имеет значение, вы можете использовать это priorityсвойство для заказа их приложения. Более высокие числа запускаются первыми. Приоритет по умолчанию - 0, если вы его не указали.

РЕДАКТИРОВАТЬ : после обсуждения, вот полное рабочее решение. Ключ должен был удалить атрибут :, 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 готов, angular обходит DOM, чтобы идентифицировать все зарегистрированные директивы и компилировать директивы одну за другой, основываясь на priority том, находятся ли эти директивы в одном и том же элементе . Мы устанавливаем приоритет нашей пользовательской директивы на большое число, чтобы гарантировать, что она будет скомпилирована первой, а terminal: trueдругие директивы будут пропущены после компиляции этой директивы.

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

Если мы не установим terminal:trueи priority: 1000, есть вероятность, что некоторые директивы скомпилированы перед нашей пользовательской директивой. И когда наша пользовательская директива использует $ compile для компиляции element => compile снова уже скомпилированные директивы. Это приведет к непредсказуемому поведению, особенно если директивы, скомпилированные до нашей пользовательской директивы, уже преобразовали DOM.

Для получения дополнительной информации о приоритете и терминале, посмотрите Как понять `терминал` директивы?

Примером директивы, которая также изменяет шаблон, является ng-repeat(приоритет = 1000), когда ng-repeatон скомпилирован, ng-repeat создайте копии элемента шаблона перед применением других директив .

Благодаря комментарию @ Izhaki, вот ссылка на 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функции, возвращаемой функцией compilecall $compile(element)(scope). Элемент будет регулярно компилироваться без специальной директивы, но с добавленными атрибутами. То, чего я пытался добиться, - это не удалять пользовательскую директиву и обрабатывать все это в одном процессе: кажется, этого нельзя сделать. Пожалуйста, обратитесь к обновленному plnkr: plnkr.co/edit/Q13bUt?p=preview .
фрапонтильо
2
Обратите внимание, что если вам нужно использовать параметр объекта attribute функций компиляции или компоновки, знайте, что директива, отвечающая за интерполяцию значений атрибута, имеет приоритет 100, и ваша директива должна иметь более низкий приоритет, чем этот, иначе вы получите только строковые значения атрибутов из-за того, что каталог является терминальным. Смотрите (см. Этот запрос на github pull и связанную с этим проблему )
Симен Эххолт
2
В качестве альтернативы удалению common-thingsатрибутов вы можете передать параметр maxPriority в команду компиляции:$compile(element, null, 1000)(scope);
Andreas
10

Вы можете справиться со всем этим с помощью простого тега шаблона. См. Http://jsfiddle.net/m4ve9/ для примера. Обратите внимание, что на самом деле мне не нужно свойство compile или link в определении супер-директивы.

В процессе компиляции Angular извлекает значения шаблона перед компиляцией, поэтому вы можете прикрепить туда любые дополнительные директивы, и Angular позаботится об этом за вас.

Если это супер-директива, которая должна сохранить исходный внутренний контент, вы можете использовать transclude : trueи заменить внутри<ng-transclude></ng-transclude>

Надеюсь, это поможет, дайте мне знать, если что-то неясно

Alex

mrvdot
источник
Спасибо, Алекс, проблема этого подхода в том, что я не могу предположить, каким будет тег. В примере это был указатель даты, т. Е. inputТег, но я бы хотел, чтобы он работал для любого элемента, такого как divs или selects.
фрапонтильо
1
Ах, да, я пропустил это. В этом случае я бы порекомендовал придерживаться div и просто убедиться, что ваши другие директивы могут с этим работать. Это не самый чистый из ответов, но лучше всего подходит в рамках методологии Angular. К тому времени, когда процесс начальной загрузки начал компилировать узел HTML, он уже собрал все директивы на узле для компиляции, поэтому добавление новой там не будет замечено исходным процессом начальной загрузки. В зависимости от ваших потребностей, вы можете найти упаковку всего в div и работу внутри, что дает вам больше гибкости, но также ограничивает возможности размещения вашего элемента.
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>

GFoley83
источник
Используется в другом шаблоне директивы. Это отлично работает и экономит мое время. Просто спасибо.
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);
                    }
                }
            };
        }
    };
}]);
Sean256
источник
Стоит отметить, что вы не можете использовать это с 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
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

Простое решение, которое может работать в некоторых случаях, - это создать и $ compile обертку, а затем добавить к ней исходный элемент.

Что-то вроде...

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

Преимущество этого решения в том, что оно упрощает задачу, не перекомпилируя исходный элемент.

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

plong0
источник