Vue.js Liste İşlemleri

Vue.js içerisinde, v-for ve direktifi yazısı başta olmak üzere listeleme neredeyse tüm ilgili diğer yazılarda da kendine yer bulan bir konu.

AA

Vue.js Liste İşlemleri başlıklı bu yazıda ise konuyu biraz daha detaylandırmak istiyorum.

List Rendering

v-for direktifini kullanarak bir dizi ve/veya nesne içeriğini kolaylıkla listeleyebilmekteyiz1. Elbette bu direktif için özel bir sözdizimi kullanılmakta; item in items. Bu arada, item of items da geçerli bir yazımdır. Bir örnekle konuyu hatırlayalım.

<div id="app">
 <div v-cloak>
  <strong v-text="message"></strong>
  <ul id="list-1">
   <li v-for="(country, index) in countries" :class="(index < 5) ? 'bold' : 'passive'" :key="index">
    <span v-if="index < 5">{{ status }}: </span>{{ country.name }}
   </li>
  </ul>
 </div>
</div>
// Vue v2.x
var app = new Vue({
 el: '#app',
 data: {
 // http://worldpopulationreview.com/countries/most-dangerous-countries/
  message: 'The top 10 most dangerous countries according to the 2018 GPI report are:',
  status: 'Current',
  countries: [
   { name: 'Syria' },
   { name: 'Afghanistan' },
   { name: 'South Sudan' },
   { name: 'Iraq' },
   { name: 'Somalia' },
   { name: 'Yemen' },
   { name: 'Libya' },
   { name: 'Democratic Republic of Congo' },
   { name: 'Central African Republic' },
   { name: 'Russia' }]
 }
});

// Vue v3.x
var app = Vue.createApp({
  data() {
    return {
      message: 'The top 10 most dangerous countries according to the 2018 GPI report are:',
      status: 'Current',
      countries: [
      { name: 'Syria' },
      { name: 'Afghanistan' },
      { name: 'South Sudan' },
      { name: 'Iraq' },
      { name: 'Somalia' },
      { name: 'Yemen' },
      { name: 'Libya' },
      { name: 'Democratic Republic of Congo' },
      { name: 'Central African Republic' },
      { name: 'Russia' }]
    }
  }
}).mount('#app');

Yukarıdaki örnekte countries içeriğini country‘e aktarmakta ve liste içerisinde country.name ile bloklar içerisinde listelememizi yapmaktayız. Ek olarak, span etiketleri içerisinde de görülebildiği üzere status içeriğini parent scope dahilinde olsa da v-for blokları içerisinde kullanabilmekteyiz. Son olarak, v-for içeriğinde tanımlı index adında ikinci bir argümanımız (optional second argument) daha var. Sıralama genelde value, key, index şeklindedir; (value, key, index) in object.

Index argümanı v-for blokları oluşturuldukta kendini 1 artırmaktadır. v-for tanımı içeriğindeki key item.key, value ise bu key’in aldığı value değeridir. Ancak, :key="index" şeklinde bir tanım daha görmektesiniz. v-bind:key="item" şeklinde de ifade edilebilecek olan bu key tanımı sıralama gibi (mutation methods) işlemlerde node identity olarak ve benzersiz bir değer (number ya da string) alacak şekilde kullanabilmekteyiz2.

<div id="app">
 <div v-cloak>
  <strong v-text="message"></strong>
  <ul id="list-1">
   <li v-for="(country, index) of countries" :class="(index < 5) ? 'bold' : 'passive'" :key=" index">
    {{ country.name }}
    <ul>
     <li v-for="value, key of country">
      {{ key }}: {{ value }}
     </li>
    </ul>
   </li>
  </ul>
 </div>
