Thêm chỉ thị từ chỉ thị trong AngularJS


197

Tôi đang cố gắng xây dựng một chỉ thị quan tâm đến việc thêm nhiều chỉ thị vào thành phần được khai báo. Ví dụ, tôi muốn xây dựng một chỉ thị mà sẽ chăm sóc thêm datepicker, datepicker-languageng-required="true".

Nếu tôi cố gắng thêm các thuộc tính đó và sau đó sử dụng thì $compilerõ ràng tôi sẽ tạo một vòng lặp vô hạn, vì vậy tôi đang kiểm tra xem tôi đã thêm các thuộc tính cần thiết chưa:

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

Tất nhiên, nếu tôi không $compilethành phần, các thuộc tính sẽ được đặt nhưng lệnh sẽ không được khởi động.

Cách tiếp cận này là đúng hay tôi đang làm sai? Có cách nào tốt hơn để đạt được hành vi tương tự?

UDPATE : với thực tế $compilelà cách duy nhất để đạt được điều này, có cách nào để bỏ qua bước biên dịch đầu tiên (phần tử có thể chứa một vài con) không? Có lẽ bằng cách thiết lập terminal:true?

CẬP NHẬT 2 : Tôi đã thử đưa lệnh vào một selectphần tử và, như mong đợi, quá trình biên dịch chạy hai lần, có nghĩa là có gấp đôi số options dự kiến .

Câu trả lời:


260

Trong trường hợp bạn có nhiều lệnh trên một thành phần DOM duy nhất và khi thứ tự chúng được áp dụng có vấn đề, bạn có thể sử dụng thuộc prioritytính để đặt hàng ứng dụng của chúng. Số cao hơn chạy đầu tiên. Ưu tiên mặc định là 0 nếu bạn không chỉ định một.

EDIT : sau khi thảo luận, đây là giải pháp làm việc hoàn chỉnh. Chìa khóa là loại bỏ thuộc tính : element.removeAttr("common-things");và cũng element.removeAttr("data-common-things");(trong trường hợp người dùng chỉ định data-common-thingstrong 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);
          }
        };
      }
    };
  });

Plunker làm việc có sẵn tại: http://plnkr.co/edit/Q13bUt?p=preview

Hoặc là:

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

BẢN GIỚI THIỆU

Giải thích tại sao chúng ta phải đặt terminal: truepriority: 1000(một số cao):

Khi DOM đã sẵn sàng, angular đi bộ DOM để xác định tất cả các chỉ thị đã đăng ký và biên dịch từng chỉ thị dựa trên việc priority các chỉ thị này có nằm trên cùng một phần tử hay không . Chúng tôi đặt mức độ ưu tiên của chỉ thị tùy chỉnh của mình thành một số cao để đảm bảo rằng nó sẽ được biên dịch trước và cùng với đó terminal: true, các chỉ thị khác sẽ bị bỏ qua sau khi lệnh này được biên dịch.

Khi chỉ thị tùy chỉnh của chúng tôi được biên dịch, nó sẽ sửa đổi thành phần bằng cách thêm các lệnh và xóa chính nó và sử dụng dịch vụ $ compile để biên dịch tất cả các lệnh (bao gồm cả các lệnh bị bỏ qua) .

Nếu chúng ta không thiết lập terminal:truepriority: 1000, có khả năng một số chỉ thị được biên soạn trước chỉ thị tùy chỉnh của chúng ta. Và khi chỉ thị tùy chỉnh của chúng tôi sử dụng $ compile để biên dịch phần tử => biên dịch lại các lệnh đã được biên dịch. Điều này sẽ gây ra hành vi không thể đoán trước đặc biệt là nếu các chỉ thị được biên soạn trước chỉ thị tùy chỉnh của chúng tôi đã chuyển đổi DOM.

Để biết thêm thông tin về mức độ ưu tiên và thiết bị đầu cuối, hãy xem Làm thế nào để hiểu 'thiết bị đầu cuối' của chỉ thị?

Một ví dụ về một lệnh cũng sửa đổi mẫu là ng-repeat(ưu tiên = 1000), khi ng-repeatđược biên dịch, ng-repeat tạo các bản sao của phần tử mẫu trước khi các lệnh khác được áp dụng .

