Vue.js Components: Custom Events

Vue.js Component Props başlıklı yazıda, Olay (Event) İle Değer Aktarmak (Emitting) başlığı altıda olayların (events) oluşturulması ve aktarılmasına değinmeye çalışmıştım.

AA

Bu yazıda, event (olay) kullanımını biraz daha detaylandırmak, component kullanımlarıyla da ilişkili olarak işlemlerin nasıl paylaşılabileceğinden bahsetmek istiyorum.

Custom Events

Props yazısında basit bir sepet (cart) örneği oluşturmuştuk. Ürünleri component olarak listelemiş, buton aracılığıyla da cart’a eklenmelerini (cart ürün adet değerini artırarak) sağlamıştık. Bu yazıda ise örnekleri değiştirerek ilerleyeceğim. Ek olarak, yakın zamanda Bulma CSS ve Semantic UI CSS framework‘lerinden bahsettim. Bu vesile ile, örnekleri daha kapsamlı bir şekilde sunmak istiyorum. Konuyu çok dağıtmadan öncelikle event tanımlama konusuna bakalım.

Component ve prop tanımlarken dikkat ettiğimiz, automatic case transformation kuralı vardı. Bu kural neticesinde PascalCase veya camelCased bir tanımlama otomatik olarak (JS ve HTML kurallarına göre) yeniden ele alınırdı. Emitted event herhangi bir kural barındırmaz ve tanımlanan ne ise o şekilde ulaşılır. Örneğin anEvent şeklinde tanımladığımız olaya an-event veya anevent yerine, yine anEvent ile ulaşabiliriz.

this.$emit('anEvent')

Aşağıdaki kullanımlarla anEvent ilişkisini test edebilirsiniz.

<component v-on:an-event="..."></component>
<component v-on:anevent="..."></component>
<component v-on:anEvent="..."></component>

Ayrıca, component ve prop’un aksine, event isimleri JS variable veya propperty isimlerinde kullanılamaz. Bu nedenle camelCase ve/veya PascalCase gereksiz olacaktır. Diğer yandan, DOM şablonları (templates) otomatik olarak listener’ları lowercase olarak ele alacaktır. Örneğin, v-on:anEvent kullanımı v-on:anevent olarak değerlendirilir. Bu nedenle, event isimleri için kebab-case kullanımı önerilir. Örneğin, v-on:an-event. Şimdi, basit bir örnek işlem gerçekleştirelim.

  <section id="app" class="section">
    <div class="container">
      <p>{{ count }}</p>
      <hr />
      <comp v-on:calculate="count += $event">New Day!</comp>
      <comp v-on:calculate="count += $event">New World!</comp>
    </div>
  </section>

Oluşturduğumuz component’i istediğimiz kadar çoğaltabiliriz. Her durumda, component içeriğinde yer alan butonlar tanımlayacağımız olayı (event) işleyeceklerdir. Olayımız şöyle olsun: v-on:click="$emit('...', data)".

// Vue v2.x
Vue.component('comp', {
  template: `
    <div class="box">
      <p><slot></slot></p>
      <button v-on:click="$emit('calculate', 1)">+1</button>
      <button v-on:click="$emit('calculate', -1)">-1</button>
    </div>`,
});

var app = new Vue({
  el: '#app',
  data: {
    count: 0
  }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      count: 0
    }
  }
});

app.component('comp', {
  template: `
    <div class="box">
      <p><slot></slot></p>
      <button v-on:click="$emit('calculate', 1)">+1</button>
      <button v-on:click="$emit('calculate', -1)">-1</button>
    </div>`
});

app.mount('#app');

Evet, button elementine tıklama sonucunda işleme alacağı olayı calculate adıyla ilettik. Bu olay aynı zamanda +1 ve -1 değerlerini de taşımakta. Şimdi, comp adıyla oluşturduğumuz component içerisinden gelen olayı dinlememiz gerekiyor. v-on:calculate ile olayı dinliyoruz. Bu aşamada v-on:calculate="count += $event" ile $event‘i yakalamış ve beraberinde getirdiği değeri count değerine eklemiş oluyoruz. Yani, component içerisindeki butonlar $root içerisinde yer alan count değerine müdahale edebiliyorlar. Örneğin, yeni bir component daha ekleyelim, ancak listen durumunu bu defa bir method ile ilişkilendirelim.

<comp v-on:calculate="counter">New World!</comp>

Vue instance içeriğine şu method tanımını ekleyelim.

methods: {
  counter(val){
    console.log(val)
  }
}

Eklediğimiz component içeriğindeki butonlara tıkladığımızda console’da buton değerini görebilirsiniz. Görüldüğü üzere, inline statement kullanımında $event property ile işlemler yapabilmekteyiz. Yukarıdaki örneği biraz daha geliştirelim ve v-on:click="..." içerisinde method ile işlem gerçekleştirelim.

  <section id="app" class="section">
    <div class="container">
      <div class="field is-grouped">
        <comp @applied="compApplied">Comp 1</comp>
        <compnew @applied="compApplied">Comp 2</compnew>
        <custom-input v-model="searchText" @writesmth="searchText = $event"></custom-input>
      </div>

      <div class="box" v-if="status">
        <div class="media-content">
          <div class="content">
            <p v-if="!searchText"><strong>It's a new world! Isn't it?</strong></p>
            <p v-else>{{ searchText }}</p>
          </div>
        </div>
      </div>
    </div>
  </section>

