Vue.js Components: Slots

Vue.js yazılarında slot kullanımına dair bir açıklama düşmememe karşın, örnekler içerisinde, en azından göz aşinalığı oluşturabilmek amacıyla sıklıkla yer vermeye çalıştım.

AA

Kısaca ifade etmek gerekirse; slot, tıpkı HTML elemanları gibi kullanılabilen <slot></slot>, bir bileşene içerik aktarabilmemizi sağlayan, dağıtım noktaları (distribution outlets) elementidir. Basit bir örnek oluşturalım:

 <section id="app" class="section" v-cloak>
    <alert>500 Internal Server Error</alert>
    <alert>400 Bad Request</alert>
    <alert>503 Service Unavailable</alert>
  </section>

alert adında, basit bir component oluşturduk. Component bulma css framework notification element‘inden1 oluşuyor. slot kullanımını p etiketi içerisinde, yalın bir şekilde görebilirsiniz.

// Vue v2.x
Vue.component('alert', {
  template: `
  <div class="notification">
    <h3 class="title is-3">Error!</h3>
    <p class="is-medium"><slot></slot></p>
  </div>`
});

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

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      //...
    }
  }
}).component('alert', {
  template: `
  <div class="notification">
    <h3 class="title is-3">Error!</h3>
    <p class="is-medium"><slot></slot></p>
  </div>`
}).mount('#app');

Slot’un bu örnekteki görevi component tag’leri içerisinde yer alan içeriği aktarmak.

    <alert>500 Internal Server Error</alert>
    <alert>400 Bad Request</alert>
    <alert>503 Service Unavailable</alert>

Her component farklı bir içeriğe sahip olsa da uygulamada bir değişiklik söz konusu değil. Elbette, aynı işlemi slot kullanmadan da gerçekleştirebilirdik.

  <section id="app" class="section" v-cloak>
    <notification content="500 Internal Server Error"></notification>
    <notification content="400 Bad Request"></notification>
    <notification content="503 Service Unavailable"></notification>
  </section>

Tabi, bu durumda içerikleri prop ile v-text ya da v-html direktiflerine aktarmamız uygun olacaktır.

// Vue v2.x
Vue.component('notification', {
  props:['content'],
  template: `
  <div class="notification is-warning">
    <h3 class="title is-3">Error!</h3>
    <p class="is-medium" v-text="content"></p>
  </div>`
});

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

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      //...
    }
  }
}).component('notification', {
  props:['content'],
  template: `
  <div class="notification is-warning">
    <h3 class="title is-3">Error!</h3>
    <p class="is-medium" v-text="content"></p>
  </div>`
}).mount('#app');

Yukarıdaki işlemler slot’un en yalın (default / unnamed slots) kullanımına örnek olarak gösterilebilir2 3. Ancak, slot’un yetenekleri elbette sadece bu kadar değil4 5. Yeni bir örnek ile slot içeriğinde template ve HTML etiketlerine yer verelim.

<section id="app" class="section" v-cloak>
  <nav class="navbar is-dark" role="navigation" aria-label="main navigation">
    <div class="navbar-menu">
      <div class="navbar-start">
        <nav-item url="/home">Home</nav-item>
        <nav-item url="/about">About</nav-item>
        <nav-item url="/portfolio">Portfolio</nav-item>
        <nav-item url="/form/contact">Contact</nav-item>
      </div>
    </div>
  </nav>
</section>
// Vue v2.x
Vue.component('nav-item', {
  props:['url'],
  template: `
  <a class="navbar-item" href="../vue-js-components-slots">
    <span class="icon is-small"><i class="fas fa-asterisk"></i></span> <slot></slot>
  </a>`
});

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

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      //...
    }
  }
}).component('nav-item', {
  props:['url'],
  template: `
  <a class="navbar-item" href="../vue-js-components-slots">
    <span class="icon is-small"><i class="fas fa-asterisk"></i></span> <slot></slot>
  </a>`
}).mount('#app');

Yukarıdaki örnekte HTML kodlarının da işleme dahil edildiği görülmekte ve bu işlemler / erişimler template’teki diğer elemanların da olduğu üzere aynı instance data ile sağlanmakta. Yani, nav-item içerisindeki slot instance’a erişebilir ancak nav-item‘a erişemez. Özetle, parent template içeriği parent scope’da (instance), child template içeriği child scope’da derlenir.

Unutmadan ekleyeyim. Bir slot değer ile eşleşmezse kendi içeriğinde belirtilen değeri (fallback content) yansıtır. v-text direktifinde herhangi bir değer yansımayacaktır6 7.

<alert>503 Service Unavailable</alert>
<alert></alert>

Yukarıdaki ifadede, <alert></alert> etiketi instance içeriğinde belirttiğimiz Extraordinery Error! değerini göndürecektir.

Vue.component('alert', {
  template: `
  <div class="notification">
    <h3 class="title is-3">Error!</h3>
    <p class="is-medium"><slot>Extraordinery Error!</slot></p>
  </div>`
});