</div>
// Vue v2.x
var app = new Vue({
 el: '#app',
 data: {
  message: 'The top 10 most dangerous countries according to the 2018 GPI report are:',
  status: 'Current',
  countries: [
   {
    name: 'Syria',
    capital: 'Damascus',
    religion: 'Sunni Muslim 74%, Alawite, Druze, and other Muslim sects 16%, Christian (various sects) 10%, Jewish (tiny communities in Damascus, Al Qamishli, and Aleppo)'
   },
   {
    name: 'Afghanistan',
    capital: 'Kabul',
    religion: 'Sunni Muslim 80%, Shi\'a Muslim 19%, other 1%'
   },
   {
    name: 'South Sudan',
    capital: 'Juba',
    religion: 'Roman Catholic Christianity 37.2%, Episcopal Churches and Other Forms of Christianity 36.5%, Traditional African Beliefs and Animism 19.7%, Islam 6.2%, other 0.4%'
   },
   {
    name: 'Iraq',
    capital: 'Baghdad',
    religion: 'Muslim 97% (Shi\'a 60-65%, Sunni 32-37%), Christian or other 3%'
   },
   {
    name: 'Somalia',
    capital: 'Mogadishu',
    religion: 'Sunni Muslim'
   },
   {
    name: 'Yemen',
    capital: 'Sanaa',
    religion: 'Muslim including Shaf\'i (Sunni) and Zaydi (Shi\'a), small numbers of Jewish, Christian, and Hindu'
   },
   {
    name: 'Libya',
    capital: 'Tripoli',
    religion: 'Sunni Muslim 97%, other 3%'
   },
   {
    name: 'Democratic Republic of Congo',
    capital: 'Kinshasa',
    religion: 'Roman Catholic 50%, Protestant 20%, Kimbanguist 10%, Muslim 10%, other syncretic sects and indigenous beliefs 10%'
   },
   {
    name: 'Central African Republic',
    capital: 'Bangui',
    religion: 'Indigenous beliefs 35%, Protestant 25%, Roman Catholic 25%, Muslim 15%'
   },
   {
    name: 'Russia',
    capital: 'Moscow',
    religion: 'Russian Orthodox 15-20%, Muslim 10-15%, other Christian 2% (2006 est.)'
   }
  ]
 }
});

// Vue v3.x
var app = Vue.createApp({
  data() {
    return {
      message: 'The top 10 most dangerous countries according to the 2018 GPI report are:',
      status: 'Current',
      countries: [
      {
        name: 'Syria',
        capital: 'Damascus',
        religion: 'Sunni Muslim 74%, Alawite, Druze, and other Muslim sects 16%, Christian (various sects) 10%, Jewish (tiny communities in Damascus, Al Qamishli, and Aleppo)'
      },
      {
        name: 'Afghanistan',
        capital: 'Kabul',
        religion: 'Sunni Muslim 80%, Shi\'a Muslim 19%, other 1%'
      },
      {
        name: 'South Sudan',
        capital: 'Juba',
        religion: 'Roman Catholic Christianity 37.2%, Episcopal Churches and Other Forms of Christianity 36.5%, Traditional African Beliefs and Animism 19.7%, Islam 6.2%, other 0.4%'
      },
      {
        name: 'Iraq',
        capital: 'Baghdad',
        religion: 'Muslim 97% (Shi\'a 60-65%, Sunni 32-37%), Christian or other 3%'
      },
      {
        name: 'Somalia',
        capital: 'Mogadishu',
        religion: 'Sunni Muslim'
      },
      {
        name: 'Yemen',
        capital: 'Sanaa',
        religion: 'Muslim including Shaf\'i (Sunni) and Zaydi (Shi\'a), small numbers of Jewish, Christian, and Hindu'
      },
      {
        name: 'Libya',
        capital: 'Tripoli',
        religion: 'Sunni Muslim 97%, other 3%'
      },
      {
        name: 'Democratic Republic of Congo',
        capital: 'Kinshasa',
        religion: 'Roman Catholic 50%, Protestant 20%, Kimbanguist 10%, Muslim 10%, other syncretic sects and indigenous beliefs 10%'
      },
      {
        name: 'Central African Republic',
        capital: 'Bangui',
        religion: 'Indigenous beliefs 35%, Protestant 25%, Roman Catholic 25%, Muslim 15%'
      },
      {
        name: 'Russia',
        capital: 'Moscow',
        religion: 'Russian Orthodox 15-20%, Muslim 10-15%, other Christian 2% (2006 est.)'
      }
      ]
    }
  }
}).mount('#app');