Nhờ nhận xét của @ Izhaki, đây là tài liệu tham khảo về ngRepeatmã nguồn: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js


5
Nó ném ra một ngoại lệ stack stack đối với tôi: RangeError: Maximum call stack size exceededvì nó cứ được biên dịch mãi mãi.
frapontillo

3
@frapontillo: trong trường hợp của bạn, hãy thử thêm element.removeAttr("common-datepicker");để tránh vòng lặp không xác định.
Khánh đến

4
Ok, tôi đã có thể sắp xếp nó ra, bạn phải thiết lập replace: false, terminal: true, priority: 1000; sau đó đặt các thuộc tính mong muốn trong compilehàm và loại bỏ thuộc tính chỉ thị của chúng ta. Cuối cùng, trong posthàm trả về compile, gọi $compile(element)(scope). Phần tử sẽ được biên dịch thường xuyên mà không có chỉ thị tùy chỉnh nhưng với các thuộc tính được thêm vào. Những gì tôi đã cố gắng đạt được là không loại bỏ chỉ thị tùy chỉnh và xử lý tất cả những điều này trong một quy trình: điều này dường như không thể thực hiện được. Vui lòng tham khảo plnkr cập nhật: plnkr.co/edit/Q13bUt?p=preview .
frapontillo

2
Lưu ý rằng nếu bạn cần sử dụng tham số đối tượng thuộc tính của các hàm biên dịch hoặc liên kết, hãy biết rằng lệnh chịu trách nhiệm nội suy các giá trị thuộc tính có mức độ ưu tiên 100 và lệnh của bạn cần có mức độ ưu tiên thấp hơn mức này, nếu không bạn sẽ chỉ nhận được giá trị chuỗi của các thuộc tính do thư mục là thiết bị đầu cuối. Xem (xem yêu cầu kéo github nàyvấn đề liên quan này )
Simen Echholt

2
như một cách thay thế để loại bỏ các common-thingsthuộc tính mà bạn có thể truyền tham số maxP Warriority cho lệnh biên dịch:$compile(element, null, 1000)(scope);
Andreas

10

Bạn thực sự có thể xử lý tất cả những điều này chỉ với một thẻ mẫu đơn giản. Xem http://jsfiddle.net/m4ve9/ để biết ví dụ. Lưu ý rằng tôi thực sự không cần một thuộc tính biên dịch hoặc liên kết theo định nghĩa siêu chỉ thị.

Trong quá trình biên dịch, Angular kéo các giá trị mẫu trước khi biên dịch, vì vậy bạn có thể đính kèm bất kỳ chỉ thị nào khác ở đó và Angular sẽ chăm sóc nó cho bạn.

Nếu đây là một siêu chỉ thị cần giữ nguyên nội dung ban đầu, bạn có thể sử dụng transclude : truevà thay thế bên trong bằng<ng-transclude></ng-transclude>

Mong rằng sẽ giúp, cho tôi biết nếu có bất cứ điều gì không rõ ràng

Alex


Cảm ơn Alex, vấn đề của phương pháp này là tôi không thể đưa ra bất kỳ giả định nào về việc thẻ sẽ là gì. Trong ví dụ, nó là một công cụ hẹn hò, tức là một inputthẻ, nhưng tôi muốn làm cho nó hoạt động cho bất kỳ yếu tố nào, chẳng hạn như divs hoặc selects.
frapontillo

1
À, ừ, tôi nhớ rồi. Trong trường hợp đó, tôi khuyên bạn nên gắn bó với div và chỉ cần đảm bảo rằng các chỉ thị khác của bạn có thể hoạt động trên đó. Đây không phải là câu trả lời rõ ràng nhất, nhưng phù hợp nhất với phương pháp Angular. Vào thời điểm quá trình bootstrap bắt đầu biên dịch một nút HTML, nó đã thu thập tất cả các lệnh trên nút để biên dịch, vì vậy việc thêm một lệnh mới sẽ không được chú ý bởi quá trình bootstrap ban đầu. Tùy thuộc vào nhu cầu của bạn, bạn có thể thấy việc gói mọi thứ trong một div và hoạt động bên trong giúp bạn linh hoạt hơn, nhưng nó cũng giới hạn nơi bạn có thể đặt phần tử của mình.
mrvdot

