Tambahkan arahan dari arahan di AngularJS

197

Saya mencoba membangun arahan yang berhati-hati menambahkan lebih banyak arahan ke elemen yang dideklarasikan. Sebagai contoh, saya ingin membangun arahan yang mengurus penambahan datepicker, datepicker-languagedan ng-required="true".

Jika saya mencoba menambahkan atribut-atribut itu dan kemudian menggunakan $compilesaya jelas menghasilkan loop tak terbatas, jadi saya memeriksa apakah saya sudah menambahkan atribut yang diperlukan:

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

Tentu saja, jika saya tidak $compileelemen, atribut akan ditetapkan tetapi arahan tidak akan bootstrap.

Apakah pendekatan ini benar atau saya salah? Adakah cara yang lebih baik untuk mencapai perilaku yang sama?

UDPATE : mengingat fakta bahwa $compilesatu-satunya cara untuk mencapai ini, apakah ada cara untuk melewati pass kompilasi pertama (elemen mungkin berisi beberapa anak)? Mungkin dengan pengaturan terminal:true?

UPDATE 2 : Saya telah mencoba menempatkan arahan ke dalam selectelemen dan, seperti yang diharapkan, kompilasi berjalan dua kali, yang berarti ada dua kali jumlah yang diharapkan option.

frapontillo
sumber

Jawaban:

260

Dalam kasus di mana Anda memiliki beberapa arahan pada elemen DOM tunggal dan di mana urutan penerapannya penting, Anda bisa menggunakan priorityproperti untuk memesan aplikasi mereka. Angka yang lebih tinggi dijalankan terlebih dahulu. Prioritas default adalah 0 jika Anda tidak menentukan satu.