Ek bir örnek bir işlem için Console üzerinden şu satırı işleme alabilirsiniz. push() kullanımına Array (Dizi) İşlemleri başlığında değindim.

app.countries.push({ name: 'Sudan', capital: 'Khartoum', religion: 'Muslim 95.3%, Christian 3.2%, other 1.5%' })

Array (dizi) örneğinin yanı sıra, object (nesne) içeriğini basit bir liste olarak yeniden value, key ve index ile görüntüleyelim:

// Vue v2.x
new Vue({
  el: '#app',
  data: {
    object: {
      firstName: 'John',
      lastName: 'Doe',
      age: 30
    }
  }
});

// Vue v3.x
Vue.createApp({
  data() {
    return {
      object: {
        firstName: 'John',
        lastName: 'Doe',
        age: 30
      }
    }
  }
}).mount('#app');
<div v-for="(value, key, index) of object">
  {{ index }}. {{ key }}: {{ value }}
</div>

Array (Dizi) İşlemleri

Vue kolaylıkla bir dizi içeriğindeki güncellemeleri takip edebilir ve dizi içerisinde işlemler gerçekleştirebilir. Örnek olarak hazırladığım to-do uygulamasında bu metotlardan sort() ve push()‘a yer vermiştim. Bu başlık altında ilgili metotlara sırasıyla göz atalım.

push()

push() metot dizi sonuna yeni bir öge (item) eklememizi sağlar ve toplam öge sayısını (length) döndürür. Countries örneğinde push() ile eklediğimiz yeni değer liste sonuna eklenmiş ve 11 değeri döndürülmüştü.

app.countries.push({ name: 'Sudan', capital: 'Khartoum', religion: 'Muslim 95.3%, Christian 3.2%, other 1.5%' })

pop()

Dizi içeriğindeki son ögeyi diziden ayırır ve döndürür.

app.countries.pop()

Yukarıdaki komutu countries’e uyguladığımızda son değer listeden silinecek ve içeriği console üzerinden bize aktarılacaktır.

shift()

shift() dizi içerisindeki ilk ögeyi siler ve tıpkı pop() ile olduğu gibi içeriğini döndürür.

unshift()

push() metotundan farklı olarak ekleme işlemini dizinin ilk satırına yapar ve yine eklemeyi de dahil ederek dizi içerisindeki öge sayısını döndürür.

splice()

Dizinin içeriğini değiştirir ve diziye öge eklemek veya kaldırmak için kullanılabilir.

app.countries.splice(2)

Yukarıdaki komutu uyguladığımızda ilk 2 öge dışındaki tüm ögeler length değeri ve içerikleriyle birlikte return edeceklerdir.

app.countries.splice(-2)

Negatif değer kullanımında ise sondan itibaren işlem gerçekleştirilecek ve son 2 eleman dizi içerisinden silinerek return edecektir.

app.countries.splice(2,4)

Bu kullanımda ise 2. ögeden sonra gelen 4 öge array içeriğinden silinecek ve return olarak iletilecektir.

sort()

Dizi içeriğini sıralamak için -ön tanımlı olarak alfabetik- sort() metotu kullanılmaktadır. Numeric işlemlerde ise işler biraz değişebilir. Eğer, sayılar string olarak sıralanacak olurlarsa, örneğin 25 sayısı 100’den büyük olacaktır. Çünkü, 2 büyüktür 1. Bu gibi durumlarda, hatalı sonuçlar üretmemek adına karşılaştırma fonksiyonunu kullanabiliriz. Hemen bir örnekle açıklayalım.

