AngularJS Directive’leriyle Sağ Tık Menüsü

Yaprak Ayazoğlu —  16 Temmuz 2014 — 3 Comments

Directive’ler AngularJS’in temel taşı ve en kuvvetli özelliğidir. Bu sayede, kendi işimize yarayacak html tagler üretebiliriz. AngularJS kütüphanesiyle yazılmış kodlara baktığımız zaman `ng-app`, `ng-repeat`, `ng-controller` diye gördüğümüz her şey aslında birer `directive`dir. Şimdiye kadar ben de bu `directive`lerin son kullanıcısıydım. Artık kafama koydum ufak da olsa bu deryaya bir adım atacağım 🙂

Detaylı bilgi edinebilmek için bir çok kaynak okudum, teknik konuşmalar izledim ve aslında directivelerden önce `linking`, `compiling`, `scope`, `data binding` gibi kavramların da iyice sindirilmesi gerektiğini fark ettim. Bu araştırma sürecinde AngularJS’in yazarlarından Misko Hevery’nin bu video kaydını oldukça faydalı buldum. Gerek `directive`ler olsun, gerek AngularJS’in çalışma prensibi olsun oldukça basit bir dille ifade edilmiş. `Scope`larla ilgili olarak bu günlük yazısına bir göz atmanızı tavsiye ederim. AngularJS’te tek yönlü bağlama (one way binding), iki yönlü bağlama (two-way binding), metin bağlama (text binding) diye üç tip yöntem var. Bütün bu yöntemler bu kaynakta güzel açıklanmış. Vaktiniz olursa bu kitaba da bakmanızı öneririm.

