Vue.js Özel Direktifler (Custom Directives)

Vue.js direktiflerinden (directives) ve yeteneklerinden daha önce bahsetmiştim.

AA

Bu yazıda, var olan direktiflere ek olarak, ihtiyaçlarımız doğrultusunda özelliklere sahip özelleştirilmiş direktifleri (custom directives) nasıl oluşturabileceğimizden ve bu süreçte nelere dikkat etmemiz gerektiğinden bahsedeceğim.

Vue.js Custom Directives

Vue.js çeşitli işlemler için ön tanımlı olarak v-if, v-model, v-bind, v-on, v-text gibi belirli yeteneklere sahip pek çok direktifi kullanıma hazır olarak bulundurmakta. Ancak, kimi zaman bu yeteneklerden daha fazlasına ihtiyaç duyulduğunda, kimi zaman ise bazı işlemleri daha pratik hale getirmek amacıyla özelleştirmelere ihtiyaç duyulabilmekte. İşte, bu gibi durumlar için Vue bize local ve/veya global olarak kendi direktiflerimizi oluşturma imkanı sunmakta. Hemen basit bir örnekle konuyu detaylandıralım. Mesela, bir input element’ine daha tıklanmadan focus’lanalım ve bunun yanı sıra border stiline de müdahale edelim.

<input type="text" placeholder="Text me!" v-focus />
// Vue v2.x
Vue.directive('focus', {
  inserted: function (el) {
    el.focus();
    el.style.border='thick solid #0000FF';
  }
});

// Vue v3.x
const app = Vue.createApp({
  //...
})
app.directive('focus', {
  mounted(el) {
    el.focus();
    el.style.border='thick solid #0000FF';
  }
})

Direktifimizin adı v-focus, ilk parametremiz olan name alanı aynı zamanda direktifin de kendisini tanımlamakta ve v- ön ekini de kullanarak elementlere, component’lere dahil edebilmekteyiz. Oluşturduğumuz v-focus direktifi bir global directive. el ile elemente ulaşabilmemizi ve DOM'a müdahale edebilmemizi sağlamakta. Aynı direktifi local olarak da oluşturalım.

// Vue v2.x
directives: {
  focus: {
    inserted: function (el) {
      el.focus();
      el.style.border='thick solid #0000FF';
    }
  }
}

// Vue v3.x
directives: {
  focus: {
    mounted(el) {
      el.focus();
      el.style.border='thick solid #0000FF';
    }
  }
}

İşlemimiz bu kadar. Yeni bir örnek üzerinden özelleştirilmiş direktif oluştururken kullanabileceğimiz hook’lara bakalım.

Custom Directives: Hook Functions

Direktif tanımlarken şu ek hook fonksiyonları da opsiyonel olarak kullanabilmekteyiz.

Vue v2.x

  • bind, elemente ilk bağlandığında, bir kez çağırılır. Tek seferlik işlemler için bu hook fonksiyonundan faydalanabiliriz.
  • inserted, çağırılan element ana node’una (parent node) eklendiğinde işleme alınır.
  • update, taşıyıcı bileşenin (containing component) VNode’unda değişiklik olduğunda, mümkünse alt/iç component’ten önce çağırılır. Direktifin değeri değişebilir ve/veya değişmeyebilir. Ancak, bind edilen değerlere (current values and old values) göz atılarak gereksiz olduğu düşünülen güncellemeler görmezden gelinebilir.
  • componentUpdated, Taşıyıcı component’in ve/veya alt/iç bileşenlerin VNode’unda değişiklik olduğunda çağırılır.
  • unbind, elementten koparıldığında, bir kez çağırılır

Vue v3.x

  • beforeMount, öğeye ilk bağlandığında ve üst bileşen bağlanmadan önce çağrılır. Bu, tek seferlik çalışmalar yapılabilecek bir aşamadır.
  • mounted, bağlı elemanın üst bileşeni oluşturulduğunda çağrılır.
  • beforeUpdate, VNode'u güncellenmeden önce çağrılır
  • updated, bileşenin VNode ve alt bileşenlerinin VNode'ları güncellendikten sonra çağrılır
  • beforeUnmount, elemanın üst bileşeni ile ilişkisi kesilmeden önce çağrılır
  • unmounted, öğeden ayrıldığında ve ana bileşenin bağlantısı kesildiğinde yalnızca bir kez çağrılır.