Şeyle bir sayı dizisine sahip olalım: numbersNew: [1, 25, 33, 51, 98, 120, 240]. Bir computed prop ile dizi içeriğini sıralayarak listeleme için kullanabiliriz.

computed: {
 shortNumbers: function () {
  return this.object.numbers.sort()
 }
}

Ancak, bu kullanımda az önce yukarıda bahsettiğim sebebten dolayı, alacağımız sonuç şu şekilde olacaktır: 1, 120, 240, 25, 33, 51, 98. Bu sonucu çözmek için karşılaştırma yapmamız gerekir. İlgili computed prop fonksiyonumuzu yeniden ele alalım:

computed: {
 shortedNumbers: function () {
  return this.object.numbersNew.sort((a, b) => a - b);
 }
}

Bu işlemin ardından alacağımız sonuç şu hale gelmiştir: 1, 25, 33, 51, 98, 120, 240. İstediğimiz de tam olarak buydu. Ancak, “You may have an infinite update loop in a component render function.” şeklinde bir uyarı almanız muhtemel. Bunun nedeni render işleminin önceden yapılmış olması ve sıralama işleminin yineleme gerektirmesi. Bu durumda slice() ile sıralanacak ögelerin bir kopyası alınarak işlem yapabilirsiz.

return this.object.numbersNew.slice().sort((a, b) => a - b);

reverse()

reverse() dizi içeriğinin sondan başa olarak şekilde yeniden sıralanmasını sağlar. Sort ile az önce gerçekleştirdiğimiz işlemi sondan başa olacak şekilde yeniden sıralayalım.

return this.object.numbersNew.slice().sort((a, b) => a - b).reverse();

Diğer Metotlar

Az önce bahsi geçen ve mutation methods olarak ifade edilen yöntemlerin dışında da elbette kullanabileceğimiz filter(), concat() ve slice() gibi yöntemler (non-mutating methods) mevcut. Diğer metotlardan farkları her zaman yeni bir dizi döndürmeleridir.

filter()

filter() metotu ile dizi içerisinde filtreleme işlemleri yapılabilir. Örneğin, numbers içeriğindeki sayıları yeni bir dizi olarak ele alalım.

filteredNumber: function () {
 return this.object.numbersNew.filter(function (number) {
  return number % 2 === 0
 })
}

Computed property olarak uyguladığımız yukarıdaki fonksiyon bize şu sayıları döndürecektir: 98, 120, 240

concat()

Elinizde birden fazla dizi varsa ve siz sadece tek bir diziye ihtiyaç duymakta iseniz, ne yaparsınız? İşte bu sorunun cevabı olarak concat() metotundan faydalanabilir ve bu dizileri tek bir dizi haline getirebiliriz. Aşağıdaki gibi 2 dizimiz olsun.

numbersNew: [1, 25, 33, 51, 98, 120, 240]
numbersOld: [1, 25, 33, 51, 98, 120, 240]

Computed olarak concat() metotu aracılığıyla bu dizileri birleştirelim.

concatNumbers: function () {
 return this.object.numbersNew.concat(this.object.numbersOld)
}

Bu işlemin sonucunda concatNumbers içeriği şöyle olacaktır: 1, 25, 33, 51, 98, 120, 240, 22, 15, 88, 21, 108, 220, 140. numbersOld yazım gereği olarak numbersNew içeriğine dahil edilmiştir.

slice()

slice() metotu, kaynak diziden kopyalanan bir dilimle birlikte yeni bir dizi döndürür. Şimdi örneğimizi biraz farklı bir şekilde ele alalım. concat() metotunda 2 diziyi concatNumbers altında birleştirmiş ve yeni bir dizi elde etmiştik. Bu diziye computed prop altından erişip slice() metotunu uygulamaya ne dersiniz?

