Vue.js Bileşenler (Components)

Bir web sitesi ya da uygulama pek çok parçadan oluşmakta. Kabaca tarif etmek gerekirse, header, nav, footer ve sidebar genellikle içerik alanı dışında kullandığımız diğer parçalardır diyebiliriz. Ayrıca, çoğu zaman bu yapılar da daha alt parçaların bir araya gelmesiyle oluşmaktadırlar.

AA

Header alanı top bar ve search formu içerebilir, footer sayfa bağlantıları ve infodan oluşabilir, sidebar yazı önerileri gibi içerikleri kapsayabilir.

<header>
    <topbar-item v-for="item in topItems"></topbar-item>
</header>
<main>
    <blogpost v-for="post in blogPosts"></blogpost>
</main>
<aside>
    <sidebar-item v-for="item in sideItems"></sidebar-item>
</aside>
<footer></footer>

Elbette bu detaylar uygulamanın ve/veya web sayfasının amacına uygun olarak, ihtiyaçlar çerçevesinde pek çok şekilde ele alınabilir. Parçalardan oluşan, kendi içinde bir bütünlük oluşturan bu yapının kolay yapılandırılabilir olması bir avantaj sağlamaktadır. Bu sayede, benzer yapıları tekrar tekrar kullanabilir, daha az kodla daha fazla işlem gerçekleştirerek performans sağlayabiliriz. Template sistemleri ile ilgili yazıda da bahsettiğim üzere bu amaca yönelik farklı noktalara odaklanan pek çok çözüm yolu mevcut. Vue.js bu süreci (Angular ve React ile benzer şekilde) bileşenlerle (components) yönetmeyi mümkün kılmakta.

Vue.js Bileşenler (Components)

Yukarıdaki örnek bileşen yapısına tekrar bir göz atalım. Basitleştirilmiş şekilde paylaştığım bu örnek gibi, web sitenizi ve/veya uygulamanızı tasarlamaya / kodlamaya başladığınızda yapıların da netleştiğini görecek ve hatta çok daha alt bileşenler oluşturmaya çalışacaksınız. Vue.js tüm bu ihtiyaçlara cevap olarak birkaç farklı şekilde bileşenler oluşturmamızı sağlar. Aşağıda bu farkılıklara dair açıklamalar ve basitten karmaşığa doğru bileşenler oluşturmaya dair örnekler paylaşmaya çalışacağım. Bu yapıların bir kural değil, ihtiyaçlar çerçevesinde ortaya çıkan çözümler olduğunu unutmayın. Bu nedenle, web sayfası ve/veya uygulamanın kullanımını göz önünde bulundurarak kendi bileşenlerinizi oluşturmaya çalışın.

Oldukça basit bir örnekle başlayalım:

<div id="app">
    <div v-cloak>
        <h1>{{ message }}</h1>
        <who></who>
    </div>
</div>
// Vue v2.x
const app = new Vue({
    el: '#app',
    data: {
        message: 'Hello World!',
    },
    components: {
        who: {
            template: `
      <div>
        <p>Edgar Allan Poe</p>
        <p>Born: January 19, 1809</p>
        <p>Died‎: ‎October 7, 1849</p>
      </div>`
        }
    }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      message: 'Hello World!',
    }
  },
  components: {
    who: {
      template: `
      <div>
        <p>Edgar Allan Poe</p>
        <p>Born: January 19, 1809</p>
        <p>Died‎: ‎October 7, 1849</p>
      </div>`
    }
  }
}).mount('#app');

Aynı işlemi şu şekilde de gerçekleştirebiliriz:

// Vue v2.x
const About = {
    template: `
  <div>
    <p>Edgar Allan Poe</p>
    <p>Born: January 19, 1809</p>
    <p>Died‎: ‎October 7, 1849</p>
  </div>`
}

const app = new Vue({
    el: '#app',
    data: {
        message: 'Hello World!',
    },
    components: {
        who: About
    }
});