Argumanlarla ilgili hook (el, binding, vnode, and prevVnode) detaylarını Custom Directive API sayfasından detaylı bir şekilde görebilirsiniz.1

Bind ile hemen bir örnek işlem gerçekleştirelim.

<section id="app" class="section" v-cloak>
  Width: {{ window.width }}, Height: {{ window.height }}
  <hr />
  <h1 class="title is-1" v-wsize:top="[window.width, window.height]">First Level</h1>
  <p class="is-medium">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vel efficitur ligula, vel lacinia tortor. Fusce eu quam arcu. Donec blandit sapien ut diam finibus venenatis. Mauris eget nisl ut diam tincidunt eleifend. Sed ut ultrices nibh. Sed quis turpis quis elit vulputate laoreet. Aenean sagittis euismod tortor, non dignissim mauris dapibus nec. Etiam est velit, luctus vel luctus ac, sagittis eget odio. In tristique tincidunt nunc id dictum. In vitae nisi a odio porttitor interdum. Morbi nec leo felis. Vestibulum dolor neque, luctus eget augue id, molestie feugiat magna. Vivamus fermentum lectus vel erat posuere interdum. Aliquam feugiat magna a risus faucibus rhoncus.</p>
  <hr />
  <h2 class="title is-2" v-wsize="[window.width-400]">Second Level</h2>
  <p class="is-medium">Fusce nisl metus, pharetra et mollis sed, iaculis malesuada justo. Etiam aliquam mi vitae nulla aliquet, vitae condimentum nisi pulvinar. Aliquam eu imperdiet sapien, pulvinar feugiat nisi.</p>
  <hr />
  <h3 class="title is-3" v-wsize.on="[window.width - window.height]">Third Level</h3>
  <p class="is-medium">Nullam porta orci ut viverra blandit. Nulla lorem sem, convallis non tortor sit amet, luctus placerat sapien. Nullam lectus tortor, lacinia vitae nisl nec, tempus feugiat nisi. Praesent ac finibus dui.</p>
</section>
// Vue v2.x
Vue.directive('wsize', {
  bind(el, binding, vnode) {
    const message = 'width: ' + binding.value[0] + ', height: ' + binding.value[1]
    el.addEventListener('click', function () {
      el.style.border='thick solid #0000FF';
      console.log('Binding arg: ' + binding.arg)
      console.log('Binding value: ' + message)
      console.log('Binding modifiers: ' + JSON.stringify(binding.modifiers))
    });
  }
});

var app = new Vue({
  el: '#app',
  data:{
    window: {
      width: 0,
      height: 0
    }
  },
  created() {
    window.addEventListener('resize', this.handleResize)
    this.handleResize();
  },
  destroyed() {
    window.removeEventListener('resize', this.handleResize)
  },
  methods: {
    handleResize() {
      this.window.width = window.innerWidth;
      this.window.height = window.innerHeight;
    }
  }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      window: {
        width: 0,
        height: 0
      }
    }
  },
  created() {
    window.addEventListener('resize', this.handleResize)
    this.handleResize();
  },
  unmounted() {
    window.removeEventListener('resize', this.handleResize)
  },
  methods: {
    handleResize() {
      this.window.width = window.innerWidth;
      this.window.height = window.innerHeight;
    }
  }
}).directive('wsize', {
  bind(el, binding, vnode) {
    const message = 'width: ' + binding.value[0] + ', height: ' + binding.value[1]
    el.addEventListener('click', function () {
      el.style.border='thick solid #0000FF';
      console.log('Binding arg: ' + binding.arg)
      console.log('Binding value: ' + message)
      console.log('Binding modifiers: ' + JSON.stringify(binding.modifiers))
    });
  }
}).mount('#app');

Yukarıdaki örnek tarayıcı penceresinin genişlik ve yükseklik değerlerini created() ve handleResize method’u ile alıp data‘ya aktarmakta. Pencere genişliği ve yüksekliği değiştirildiğinde ilgili değerlerin de reaktif bir şekilde güncellendiğini görebilirsiniz. Gelelim custom directive kullanımına. Görüldüğü üzere wsize adında bir global directive oluşturulmuş.

