เพิ่มคำสั่งจากคำสั่งใน AngularJS


197

ฉันกำลังพยายามสร้างคำสั่งที่ดูแลการเพิ่มคำสั่งเพิ่มเติมให้กับองค์ประกอบที่ประกาศไว้ ตัวอย่างเช่นผมต้องการที่จะสร้างคำสั่งที่จะดูแลของการเพิ่มdatepicker, และdatepicker-languageng-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องค์ประกอบองค์ประกอบจะถูกตั้งค่า แต่คำสั่งจะไม่ถูก bootstrapped

วิธีนี้ถูกต้องหรือฉันทำผิดหรือเปล่า? มีวิธีที่ดีกว่าเพื่อให้บรรลุพฤติกรรมเดียวกันหรือไม่

UDPATE : เนื่องจากความจริงที่ว่า$compileเป็นวิธีเดียวที่จะบรรลุเป้าหมายนี้มีวิธีข้ามการรวบรวมครั้งแรกหรือไม่ (องค์ประกอบอาจมีลูกหลายคน) อาจจะด้วยการตั้งค่าterminal:true?

UPDATE 2 : ฉันได้พยายามวางคำสั่งเป็นselectองค์ประกอบและเป็นไปตามคาดรวบรวมวิ่งสองครั้งซึ่งหมายความว่ามีเป็นสองเท่าของจำนวนที่คาดว่าoptions

คำตอบ:


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

การสาธิต

คำอธิบายว่าทำไมเราต้องตั้งค่าterminal: trueและpriority: 1000(จำนวนสูง):

เมื่อ DOM พร้อมแล้ว Angular จะเดิน DOM เพื่อระบุคำสั่งที่ลงทะเบียนทั้งหมดและคอมไพล์คำสั่งทีละรายการโดยขึ้นอยู่กับpriority ว่าคำสั่งเหล่านี้อยู่ในองค์ประกอบเดียวกันหรือไม่ เรากำหนดลำดับความสำคัญของคำสั่งที่กำหนดเองของเราเป็นจำนวนสูงเพื่อให้แน่ใจว่ามันจะถูกรวบรวมก่อนและด้วยterminal: trueคำสั่งอื่น ๆ จะถูกข้ามหลังจากคำสั่งนี้ถูกรวบรวม

เมื่อสั่งของเราเองจะรวบรวมก็จะปรับเปลี่ยนองค์ประกอบโดยการเพิ่มคำสั่งและลบตัวเองและใช้บริการ $ รวบรวมเพื่อรวบรวมคำสั่งทั้งหมด (รวมทั้งผู้ที่ถูกข้าม)

หากเราไม่ได้ตั้งค่าterminal:trueและpriority: 1000มีโอกาสที่จะมีการรวบรวมคำสั่งบางอย่างก่อนคำสั่งที่กำหนดเองของเรา และเมื่อคำสั่งที่กำหนดเองของเราใช้การรวบรวม $ เพื่อรวบรวมองค์ประกอบ => รวบรวมอีกครั้งคำสั่งที่รวบรวมแล้ว สิ่งนี้จะทำให้เกิดพฤติกรรมที่คาดเดาไม่ได้โดยเฉพาะอย่างยิ่งหากคำสั่งที่รวบรวมไว้ก่อนคำสั่งที่กำหนดเองของเราเปลี่ยน DOM แล้ว

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับลำดับความสำคัญและเทอร์มินัลดูที่วิธีการทำความเข้าใจคำสั่งเทอร์มินัล