// Vue v3.x
const About = {
  template: `
  <div>
    <p>Edgar Allan Poe</p>
    <p>Born: January 19, 1809</p>
    <p>Died‎: ‎October 7, 1849</p>
  </div>`
}

const app = Vue.createApp({
  data() {
    return {
      message: 'Hello World!',
    }
  },
  components: {
    who: About
  }
}).mount('#app');

Bu örnekte bizim için 2 önemli nokta var. İlki #app içeriğinde yer alan <who></who> , diğeri de Vue instance içeriğindeki components nesnesi. Aslında, iki alanın da ilişkili olduğu aşikar. Bu basit örnek üzerinden Vue bileşenlerinin (components) nasıl çalıştığına bir bakalım.

Yazının giriş bölümünde, Vue component oluşturmak için (component registration) birden fazla yöntem olduğundan bahsetmiştim. Yukarıdaki yöntem bunlardan biri ve en temel olanı diyebiliriz. Tıpkı bir computed prop, method gibi bir components nesnesi altında istediğimiz sayıda component oluşturabilmekteyiz. Who olarak ifade edilmiş olan component bunlardan biri ve örneğimiz içerisindeki who etiketini ifade etmekte. Görüldüğü üzere component tanımı aynı zamanda etiket olarak kullanılmakta ve içeri template olarak tanımlanan ne ise o şekilde oluşturulmakta. Template tanımında yer alan div bir root element ve p etiketlerini kapsamakta. Bileşen işlemlerinde tıpkı normal instance tanımlamalarında olduğu gibi bir ana kapsayıcı kullanma zorunluluğu söz konusu. İlgili div etiketini kaldırdığınızda ya da yazmayı unuttuğunuzda, Component template should contain exactly one root element. uyarısını alma nedeniniz de bu olacaktır. Şimdi de bir diğer component tanımlamasına geçelim.

// Vue v2.x
Vue.component('person', {
    template: `
  <div>
    <p>Edgar Allan Poe</p>
    <p>Born: January 19, 1809</p>
    <p>Died‎: ‎October 7, 1849</p>
  </div>`
});

const app = new Vue({
    el: '#app',
    data: {
        message: 'Hello World!',
    }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      message: 'Hello World!',
    }
  }
})

app.component('person', {
  template: `
  <div>
    <p>Edgar Allan Poe</p>
    <p>Born: January 19, 1809</p>
    <p>Died‎: ‎October 7, 1849</p>
  </div>`
});

app.mount('#app');

Bileşen tanımlamamızı bu defa Vue.component('person', { template: '...' }); şeklinde yaptık ve bileşen adımızı person olarak belirledik. Görüldüğü üzere tanımlanış biçimi dışında kurallarımız aynı. Unutmadan, az önceki örnek ile birlikte 2 biçimi de beraber kullanmak mümkün.

Şimdi, örneklere kısa bir ara verip yerel (local) ve evrensel (global) bileşenlerden bahsetmek istiyorum.

Global Component Registration

Bir bileşen (component) evrensel (global) olarak tanımlandığında herhangi bir root Vue instance içerisinde rahatlıkla kullanılabilirler.

Vue.component('name-1', {
    /* ... */ })
Vue.component('name-2', {
    /* ... */ })
Vue.component('name-3', {
    /* ... */ })

// Vue v2.x
new Vue({
    el: '#app'
    //...
})

// Vue v3.x
const app = Vue.createApp({
    //...
}).mount('#app');

Unutmadan, yukarıdaki örnek bileşen içeriklerinde yer alan name her bileşen için özel ve ayrıştırıcı olmalıdır. Ad tanımlarken kebab-case ve/veya PascalCase kullanılabilir. Bileşen adı, bir bileşenin aldığı ilk argüman olma özelliğine sahiptir1 2.

Tekrar yukarıdaki global compotent registration örneğine dönecek olursak, ilgili tanımlamaları aşağıdaki şekilde kullanabiliriz.

<div id="app">
    <name-1></name-1>
    <name-2></name-2>
    <name-3></name-3>
