Добавьте директивы из директивы в AngularJS

Я пытаюсь создать директиву, которая позаботится о добавлении дополнительных директив в элемент, для которого она объявлена. Например, я хочу построить директиву, которая заботится о добавлении 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.

7 ответов

Решение

В тех случаях, когда у вас есть несколько директив для одного элемента 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 сделайте копии элемента шаблона до того, как будут применены другие директивы.

Благодаря комментарию @ Изаки, вот ссылка на ngRepeat исходный код: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

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

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

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

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

Alex

Вот решение, которое перемещает директивы, которые должны динамически добавляться, в представление, а также добавляет некоторую необязательную (базовую) условную логику. Это поддерживает директиву в чистоте без жестко запрограммированной логики.

Директива принимает массив объектов, каждый объект содержит имя добавляемой директивы и значение для передачи ей (если есть).

Я изо всех сил пытался придумать сценарий использования такой директивы, пока не подумал, что было бы полезно добавить некоторую условную логику, которая добавляет директиву только на основе некоторого условия (хотя ответ ниже все еще надуман). Я добавил опционально 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>

Я хотел добавить свое решение, так как принятое решение не совсем для меня.

Мне нужно было добавить директиву, а также сохранить элемент.

В этом примере я добавляю простую директиву в стиле 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);
                    }
                }
            };
        }
    };
}]);

Try storing the state in a attribute on the element itself, such as 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);

        }

      }
    };
  });

Я надеюсь, это поможет вам.

Произошло изменение с 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();
    }
}

(Из принятого ответа: /questions/27719455/dobavte-direktivyi-iz-direktivyi-v-angularjs/27719481#27719481 от Khanh TO).

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

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

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

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

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

Другие вопросы по тегам