3
@frapontillo Bạn có thể sử dụng một khuôn mẫu như một chức năng elementattrsđược truyền vào. Đã cho tôi nhiều tuổi để làm việc đó và tôi chưa thấy nó được sử dụng ở bất cứ đâu - nhưng dường như nó hoạt động tốt: stackoverflow.com/a/20137542/1455709
Patrick

6

Đây là một giải pháp di chuyển các chỉ thị cần được thêm động, vào chế độ xem và cũng thêm một số logic điều kiện (cơ bản) tùy chọn. Điều này giữ cho lệnh sạch sẽ không có logic mã hóa cứng.

Lệnh này lấy một mảng các đối tượng, mỗi đối tượng chứa tên của lệnh được thêm vào và giá trị được truyền cho nó (nếu có).

Tôi đã vật lộn để nghĩ về một trường hợp sử dụng cho một lệnh như thế này cho đến khi tôi nghĩ rằng có thể hữu ích khi thêm một số logic có điều kiện chỉ thêm một lệnh dựa trên một số điều kiện (mặc dù câu trả lời dưới đây vẫn còn tồn tại). Tôi đã thêm một thuộc tính tùy chọn ifnên chứa giá trị bool, biểu thức hoặc hàm (ví dụ: được xác định trong bộ điều khiển của bạn) để xác định xem có nên thêm lệnh hay không.

Tôi cũng đang sử dụng attrs.$attr.dynamicDirectivesđể có được những lời tuyên bố thuộc tính chính xác sử dụng để thêm các chỉ thị (ví dụ data-dynamic-directive, dynamic-directive) không có giá trị chuỗi hard-coding để kiểm tra.

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>


Được sử dụng trong một mẫu chỉ thị khác. Nó hoạt động tốt và tiết kiệm thời gian của tôi. Chỉ cần cảm ơn.
jcstritt

4

Tôi muốn thêm giải pháp của mình vì giải pháp được chấp nhận không hoàn toàn phù hợp với tôi.

Tôi cần phải thêm một chỉ thị nhưng cũng giữ cho tôi về yếu tố này.

Trong ví dụ này tôi đang thêm một chỉ thị kiểu ng đơn giản cho phần tử. Để ngăn chặn các vòng lặp biên dịch vô hạn và cho phép tôi giữ lệnh của mình, tôi đã thêm một kiểm tra để xem liệu những gì tôi đã thêm có trước khi biên dịch lại phần tử khô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);
                    }
                }
            };
        }
    };
}]);

Điều đáng chú ý là bạn không thể sử dụng điều này với transclude hoặc mẫu, vì trình biên dịch cố gắng áp dụng lại chúng trong vòng thứ hai.
spikyjt

1

Hãy thử lưu trữ trạng thái trong một thuộc tính trên chính phần tử, chẳng hạn như superDirectiveStatus="true"

Ví dụ:

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

        }

      }
    };
  });

Tôi hy vọng cái này sẽ giúp bạn.


Cảm ơn, khái niệm cơ bản vẫn giữ nguyên :). Tôi đang cố gắng tìm ra một cách để bỏ qua việc biên dịch đầu tiên. Tôi đã cập nhật câu hỏi ban đầu.
frapontillo

Việc biên dịch kép phá vỡ mọi thứ một cách khủng khiếp.
frapontillo

1

Có một sự thay đổi từ 1.3.x thành 1.4.x.

Trong Angular 1.3.x, nó hoạt động:

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

Bây giờ trong Angular 1.4.x chúng ta phải làm điều này:

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

(Từ câu trả lời được chấp nhận: https://stackoverflow.com/a/19228302/605586 từ Khánh ĐẾN).


0

Một giải pháp đơn giản có thể hoạt động trong một số trường hợp là tạo và $ biên dịch trình bao bọc và sau đó nối phần tử ban đầu của bạn vào nó.

Cái gì đó như...

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

Giải pháp này có ưu điểm là giữ cho mọi thứ đơn giản bằng cách không biên dịch lại phần tử gốc.

Điều này sẽ không hoạt động nếu bất kỳ chỉ thị requirenào được thêm vào bất kỳ chỉ thị nào của phần tử gốc hoặc nếu phần tử gốc có định vị tuyệt đối.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.