sliceNumbers: function () {
 return this.concatNumbers.slice(2, 6)
}

Evet, yukarıdaki işlemle birlikte, 10 değerli bir dizimiz olan concatNumbers içeriğinde yer alan 3, 4, 5 ve 6. değerlerin yer aldığı yeni bir dizi oluşturmuş olduk. Tek bir değer ile, verilen değer sonrasındaki ögeleri içeren bir dizi de yaratabilirdik ya da negatif değerler ile sondan başlayarak da işlemler yapabiliriz; bu gibi işlemlere splice() metotu altında yer vermiştik.

Dikkat Edilmesi Gerekenler

JavaScript’teki sınırlamalar nedeniyle, Vue.js bir diziye index ile doğrudan bir öge atandığında (set) ve/veya bir dizinin uzunluğunu değiştirildiğinde dizideki değişiklikleri algılayamayabilir. Reaktif (reactive) olmayan bu durumlara örnek olarak, sayılarla işlem yaptığımız, computed olmayan herhangi bir dizi için (örneğin shortNumbersNew) şu komutu console aracılığıyla uygulayabiliriz:

app.shortNumbersNew[app.shortNumbersNew.length] = 10000

İlgili satırı uyguladığımızda app.shortNumbersNew dizisine 10000 rakamı da dahil edilecek ve dizideki öge sayısı (length) değişecektir. Yine console üzerinden app.sliceNumbers ile dizi içerisindeki ögeleri listeleyebilir ve öge sayısını alabiliriz.

Az önceki ekleme işleminin ardından öge sayımız 8’e ulaşmış olmalı. app.shortNumbersNew.length ile son durumu edinebiliriz. Aşağıdaki komutu uyguladığımızda öge sayımız 20 olacaktır.

app.shortNumbersNew.length = 20

Yukarıdaki işlemin ardından 20 - app.shortNumbersNew.length kadar boş (empty) öge dizi içerisine eklenecektir. Ancak, yine bu durumdan Vue haberdar olmayacaktır. Peki, bu 2 duruma karşı çözüm mevcut değil mi? Olmaz olur mu! Bahsi geçen ilk duruma karşı global olarak Vue.set() fonksiyonunu hem diziler hem de nesneler için kullanabiliriz. Unutmanda, bir alias olarak app.$set() de aynı işi görecektir.

Not: Vue.js 3 ile birlikte this.myArray[index] = newValue, this.myObject[key] = value ve delete this.myObject[key] şeklinde de işlemler yapılabilmekte.

Vue.set(vm.items, indexOfItem, newValue)

Yukarıdaki örneğimizi tekrarlayalım ve app.shortNumbersNew dizisine 1000 değerini ekleyelim.

Vue.set(app.shortNumbersNew, app.shortNumbersNew.length, 1000)

Bu işlemi splice() metotu ile de gerçekleştirebiliriz.

app.shortNumbersNew.splice(app.shortNumbersNew.length, 1, 1000)

Nesnelerde ayrıca Object.assign() ve _.extend() de kullanabilmekteyiz. Ancak, bu işlem de reactive olmayacaktır.

Object.assign(app.object, {
  numbersNewOld: [2,33,44,55,66,77,88]
})

Gerçekleştirmek istediğimiz işlemi reaktif olarak şu şekilde yineleyebiliriz:

app.object = Object.assign({}, app.object, {
  numbersNewOld: [2,33,44,55,66,77,88]
})

Sonuç Olarak

Örnek uygulamalar içerisinde bu yazıya sıklıkla referansta bulunacağım. Ancak, farklı örnekler ve satır arası notlar için elbette Vue.js Guide içeriğine göz atabilirsiniz3. Ayrıca, aşağıda oldukça faydasını gördüğüm bazı yazıları listeledim, umarım sizin de işinize yararlar.