Vue.directive('wsize', {
  //...
});

Oluşturulan direktive içeriğinde bind hook fonksiyonunu barındırıyor. Hemen araya basit bir örnek ekleyeyim ve ardından ana örnek ile devam edelim.

Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})

el ile elemente ulaşabilir ve DOM‘a müdahale edebiliriz. Bu işlemin tek seferlik işleme alınacağını unutmamalısınız. Ek olarak, bind hook içerisinde bir olay da tanımlı ve el üzerinden ilgili element ile ilişkili bir olay gerçekleştirildiğinde işleme alınmakta.

el.addEventListener('click', function () {
  //...
});

İlgili tüm işlemleri toparlayacak ve bir arada görüntüleyecek olursak;

bind(el, binding, vnode) {
  const message = 'width: ' + binding.value[0] + ', height: ' + binding.value[1]
  el.addEventListener('click', function () {
    el.style.border='thick solid #0000FF';
    console.log('Binding arg: ' + binding.arg)
    console.log('Binding value: ' + message)
    console.log('Binding modifiers: ' + JSON.stringify(binding.modifiers))
  });
}

İnternet tarayıcınızın console’u üzerinden başlıkları tıklayarak argümanlar sayesinde bazı değerlere ulaşabiliriz; binding.arg, binding.value, binding.modifiers ve değişiklikleri takip edebiliriz. Peki, bu değerler tam olarak ne ifade etmekteler?

Directive Hook Arguments

Argümanların tamamı ve değinmediğim diye detaylar için Vue.js > Custom Directives > Directive Hook Arguments bölümüne göz atabilirsiniz1 2. Sıklıkla kullanılan bir direktif üzerinden bakalım:

<button v-on:click.prevent="addItem">Item Ekle</button>

Yukarıdaki örnekte, v-on direktifi click argument ve prevent modifier’ına sahiptir. addItem ise direktifin aldığı değerdir. Hemen liste halinde bu alanları açıklayalım.

  • el: Direktifin bağlanacağı element. Doğrudan DOM’u manipüle etmek için kullanılabilir.
  • binding: Bir object tanımıdır ve şu özellikleri (properties) barındırır.
    • name, direktifin adı (v- prefix olmadan tanımlanır, ancak bu prefix ile çağırılır).
    • value, direktife atanan değerdir. Örneğin, v-my-directive="1 + 1" tanımında değer 2 olacaktır.
    • oldValue, update ve componentUpdated kullanımında son atanan değil, bir önceki değerdir. Değer değişikliği olmaksızın kullanılabilir.
    • expression, atanan değeri string olarak alır. Örneğin, v-my-directive="1 + 1" için expression "1 + 1" şeklinde olacaktır.
    • arg, direktife atanan argümandır. Örneğin, v-my-directive:foo için argüman "foo" olacaktır.
    • modifiers, bir object modifier içerebilir. Örneğin, v-my-directive.foo.bar‘nun içerdiği modifier’lar { foo: true, bar: true } olacaktır..
  • vnode, Vue compiler tarafından oluşturulan virtual node’dur.
  • oldVnode, update ve componentUpdated hook’ları ile erişilebilen bir önceki virtual node’dur.

Yukarıdaki örnekte, bir başlığı tıkladığınızda hem başlığın style’ına müdahale edilmekte hem de console.log ile argument.arg, argument.value ve argument.modifiers değerleri yansıtılmaktadır.

Dinamik Argümanlar

Ayrı bir başlık altında eklemekte fayda var. Argümanlar, aynı zamanda dinamik olabilmektedirler; v-directive:argument=[dataproperty]. Yine, yukarıdaki örnekte görüldüğü üzere v-wsize:top="[window.width, window.height]" ile window.width ve window.height değerleri dinamik olarak atanmaktadırlar.

Son Olarak

Evet, Vue.js temel ihtiyaçlar çerçevesinde ön tanımlı olarak oldukça kapsamlı yetenekler barındıran direktiflere sahip. Ancak, Vue bu ön tanımlı direktiflerin yanı sıra, daha pratik ve/veya ihtiyaçlara göre özelleştirilmiş yetenekler barındırmasını istediğimiz direktifler için de özelleştirilmiş tanımlar yapma imkanı sunmakta.