comp ve compnew adında 2 component oluşturalım. İlk component bir önceki örnekte olduğu gibi inline olarak event'i işlesin1 2. Bir diğer component ise method olarak aynı etkinliği işleme alsın.

// Vue v2.x
Vue.component('comp', {
  template: `
    <p class="control">
      <a class="button" @click="childCompApplied">
        <slot></slot>
      </a>
    </p>
  `,
  methods: {
    childCompApplied() {
      this.$emit('applied')
    }
  }
});

Vue.component('compnew', {
  template: `
  <p class="control">
    <a class="button" @click="$emit('applied')">
      <slot></slot>
    </a>
  </p>
  `,
});

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      class="input" type="text"
      v-bind:value="value"
      placeholder="Type something..."
      @input="$emit('writesmth', $event.target.value)">
  `
});

var app = new Vue({
  el: '#app',
  data: {
    status: false,
    count: 0,
    searchText: ''
  },
  methods: {
    compApplied() {
      (this.status == false) ? this.status = true: this.status = false;
    }
  },
  computed: {
    //
  }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      status: false,
      count: 0,
      searchText: ''
    }
  },
  methods: {
    compApplied() {
      (this.status == false) ? this.status = true: this.status = false;
    }
  },
  computed: {
    //
  }
})

app.component('comp', {
  template: `
  <p class="control">
    <a class="button" @click="childCompApplied">
      <slot></slot>
    </a>
  </p>
  `,
  methods: {
    childCompApplied() {
      this.$emit('applied')
    }
  }
});

app.component('compnew', {
  template: `
  <p class="control">
    <a class="button" @click="$emit('applied')">
      <slot></slot>
    </a>
  </p>
  `,
});

app.component('custom-input', {
  props: ['value'],
  template: `
    <input
      class="input" type="text"
      v-bind:value="value"
      placeholder="Type something..."
      @input="$emit('writesmth', $event.target.value)">
  `
});

app.mount('#app');

Görüldüğü üzere, sonuçta hiçbir farklılık yok; event fire ve listen süreçleri aynı şekilde gerçekleşmekte3 4. Yine, yukarıdaki örnekte custom-input adında bir component yer almakta. Form input elemanı içeriğine bir değer girildiğinde, value searchText = $event ile searchText‘e değer olarak aktarılmakta.

v-model Direktifi

Varsayılan olarak, v-model içeren bir component değeri (value) prop ve input event olarak alır. Ancak, checkbox ve radio button gibi input tiplerinde value farklı amaçlarla kullanılabilir. model: { prop: '', event: ''} option bu gibi durumlarda ortaya çıkabilecek sorunları önlemeye yardımcı olabilir.

<base-checkbox v-model="lovingVue"></base-checkbox> {{ lovingVue }}
// Vue v2.x
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
});

// Vue v3.x
app.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
});

Inline statement ve method ile örnek işlemler yaptık. Bu örneklerde parent ve child component iletişimine değindik. Peki, sürece ilişkisiz component’ler arasında iletişimi dahil etmek istersek ne yapmalıyız?

Herhangi bir Vue instance $emit event’i dinleyebilir (listen). O halde şöyle bir kullanım işimize yarayabilir: window.Event = new Vue();

window.Event = new class {
  constructor() {
    this.vue = new Vue();
  }

  fire(event, data = null) {
    this.vue.$emit(event, data);
  }

  listen(event, callback) {
    this.vue.$on(event, callback);
  }
}

// Vue v2.x
Vue.component('comp', {
  template: `
  <div @click="compApplied">
    <slot></slot>
  </div>`,
  methods: {
    compApplied() {
      Event.fire('applied')
    }
  }
});

Vue.component('compnew', {
  template: `
  <comp @applied>Hello!</comp>`,
  methods: {
    compApplied() {
      Event.fire('applied')
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    status: false,
  },
  created() {
    Event.listen('applied', () => (this.status == false) ? this.status = true : this.status = false)
  }
});

// Vue v3.x
//
//
//
//

$emit ve $on yerine constructor ile fire ve listen tanımlamalarını yaptık ve tüm işlemlerimizin yine aynı şekilde uygulandığını gördük.

  <section id="app" class="section" v-cloak>
    <comp @applied>Hello!</comp>
    <compnew></compnew>
    <p v-show="status"><strong>It's a new world! Isn't it?</strong></p>
  </section>

Custom events ile ilgili örnekler ve anlatımlar şimdilik bu kadar. Özellikle bu yazı çerçevesinde laracasts.com eğitimlerinin çok faydasını gördüm. Ayrıca, bu yazıyı yayınladığım zaman içerisinde Vue 3 ile ilgili de yeni paylaşımlar yapılmaktaydı. Özellikle, Chris Fritz‘in (Core Team Member) Vue 3: What I’m Most Excited About sunumunu yazı ile de ilintili olması vesilesiyle öneriyorum.