</div>

ya da

<div id="app">
    <component is="name-1"></component>
    <component is="name-2"></component>
    <component is="name-3"></component>
</div>

Görüldüğü üzere tek bir root Vue instance’imiz var (#app) ve bileşenlerimizin hepsi bu kapsayıcı (wrapper) içerisinde kullanılmaktalar. Aynı şekilde, dilersek oluşturduğumuz bu bileşenleri birbirlerinin alt bileşenleri olarak da kullanabiliriz.

Local Component Registration

Global component registration dışında bazı gereksinimler söz konusu olabilir. Örneğin, webpack gibi bir derleme sistemi (build system) kullanmak gerektiğinde tüm bileşenler evrensel olarak kaydedilecektir. Bu bileşenlerden bazılarını kullanmayı bıraksak bile son derlemenize yine de dahil edileceklerdir. Bu, gereksiz yük ve performans sorunu anlamına gelmektedir. Çözüm olarak bileşenler JavaScript nesneleri (objects) olarak oluşturulabilirler ve kullanılacak olan bileşen components option olarak instance’a dahil edilebilir.

const name1 = {
    /* ... */ }
const name2 = {
    /* ... */ }
const name3 = {
    /* ... */ }

// Vue v2.x
new Vue({
    el: '#app',
    components: {
        'name-1': name1,
        'name-2': name2
    }
});

// Vue v3.x
const app = Vue.createApp({
    components: {
        'name-1': name1,
        'name-2': name2
    }
}).mount('#app');

Unutmadan, yerel (local) olarak oluşturulan bileşenler alt bileşen (subcomponent) olarak kullanılamazlar ve ilgili bileşen içeriğinde yeniden oluşturulmaları gerekir.

Babel ve Webpack gibi ES2015+ modülleri kullanılıyor ise bileşen tanımları şu şekildedir3 4:

require name1 from './name1.vue'
import name2 from './name2.vue'

export default {
    components: {
        // 'name-1': name1
        name1,
        name2
    },
    // ...
}

Evet, component registration sonrasında örneklere ve bileşen özelliklerine kaldığımız yerden devam edebiliriz.

Çoklu Bileşen Kullanımı

Yazıdaki ilk örneğimizi biraz eklemeler yaparak ve global olarak yeniden ele alalım.

<div id="app">
    <div v-cloak>
        <h1>{{ message }}</h1>
        <person></person>
        <person></person>
        <person></person>
    </div>
</div>
Vue.component('person', {
    template: `
  <div>
    <p>Edgar Allan Poe</p>
    <p>Born: January 19, 1809</p>
    <p>Died‎: ‎October 7, 1849</p>
  </div>`
});

// Vue v2.x
const app = new Vue({
    el: '#app',
    data: {
        message: 'Hello World!'
    }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      message: 'Hello World!',
    }
  }
}).mount('#app');

Görüldüğü üzere person etiketini pek çok kez kullanabilmekteyiz. Buradaki sorun her kullanımda bize template içerisinde yer alan aynı değerleri de getiriyor oluşu. Bu gibi durumlar için sıklıkla component props kullanacağız. Ancak, öncesinde örneği biraz değiştireceğim.

<div id="app">
    <div v-cloak>
        <h1>{{ message }}</h1>
        <author-detail v-for="(author, index) in authors" :key="index" :author="author"></author-detail>
    </div>
</div>

Öncelikle HTML alanımızı oluşturalım. author-detail olarak bir bileşen oluşturmak istiyorum. Bu bileşenin içerisinde birden fazla yazar bilgisi mevcut olacak. Dolayısıyla bileşenin bu yazarları tanımladığım bilgilerle birlikte listelemesini istiyorum.

Bu işlem için oluşturacağım template ‘i instance içeriğinde değil, harici bir script tanımı içerisinde oluşturmak istiyorum.

