it-roy-ru.com

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

195
frapontillo

В тех случаях, когда у вас есть несколько директив для одного элемента 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);
      }
    };
  });

ДЕМО

Объяснение, почему мы должны установить terminal: true и priority: 1000 (большое число):

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

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

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

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

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

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

258
Khanh TO

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

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

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

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

Alex

10
mrvdot

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

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

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

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

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

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

Попробуйте сохранить состояние в атрибуте самого элемента, например, 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
Kemal Dağ

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

1
Thomas

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

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

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

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

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

0
plong0