Bu kadar araştırma yaptıktan sonra artık sağ-tık directive’ini yazmaya başlayalım derim. Öncelikle, bir directive tasarlarken bu elemanın tekrar tekrar kullanılabilecek olması gerekmektedir. Peki, tekrar tekrar kullanılabilecek bir sağ-tık menüsü oluştururken nasıl bir yol izleyelim. Benim aklıma iki yöntem geldi:

  1. Sağ tıklandığı zaman gösterilip(show), DOM elemanı terk edildiği(mouseleave) zaman gizlemek (JSFiddle #1)
  2. Sağ tıklanan elemana context menünüb eklenip(append), çıkartılması(remove). (JSFiddle #2)

Eminim ki bu yöntemlerden daha iyi bir yöntem kozmozun içinde bir yerlerde mevcuttur. Ben de daha detaylı bir araştırma yapıp, bu konu üzerine daha çok emek harcayıp daha iyi bir directive yazabilirdim ama en azından birazcık kötü olsun ama bizim olsun mantığıyla kendi bu konu ile ilgili yaklaşımlarımı özetlemek istedim. Eğer aklınıza başka yöntemler, mevcut yöntemlere eklenmelik güzellikler geliyorsa yorumlarınızı bekliyorum 🙂

Directive Denilince Aklıma Gelenler

Yöntemleri detaylı olarak anlatmadan önce aslında kendi örneklerim üzerinden Directivelerin çalışma mantığını anlatmak istiyorum. 1. yaklaşımın HTML kodundan başlayalım. Aşağıdaki kodda, `id` gibi görmeye alışkın olduğumuz attributelar haricinde `ng-controller`, `ng-contextmenu`, `ng-show` gibi tuhaf attributelar da görüyoruz. Aslında bunların hepsi birer AngularJS Directive’i.

<div ng-controller="MainCtrl">
  <div ng-contextmenu>Right Click On the Item!
     <ul id="contextmenu-node" ng-show="isVisible">
       <li>Item 1</li>
       <li>Item 2</li>
     </ul>
   </div>
</div>

Bu Directive’ler Nasıl İşer Hale Geliyor ki?

  • AngularJS İlk Yüklemesi

Websitemizin `index.html` dosyası içinde  `<script src=”//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.18/angular.min.js”> </script>`  satırını yazığımız andan itibaren aslında AngularJS çatısı artık bizim elimiz kolumuz. Tarayıcı yüklediği Javascript dosyasını aynı zamanda çalıştırıyor. Biz de AngularJS koduna baktığımız zaman en son satırında kendisini çağırdığını görüyoruz.

(function(window, document, undefined) {
'use strict';
...
...//AngularJS magical code here
...
})(window, document); //last line calls to the single function.

Dolayısıyla, tarayıcının Javascript işleyen birimi bu kodu ekler eklemez çalıştırdı. Dikkat ederseniz malum fonksiyonun girdi parametrelerden biri de güncel pencerenin HTML’inin `document` objesi. Angular çalıştığı zaman, bu `document` objesine aynı zamanda `DOMContextLoaded` etkinliğini(event) ekliyor. Yani HTML tamamen ayrıştırılıp da DOM objesi yaratıldığı zaman, Angular kodu geri çağrıyı(callback) yakalayacak, DOM objesinde ki bütün `Directive`leri işleyecek ve çalışır hale getirecek. Tabii ki ilk yükleme kısmı için söylenecek çok fazla şey var ama burada bilinmesi gereken şey, bütün DOM yüklendikten sonra AngularJS’in kendisiyle ilgili bütün directiveleri işliyor olduğu.

Peki ilk yüklemeden sonra neler oluyor?

  • Directivelerin Anası `ng-app`

AngularJS tırım tırım `Directive`lerin anasını yani  `ng-app` i arıyor. `ng-app` yoksa yemek de yok ve sadece bir tane `ng-app` olsun istiyor. İlk aşkı bulduktan sonra diğerlerini de tınlamıyor. Bundan sonra da diğer `Directive`ler işler hale getiriliyor. Yani bizim koddaki `ng-controller`, `ng-contextmenu`, `ng-show` artık bir şey ifade ediyor.

Benim yazdığım directive `ng-contextmenu` nasıl çalışıyor ki?

Peki, Angular `ng-contextmenu`yü işledikten sonra neler oldu? JSFiddle ve benim post arasında mekik dokumayın diye kodu ayağiniza getirdim:

myApp.directive("ngContextmenu", function () {
 contextMenu = {};
 contextMenu.restrict = "AE";
  contextMenu.scope = {
    "isVisible": "="
  };
  contextMenu.link = function (lScope, lElem, lAttr) {

  lElem.on("contextmenu", function (e) {
    e.preventDefault();
    console.log("Element right clicked.");
    lScope.$apply(function () {
       lScope.$parent.isVisible = true;
    });

   });
  lElem.on("mouseleave", function (e) {
    console.log("Leaved the div");
    lScope.$apply(function () {
      lScope.$parent.isVisible = false;
    });
  });
};
return contextMenu;
});

Hizmette sınır yok:)

Dikkat ederseniz en başta bir `contextMenu` objesi yarattım(`var contextMenu = {}`). Sonra bu objeye `restrict`, `scope` ve `link` elemanları tanımladım. En son olarak bu objeyi döndüm. Döndüğüm obje AngularJS’in bir directive tanımlamak için gerekli olan bilgileri içeriyor.

Bu `Restrict` ne ki?

`restrict` bu `Directive`i eleman içinde nasıl tanımlayacağımı ifade ediyor. Sadece “A”, “E” yada “C” harflerini atayabiliyorum. `A` dediğimiz `attribute name`, `E` dediğimiz `element name`, `C` dediğimiz de `class-name` olur. Yani `A` dediğimiz zaman `<div hanimis-benim-directivim></div>`  gibi kullanıyoruz. `E` dediğimiz zaman kendi HTML elemanımı tanımlamış oluyorum ve `<hanimis-benim-directivim></hanimis-benim-directivim>` gibi kullanabiliyorum. `C` dediğim zaman da kendi `class`olarak tanımlayabiliyorum. Mesela, `<div class=’hanimis-benim-directivim’></div>`. Genellikle de `AE` opsiyonları kullanılıyor.

Peki ya Scope?

Scope ile directive’in bulunduğu scope’taki hangi elemanlara erişimi olduğunu ifade edebiliyoruz. Ben scope içinde sadece `isVisible=”=”` gibi bir ifade kullanmışım. Yani diyorum ki: “Benim işim sadece  `isVisible` elemanıyla başka hiç bir şey umrumda bile değil!” Burada `=` işaretiyle iki yönlü bağlama yapmış oluyoruz. Yani scope içinde `isVisible` elemanında yapılan herhangi bir değişiklik `Directive`’in içinde de algılandığı gibi, `Directive`’in içinde yaptığımız değişiklik de scope’da da algılanıyor. Bunun dışında iki farklı binding türü daha var. Artık bunların açıklamalarını da başka bir yazıda konuşuruz.

Link?

Link fonksiyonu, `Directive` DOM elemanına bağlanırken çalıştırılıyor. AngularJS, directiveleri bulup da işlerken directive’in tanımlı olduğu DOM elemanının başını bu link fonksiyonu ile bağlıyor.

Dikkat edilirse, bizim örneğimizde bu fonksiyona lScope, lElem, lAttr gibi girdiler var. lElem bu directive’in bağlandığı eleman oluyor. lScope ise bulunduğu scope oluyor. Dikkat ederseniz link fonksiyonun da elemana `contextmenu` ve `mouseleave` aksiyonlarını ekledim. Yani ilgili elemana sağ tıklandığı zaman `contextmenu` açılsın. Mouse imleci elemandan ayrıldığı zaman da bu menü kapansın istiyorum.

Sağ tık menüsü haricinde bir torba laf ettik. Artık konunun anasına dönelim:

Birinci Yöntem:

İlk yöntem aslında tekrar tekrar kullanılabilme prensibine daha yakın ancak bu durumda da sağ tıkladığım elemandan ayrılıp, menü üzerinde gezmeye karar verirsem sağ tık menüsü kayboluyor. Çünkü burada context-menu en başta HTML içinde yaratılmış. Elemana sağ tıklanması durumunda ise sadece görünürlük durumunu değiştiriliyor. Oysa ki sağ tıklanılan elemanın içine append edilmesi gerekiyor ki context-menu üzerinde gezebilelim. Belki context-div sağ tıklanan elemana dinamik olarak eklenebilir ama bu yöntem nedense pek şık gelmedi.

İkinci Yöntem:

Bu yüzden bir de ekleme & çıkarma yöntemini de denemeye karar verdim. Bu yöntemde, sağ tık menüsünü HTML’de tanımlamak yerine Javascript kodumda(`$scope.myContextDiv`) tanımladım. Şimdi bu durumda

  1. Bunu da `context` iki tane önemli görevi var: HTML’de bu `context-menu`nün kullanılmasını istediğimiz elemanlara attribute olarak eklememiz gerekiyor. Benim örneğimdeki HTML’deki  `<div context-menu=”myContextDiv”> Right Click On the Item!</div>` kod parçacığında da görüldüğü gibi `context-menu` attribute’una scope’ta JS dosyasında yaratmış olduğum(`myContextDiv`) sağ tık menüsünü yazdım. Bu şekilde sağ tık menüsünü bu elemana bağlamış oldum.
  2. Sağ tıklanması durumunda bu context-menüyü ilgili elemana şu şekilde append ettim:  `lElem.append( $compile( lScope[ lAttr.contextMenu ])(lScope) );` Dikkat edersiniz ki elemanı append ederken önce compile ediyorum. AngularJS ilk yüklenme esnasında bütün directiveleri gezip bunları işler hale getiriyordu. Peki biz ilk yüklemeden sonra içinde `Directive`ler barındıran bir kodu DOM’a yüklersek ne olacak? Tabii ki çalışmayacak! Dolayısıyla, ilk açılışta AngularJS’in yaptığını bizim elle yapmamız gerekiyor. İşte `$compile` fonksiyonu tam da bu işe yarıyor.
  3. İmlecin DOM elemanından ayrılması durumunda da sağ tık menüsü, bu elemandan çıkarılıyor.

Eğer sağ tık menüsündeki seçeneklerin çalışıp çalışmadığını görnek de istiyorsanız, tarayıcınızın geliştirici arayüzünü açıp, konsol loglarına bakabilirsiniz.

Yöntemler, kodlama ile ilgilli herhangi bir öneriniz, düzeltmeniz varsa yorum yazmaktan hiç çekinmeyin 🙂

For English version refer to here.

Yaprak Ayazoğlu

Posts

3 responses to AngularJS Directive’leriyle Sağ Tık Menüsü

  1. Yazı için teşekkürler, gayet açıklayıcı olmuş. AngularJS hakkında türkçe olarak makale kıtlığı çekerken bu iyi geldi 🙂

  2. Güzel ve Eğlenceli bir yazı olmuş teşekkürler

  3. Güzel bir Türkçe kaynak.

Alperen Karip için bir cevap yazın Cevabı iptal et