<script type="text/x-template" id="authorIndex">
    <div>
    <p>Name: {{author.name}}</p>
    <p>Born: {{author.born}}</p>
    <p>Died: {{author.died}}</p>
    <hr />
  </div>
</script>

Esasında template item’i içerisinde tanımlamaktan hiçbir farkı yok. Sadece alternatif bir kullanım örneği olmasını istedim. Kritik nokta template item’ına bu script’in id değerini vermemizin gerektiği. Yazar bilgilerimiz ile birlikte instance şu şekilde olacaktır.

Vue.component('author-detail', {
    props: ['author'],
    template: '#authorIndex',
});

// Vue v2.x
const app = new Vue({
    el: '#app',
    data: {
        message: 'Favorite Authors',
        authors: [{
                name: 'Edgar Allan Poe',
                born: 'January 19, 1809',
                died: 'October 7, 1849'
            },
            {
                name: 'Franz Kafka',
                born: 'July 3, 1883',
                died: 'June 3, 1924'
            },
            {
                name: 'Emil M. Cioran',
                born: 'April 8, 1911',
                died: 'June 20, 1995'
            }
        ]
    }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
        message: 'Favorite Authors',
        authors: [{
                name: 'Edgar Allan Poe',
                born: 'January 19, 1809',
                died: 'October 7, 1849'
            },
            {
                name: 'Franz Kafka',
                born: 'July 3, 1883',
                died: 'June 3, 1924'
            },
            {
                name: 'Emil M. Cioran',
                born: 'April 8, 1911',
                died: 'June 20, 1995'
            }
        ]
    }
  }
})

Diğer bileşen tanımlamamızı da hatırlatmakta fayda var. Aynı bileşen tanımını şu şekilde de yapabilirdik:

data: {
    // ... /
},
components: {
    'author-detail': {
        props: ['author'],
        template: '#authorIndex',
    }
}
// ya da
data() {
    return {
        components: {
            'author-detail': {
                props: ['author'],
                template: '#authorIndex',
            }
        }
    }
}

Evet, örnekte yer alan props ile v-for direktifi kullanarak authors içeriğindeki yazarları sırasıyla listeledik. Props ile ilgili detaylardan ve bileşenlerin nasıl birbirleriyle haberleşebileceğinden ayrı bir yazıda bahsedeceğim.

Şimdilik her şey yolunda görünüyor. Lakin, eklemek istediğim bir konu var. Instance içerisinde hep data object olarak tanımlı. Fakat, global component registration aşamasında component kendi data yapısına sahip olduğunda data’nın bir function olması gerekiyor. Bunun nedeni, object olması durumunda root Vue instance ile ilişkili kalacağı ve kendi içeriğini yansıtamayacağı. Bu durumda, function tanımı yapmadığınızda hata ile durumdan haberdar edilirsiniz. Elbette bunu kasıtlı olarak istemiyorsak -ki çoğu zaman istemeyeceğiz- tanımlamamızı şu şekilde yapmamız gerekir:

Vue.component('name1', {
    data: function() {
        return {
            /.../
        }
    },
    template: '...'
})

Son bir örnek yapalım.

<div id="app">
    <div v-cloak>
        <h1>{{ message }}</h1>
        <button-counter></button-counter>
        <button-counter></button-counter>
        <button-counter></button-counter>
        <button-counter></button-counter>
    </div>
</div>
Vue.component('button-counter', {
    data: function() {
        return {
            count: 0
        }
    },
    methods: {
        addCount() {
            return this.count++
        }
    },
    template: '<button v-on:click="addCount()">You clicked me {{ count }} times.</button>'
})

// Vue v2.x
const app = new Vue({
    el: '#app',
    data: {
        message: 'Favorite Authors',
    }
});

// Vue v3.x
const app = Vue.createApp({
  data() {
    return {
      message: 'Favorite Authors',
    }
  }
})

Son örnekle bileşen (component) başlangıç yazısını tamamlamış olduk. Bir sonraki yazıda props ile ilgili daha detaylı bilgi aktarmaya çalışacağım.