Named Slots

Şimdiye kadar değindiğimiz tüm örneklerde tek bir slot kullanıldığını fark etmişsinizdir. Ancak, bu bir zorunluluk değil. Slot’ları isimlendirerek (named slots) istediğimiz özelliklerde ve farklı alanlarda kullanabilmekteyiz8 9. Örnek bir structure oluşturalım ve slot’ı nasıl dahil edebileceğimize bakalım.

<div class="container">
  <div id="header">
    ...
  </div>
  <div id="main">
    ...
  </div>
  <div id="footer">
    ...
  </div>
</div>

Yukarıdaki structure’da content gelmesi gereken alana <slot></slot> etiketini eklediğimizde her slot tanımı için aynı içerik dönecektir. Bu durumu name attribute ile <slot name=”header”></slot> kontrol edebilmekteyiz. Hemen ilgili örneğimizi güncelleyelim.

<div class="container">
  <div id="header">
    <slot name="header"></slot>
  </div>
  <div id="main">
    <slot name="main"></slot>
  </div>
  <div id="footer">
    <slot name="footer"></slot>
  </div>
</div>

İsimlendirmeler elbette örnek olarak eklendi. Siz, kendi tanımlamalarınızı yapabilirsiniz. Unutmayın, bir slot name attribute olmaksızın eklendiğinde, ön tanımlı olan içeriği döndürür. Semantic UI kullanarak bir örnek işlem gerçekleştirelim.

Semantic UI & Vue.js

Yukarıdaki görselde yer alan card’lar için şu instance işimizi görecektir.

Vue.component('person-card', {
  template: `
  <div class="card">
    <div class="image"><img :src="../vue-js-components-slots" :alt="person.name" :title="person.name" /></div>
    <div class="content">
      <div class="header">{{ person.name }}</div>
      <div class="meta"><a>{{ person.status }}</a></div>
      <div class="description">{{ getFirstName }} is an {{ person.job.toLowerCase() }} living in {{ person.city }}.</div>
    </div>
    <div class="extra content">
      <span class="right floated">Joined in {{ person.date }}</span>
      <span><i class="user icon"></i> {{ person.friends }} Friends</span>
    </div>
  </div>`,

  props: ['person'],
  computed: {
    getFirstName(){
      return this.person.name.split(' ').slice(0, -1).join(' ')
    },
    getProfilePicture(){
      return 'https://semantic-ui.com/images/avatar2/large/' + this.person.avatar
    },
  }
});

// Vue v2.x
new Vue({
  el: '#app',
  data: {
    people: [{
      name: 'Molly Hess',
      avatar: 'molly.png',
      status: 'Coworker',
      job: 'Personal Assistant',
      city: 'Paris',
      date: 2011,
      friends: 35
    }, {
      name: 'Matthew Giampietro',
      avatar: 'matthew.png',
      status: 'Friends',
      job: 'Film-maker',
      city: 'New York',
      date: 2013,
      friends: 75
    }, {
      name: 'Elyse Ossi',
      avatar: 'elyse.png',
      status: 'Coworker',
      job: 'Copywriter',
      city: 'New York',
      date: 2014,
      friends: 151,
    }]
  },
  computed: {
    reOrderPeopleByName(index){
      return this.people.reverse().sort((a, b) => a.date - b.date);
    }
  }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      people: [{
        name: 'Molly Hess',
        avatar: 'molly.png',
        status: 'Coworker',
        job: 'Personal Assistant',
        city: 'Paris',
        date: 2011,
        friends: 35
      }, {
        name: 'Matthew Giampietro',
        avatar: 'matthew.png',
        status: 'Friends',
        job: 'Film-maker',
        city: 'New York',
        date: 2013,
        friends: 75
      }, {
        name: 'Elyse Ossi',
        avatar: 'elyse.png',
        status: 'Coworker',
        job: 'Copywriter',
        city: 'New York',
        date: 2014,
        friends: 151,
      }]
    },
    computed: {
      reOrderPeopleByName(index){
        return this.people.reverse().sort((a, b) => a.date - b.date);
      }
    }
  }
}).mount('#app');
 <div id="app" class="ui container" v-cloak>
    <div class="row">
      <div class="one wide column">
          <div class="ui link cards">

            <person-card v-for="(person, index) in reOrderPeopleByName" :key="'people' + '-' + index" v-bind:person="person"></person-card>

            <person v-for="(person, index) in people" :key="'person' + '-' + index">
              <div slot="profile-picture"><img :src="../vue-js-components-slots" :alt="person.name"  :title="person.name" /></div>
              <div slot="header">{{ person.name }}</div>
              <div slot="name">{{ person.name }}</div>
              <div slot="details">{{ person.name }} is an {{ person.job.toLowerCase() }} living in {{ person.city }}.</div>
              <div slot="time">Joined in {{ person.date }}</div>
              <div slot="friends">{{ person.friends }} Friends</div>
            </person>

          </div>
      </div>
    </div>
  </div>