ตัวอย่างของคำสั่งที่ยังปรับเปลี่ยนแม่แบบคือng-repeat(priority = 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");เพื่อหลีกเลี่ยงการวนซ้ำไม่ จำกัด
Khanh ถึง

4
ตกลงฉันได้รับสามารถจัดเรียงออกคุณจะต้องตั้งค่าreplace: false, terminal: true, priority: 1000; จากนั้นตั้งค่าคุณสมบัติที่ต้องการในcompileฟังก์ชั่นและลบแอตทริบิวต์คำสั่งของเรา สุดท้ายในpostฟังก์ชั่นส่งกลับโดยโทรcompile $compile(element)(scope)องค์ประกอบจะถูกรวบรวมอย่างสม่ำเสมอโดยไม่มีคำสั่งที่กำหนดเอง แต่มีคุณสมบัติเพิ่ม สิ่งที่ฉันพยายามบรรลุไม่ใช่การลบคำสั่งที่กำหนดเองและจัดการทั้งหมดนี้ในขั้นตอนเดียว: ไม่สามารถทำได้ดูเหมือนว่า โปรดดู plnkr ปรับปรุง: plnkr.co/edit/Q13bUt?p=preview
frapontillo

2
โปรดทราบว่าหากคุณต้องการใช้พารามิเตอร์วัตถุคุณลักษณะของฟังก์ชั่นการรวบรวมหรือการเชื่อมโยงรู้ว่าคำสั่งที่รับผิดชอบในการแก้ไขค่าคุณลักษณะมีลำดับความสำคัญ 100 และคำสั่งของคุณต้องมีลำดับความสำคัญต่ำกว่านี้มิฉะนั้นคุณจะได้รับ ค่าสตริงของแอตทริบิวต์เนื่องจากไดเรกทอรีเป็นสถานี ดู (ดูนี้คำขอ GitHub ดึงและปัญหาที่เกี่ยวข้อง )
Simen Echholt

2
เป็นอีกทางเลือกหนึ่งในการลบcommon-thingsแอ็ตทริบิวต์ที่คุณสามารถส่งผ่านพารามิเตอร์ maxPriority ไปยังคำสั่งการคอมไพล์:$compile(element, null, 1000)(scope);
Andreas

10

คุณสามารถจัดการทั้งหมดนี้ได้ด้วยแท็กเทมเพลตธรรมดา ดูhttp://jsfiddle.net/m4ve9/สำหรับตัวอย่าง โปรดทราบว่าจริง ๆ แล้วฉันไม่ต้องการคอมไพล์หรือคุณสมบัติการเชื่อมโยงบนคำจำกัดความ super-directive

ในระหว่างกระบวนการรวบรวม Angular จะดึงค่าเทมเพลตก่อนการรวบรวมดังนั้นคุณสามารถแนบคำสั่งเพิ่มเติมที่นั่นและ Angular จะดูแลให้คุณ

หากนี่เป็นคำสั่งสุดยอดที่จำเป็นต้องรักษาเนื้อหาภายในดั้งเดิมไว้คุณสามารถใช้transclude : trueและแทนที่ภายในด้วย<ng-transclude></ng-transclude>

หวังว่าจะช่วยได้โปรดแจ้งให้เราทราบหากมีสิ่งใดไม่ชัดเจน

อเล็กซ์


ขอบคุณอเล็กซ์ปัญหาของวิธีนี้คือฉันไม่สามารถตั้งสมมติฐานว่าแท็กจะเป็นอย่างไร ในตัวอย่างมันเป็น datepicker เช่นinputแท็ก แต่ฉันต้องการให้มันทำงานกับองค์ประกอบใด ๆ เช่นdivs หรือselects
frapontillo

1
อาใช่ฉันคิดถึงเรื่องนั้น ในกรณีนั้นฉันขอแนะนำให้ติดกับ div และเพียงแค่ทำให้แน่ใจว่าคำสั่งอื่น ๆ ของคุณสามารถทำงานได้ มันไม่ได้เป็นคำตอบที่สะอาดที่สุด แต่เหมาะสมที่สุดกับวิธีการเชิงมุม เมื่อถึงเวลาที่กระบวนการบูทสแตรปเริ่มการรวบรวมโหนด HTML มันได้ทำการรวบรวมคำสั่งทั้งหมดบนโหนดเพื่อการคอมไพล์ดังนั้นการเพิ่มอันใหม่จะไม่ถูกสังเกตเห็นโดยกระบวนการบูทสแทรปดั้งเดิม ขึ้นอยู่กับความต้องการของคุณคุณอาจพบว่าการห่อทุกอย่างใน div และการทำงานภายในนั้นทำให้คุณมีความยืดหยุ่นมากขึ้น
mrvdot

3
@fontontillo คุณสามารถใช้เทมเพลตเป็นฟังก์ชั่นที่มีelementและattrsส่งต่อได้ใช้เวลากับฉันในการทำงานและฉันไม่ได้เห็นมันใช้งานได้ทุกที่ - แต่ดูเหมือนว่าจะทำงานได้ดี: stackoverflow.com/a/20137542/1455709
แพทริค

6

ต่อไปนี้เป็นโซลูชันที่ย้ายคำสั่งที่จำเป็นต้องเพิ่มแบบไดนามิกลงในมุมมองและยังเพิ่มตรรกะเงื่อนไขเพิ่มเติม (พื้นฐาน) สิ่งนี้ทำให้คำสั่งสะอาดโดยไม่มีลอจิกแบบเข้ารหัส

คำสั่งใช้อาร์เรย์ของวัตถุแต่ละวัตถุมีชื่อของคำสั่งที่จะเพิ่มและมูลค่าที่จะผ่านไป (ถ้ามี)

ฉันกำลังดิ้นรนที่จะนึกถึงกรณีใช้งานสำหรับคำสั่งเช่นนี้จนกระทั่งฉันคิดว่ามันอาจมีประโยชน์ในการเพิ่มตรรกะเงื่อนไขที่เพิ่มคำสั่งตามเงื่อนไขบางอย่างเท่านั้น ฉันเพิ่มifคุณสมบัติทางเลือกที่ควรมีค่าบูลนิพจน์หรือฟังก์ชัน (เช่นกำหนดไว้ในคอนโทรลเลอร์ของคุณ) ที่กำหนดว่าควรเพิ่มคำสั่งหรือไม่

ฉันยังมีการใช้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

วิธีแก้ปัญหาง่ายๆที่สามารถใช้งานได้ในบางกรณีคือการสร้างและรวบรวม $ wrapper แล้วผนวกองค์ประกอบต้นฉบับของคุณเข้าด้วยกัน

สิ่งที่ต้องการ...

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

โซลูชันนี้มีข้อดีที่ทำให้สิ่งต่าง ๆ ง่ายขึ้นโดยไม่ต้องคอมไพล์ส่วนประกอบเดิมซ้ำ

สิ่งนี้จะไม่ทำงานหากคำสั่งเพิ่มเติมrequireใด ๆ ของคำสั่งขององค์ประกอบดั้งเดิมหรือหากองค์ประกอบดั้งเดิมมีการวางตำแหน่งที่แน่นอน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.