EDIT : setelah diskusi, inilah solusi lengkap untuk bekerja. Kuncinya adalah untuk menghapus atribut : element.removeAttr("common-things");, dan juga element.removeAttr("data-common-things");(dalam kasus pengguna menentukan data-common-thingsdi 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 yang berfungsi tersedia di: http://plnkr.co/edit/Q13bUt?p=preview

Atau:

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

Penjelasan mengapa kita harus mengatur terminal: truedan priority: 1000(angka tinggi):

Ketika DOM siap, sudut berjalan DOM untuk mengidentifikasi semua arahan terdaftar dan menyusun arahan satu per satu berdasarkan priority jika arahan ini berada di elemen yang sama . Kami menetapkan prioritas arahan khusus kami ke angka yang tinggi untuk memastikan bahwa arahan tersebut akan dikompilasi terlebih dahulu dan terminal: true, arahan lainnya akan dilewati setelah arahan ini dikompilasi.

Ketika arahan khusus kami dikompilasi, itu akan memodifikasi elemen dengan menambahkan arahan dan menghapus sendiri dan menggunakan layanan $ compile untuk mengkompilasi semua arahan (termasuk yang dilewati) .

Jika kami tidak menetapkan terminal:truedan priority: 1000, ada kemungkinan beberapa arahan dikompilasi sebelum arahan adat kami. Dan ketika arahan khusus kami menggunakan $ compile untuk mengkompilasi elemen => kompilasi lagi arahan yang sudah dikompilasi. Ini akan menyebabkan perilaku yang tidak dapat diprediksi terutama jika arahan yang dikompilasi sebelum arahan khusus kami telah mengubah DOM.

Untuk informasi lebih lanjut tentang prioritas dan terminal, lihat Bagaimana memahami `terminal` dari direktif?

Contoh dari arahan yang juga memodifikasi template adalah ng-repeat(priority = 1000), ketika ng-repeatdikompilasi, ng-repeat buat salinan dari elemen template sebelum arahan lain diterapkan .

Berkat komentar @ Izhaki, berikut ini adalah referensi ke ngRepeatkode sumber: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

Khanh TO
sumber
5
Ini melempar pengecualian stack overflow kepada saya: RangeError: Maximum call stack size exceededseperti yang terus dikompilasi selamanya.
frapontillo
3
@ frapontillo: dalam kasus Anda, coba tambahkan element.removeAttr("common-datepicker");untuk menghindari loop yang tidak terbatas.
Khanh TO
4
Ok, saya sudah bisa mengatasinya, Anda harus set replace: false, terminal: true, priority: 1000; kemudian atur atribut yang diinginkan dalam compilefungsi dan hapus atribut direktif kami. Akhirnya, dalam postfungsi yang dikembalikan oleh compile, panggil $compile(element)(scope). Elemen akan dikompilasi secara teratur tanpa arahan khusus tetapi dengan atribut yang ditambahkan. Apa yang saya coba capai adalah tidak menghapus arahan khusus dan menangani semua ini dalam satu proses: sepertinya ini tidak bisa dilakukan. Silakan lihat plnkr yang diperbarui: plnkr.co/edit/Q13bUt?p=preview .
frapontillo
2
Perhatikan bahwa jika Anda perlu menggunakan parameter objek atribut fungsi kompilasi atau tautan, ketahuilah bahwa arahan yang bertanggung jawab untuk menginterpolasi nilai atribut memiliki prioritas 100, dan arahan Anda harus memiliki prioritas yang lebih rendah daripada ini, atau Anda hanya akan mendapatkan nilai string atribut karena direktori menjadi terminal. Lihat (lihat permintaan tarik github ini dan masalah terkait ini )
Simen Echholt
2
sebagai alternatif untuk menghapus common-thingsatribut Anda bisa melewati parameter maxPriority ke perintah kompilasi:$compile(element, null, 1000)(scope);
Andreas
10

Anda benar-benar dapat menangani semua ini hanya dengan tag templat sederhana. Lihat http://jsfiddle.net/m4ve9/ untuk contoh. Perhatikan bahwa saya sebenarnya tidak memerlukan properti kompilasi atau tautan pada definisi super-direktif.

Selama proses kompilasi, Angular menarik nilai template sebelum kompilasi, sehingga Anda dapat melampirkan arahan lebih lanjut di sana dan Angular akan menanganinya untuk Anda.

Jika ini adalah arahan super yang perlu mempertahankan konten internal asli, Anda dapat menggunakannya transclude : true dan mengganti bagian dalamnya<ng-transclude></ng-transclude>

Semoga itu bisa membantu, beri tahu saya jika ada sesuatu yang tidak jelas

Alex

mrvdot
sumber
Terima kasih Alex, masalah untuk pendekatan ini adalah bahwa saya tidak dapat membuat asumsi tentang apa yang akan menjadi tag. Dalam contoh itu adalah datepicker, yaitu inputtag, tapi saya ingin membuatnya berfungsi untuk elemen apa pun, seperti divs atau selects.
frapontillo
1
Ah, ya, aku merindukan itu. Dalam hal ini saya akan merekomendasikan tetap dengan div dan hanya memastikan arahan Anda yang lain dapat bekerja pada itu. Ini bukan jawaban terbersih, tetapi paling cocok dalam metodologi Angular. Pada saat proses bootstrap mulai mengkompilasi sebuah node HTML, itu sudah mengumpulkan semua arahan pada node untuk kompilasi, jadi menambahkan yang baru di sana tidak akan diperhatikan oleh proses bootstrap asli. Tergantung pada kebutuhan Anda, Anda mungkin menemukan membungkus segala sesuatu dalam div dan bekerja di dalamnya yang memberi Anda lebih banyak fleksibilitas, tetapi juga membatasi di mana Anda dapat meletakkan elemen Anda.
mrvdot
3
@ frapontillo Anda dapat menggunakan templat sebagai fungsi dengan elementdan attrsmeneruskan. Butuh waktu lama untuk menyelesaikannya, dan saya belum melihatnya menggunakannya di mana pun - tetapi tampaknya berfungsi dengan baik: stackoverflow.com/a/20137542/1455709
Patrick
6

Inilah solusi yang menggerakkan arahan yang perlu ditambahkan secara dinamis, ke tampilan dan juga menambahkan beberapa logika kondisional opsional (dasar). Ini menjaga arahan tetap bersih tanpa logika kode-keras.

Arahan mengambil array objek, masing-masing objek berisi nama direktif yang akan ditambahkan dan nilai untuk diteruskan (jika ada).

Saya berjuang untuk memikirkan use-case untuk arahan seperti ini sampai saya berpikir bahwa mungkin berguna untuk menambahkan beberapa logika kondisional yang hanya menambahkan arahan berdasarkan pada beberapa kondisi (meskipun jawaban di bawah ini masih dibuat-buat). Saya menambahkan opsionalif properti yang harus mengandung nilai bool, ekspresi atau fungsi (misalnya didefinisikan dalam controller Anda) yang menentukan apakah arahan harus ditambahkan atau tidak.

Saya juga menggunakan attrs.$attr.dynamicDirectivesuntuk mendapatkan deklarasi atribut yang tepat digunakan untuk menambahkan direktif (misalnya data-dynamic-directive, dynamic-directive) tanpa nilai string hard-coding untuk memeriksa.

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
sumber
Digunakan dalam template petunjuk lain. Ini bekerja dengan baik dan menghemat waktu saya. Terima kasih.
jcstritt
4

Saya ingin menambahkan solusi saya karena yang diterima tidak bekerja untuk saya.

Saya perlu menambahkan arahan tetapi juga menjaga saya pada elemen.

Dalam contoh ini saya menambahkan arahan gaya-ng sederhana ke elemen. Untuk mencegah loop kompilasi yang tak terbatas dan memungkinkan saya untuk menjaga direktif saya, saya menambahkan cek untuk melihat apakah apa yang saya tambahkan ada sebelum mengkompilasi ulang elemen.

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
sumber
Perlu dicatat bahwa Anda tidak dapat menggunakan ini dengan transclude atau templat, karena kompilator mencoba untuk menerapkannya kembali pada putaran kedua.
spikyjt
1

Coba simpan status dalam atribut pada elemen itu sendiri, seperti superDirectiveStatus="true"

Sebagai contoh:

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

        }

      }
    };
  });

Saya harap ini membantu Anda.

Kemal Dağ
sumber
Terima kasih, konsep dasarnya tetap sama :). Saya mencoba mencari cara untuk melewati pass kompilasi pertama. Saya telah memperbarui pertanyaan aslinya.
frapontillo
Kompilasi ganda memecah hal-hal dengan cara yang mengerikan.
frapontillo
1

Terjadi perubahan dari 1.3.x ke 1.4.x.

Di Angular 1.3.x ini berfungsi:

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

Sekarang dalam Angular 1.4.x kita harus melakukan ini:

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

(Dari jawaban yang diterima: https://stackoverflow.com/a/19228302/605586 dari Khanh TO).

Thomas
sumber
0

Solusi sederhana yang bisa digunakan dalam beberapa kasus adalah membuat dan $ mengkompilasi pembungkus lalu menambahkan elemen asli Anda ke dalamnya.

Sesuatu seperti...

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

Solusi ini memiliki keuntungan bahwa hal itu tetap sederhana dengan tidak menyusun ulang elemen asli.

Ini tidak akan berfungsi jika salah satu direktif yang ditambahkan adalah requiresalah satu arahan elemen asli atau jika elemen asli memiliki posisi absolut.

plong0
sumber