person-card herhangi bir slot içermemekte ve içeriği doğrudan component içeriğinden sağlanmakta. Ancak, person component header, name, details, time, friends adında slot’ların da yer aldığı bir HTML içeriğine sahip. İçerikten de anlaşıldığı üzere <div slot="name">{{ person.name }}</div> ve/veya <template slot="name">{{ person.name }}</template> yer turucu olarak <slot name="name">...</slot> ile ilişkili hareket etmekte.

Slot’ların name belirtilmediği durumlarda aynı içeriği döndüreceklerinden bahsetmiştim. Ancak, slot kullanımında, dikkat edilmesi gereken bir konudan daha bahsetmem gerekli.

Vue.js & Semantic UI

Bir slot bir wrapper element içerisine yerleştirildiğinde, beraberinde bir div etiketi daha getirmekte. Bu sebeple, örneğin yukarıdaki kodu çalıştırdığınızda person component içeriğindeki profil görsellerinin taştığını göreceksiniz. Bu gibi durumlarda, ilgili etiketi template etiketiyle düzenlememiz gerekir.

<person v-for="(person, index) in people" :key="'person' + '-' + index">
  <template slot="profile-picture"><img :src="../vue-js-components-slots" :alt="person.name"  :title="person.name" /></template>
  <template slot="header">{{ person.name }}</template>
  <template slot="name">{{ person.name }}</template>
  <template slot="details">{{ person.name }} is an {{ person.job.toLowerCase() }} living in {{ person.city }}.</template>
  <template slot="time">Joined in {{ person.date }}</template>
  <template slot="friends">{{ person.friends }} Friends</template>
</person>

Evet, ilgili component’i yukarıdaki gibi düzenlediğimizde sorun kalmayacaktır. Evet, name ile slot tanımlarını iliştilendirebildiğimize göre, bir sonraki başlığa geçip, slot’lara nasıl ayrı scope tanımlayabileceğimize bakalım.

Slot Scopes

Slot’un name ile özelleştirilmesinin yanı sıra, slot tanımında şöyle bir tanıma yer vermiştim; “parent template içeriği parent scope’da (instance), child template içeriği child scope’da derlenir.” Kimi durumlarda, slot ile child component içerisindeki dataya ulaşmak istyebiliriz. Bu durumda, yine v-for direktifi ile listing işlemlerimizi gerçekleştirmemiz, ardından slot-scope ile bir slot prop oluşturmamız gerekir.

  <div id="app" class="ui container" v-cloak>
    <div class="row">
      <div class="one wide column">
        <div class="ui link cards">
          <person v-for="(person, index) in people" :person="person" :key="person.id">
            <template slot-scope="who">
              <p>Name: {{ who.name }}</p>
              <p>Job: {{ who.name }}</p>
              <p>City: {{ who.name }}</p>
            </template>
          </person>
        </div>
      </div>
    </div>
  </div>

Yukarıdaki HTML içeriğinde who ile people içeriğindeki verileri kullanabilmekteyiz.

Vue.component('person', {
  template: `
  <div class="card">
    <div class="content">
      <slot v-bind="person">{{ person.name }}</slot>
    </div>
  </div>`,
  props: ['person']
});

// Vue v2.x
new Vue({
  el: '#app',
  data: {
    people: [{
      id: 1,
      name: 'Molly Hess',
      job: 'Personal Assistant',
      city: 'Paris'
    }, {
      id: 2,
      name: 'Matthew Giampietro',
      job: 'Film-maker',
      city: 'New York'
    }, {
      id: 3,
      name: 'Elyse Ossi',
      job: 'Copywriter',
      city: 'New York'
    }]
  }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      people: [{
        id: 1,
        name: 'Molly Hess',
        job: 'Personal Assistant',
        city: 'Paris'
      }, {
        id: 2,
        name: 'Matthew Giampietro',
        job: 'Film-maker',
        city: 'New York'
      }, {
        id: 3,
        name: 'Elyse Ossi',
        job: 'Copywriter',
        city: 'New York'
      }]
    },
    computed: {
      reOrderPeopleByName(index){
        return this.people.reverse().sort((a, b) => a.date - b.date);
      }
    }
  }
}).mount('#app');

Slot scope ile ilgili yeni geliştirmeler ve kullanımlar için Vue > Scoped Slots başlığını inceleyebilirsiniz10 11. Bu yazı serisini server olmaksızın, CDN aracılığıyla gerçekleştirebileceğimiz işlemlerle sınırlı tutmaya gayret ettim. Ancak, NPM ve CLI kullanımına değineceğim bir sonraki Vue yazı serisi içerisinde slot kullanımına dair ek bilgiler paylaşacağım.

Sonuç Olarak

Ayrıca, aşağıdaki yazıları da hem açıklamalar hem de örnekler için önerebilirim: