Mert Özen Her Satırda Daha İleri
Frontend Angular Material ve CDK — #06

CDK Drag and Drop: cdkDrag, cdkDropList, Sıralanabilir Liste ve Listeler Arası Taşıma

Mert Özen 15 Kas 2026 14 dk 6 görüntülenme
CDK Drag and Drop: cdkDrag, cdkDropList, Sıralanabilir Liste ve Listeler Arası Taşıma

CDK Drag and Drop'ta cdkDropList ve cdkDrag sürüklemeyi yönetir, array'i sen değiştirirsin: moveItemInArray ve transferArrayItem'ı kopyaya uygulayıp signal'e set et, satırları kimlikle track et. CDK klavyeyle sıralama sunmaz; Bookstore'da okuma listesi ve kurumsal sevkiyat planına buton alternatifi ekliyor, taşınan satırda kaybolan focus'u, tutamak ve touch-action ile dokunmatik davranışı ve 500 satırda yeniden render maliyetini ölçüyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22.2 (TypeScript 6.0), Angular Material ve CDK (Component Dev Kit; Angular ekibinin stilsiz davranış library'si) 22.2.7, .NET 10 ve Node.js 24 ile denenmiştir; tarayıcı davranışı Playwright 1.65 ile headless Chromium'da gözlenmiştir. Bookstore.Api ölçümlerde SQL Server yerine SQLite ile çalıştı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) iki ekran kuruyoruz. Müşteri tarafında "My reading list": okumak istediği kitapları sürükleyerek sıraya koyuyor. Yönetim panelinde kurumsal bir siparişin sevkiyat planı: okul kütüphanesinin siparişindeki kalemleri "This shipment" ve "Next shipment" listeleri arasında taşıyıp kısmi sevkiyatı planlıyoruz. İkisini de CDK Drag and Drop ile yapıyoruz. Üç soru: CDK Drag and Drop klavyeyle sıralamayı kendisi destekliyor mu? Bir satırı aşağı taşıyan butona basınca satır DOM'da (Document Object Model; sayfanın tarayıcıdaki element ağacı) yer değiştirir; focus o butonda kalır mı? Telefonda kitabın üstünde parmağını kaydıran kullanıcı sayfayı mı kaydırır, kitabı mı sürükler?

cdkDropList ve cdkDrag ile Sıralanabilir Liste

Bir listeyi sıralanabilir yapmak için iki directive yeterli: container'a cdkDropList, her satıra cdkDrag. CDK sürüklemeyi, diğer satırların kaymasını ve bırakılan yeri hesaplıyor; veriyi değiştirmiyor. Bırakınca cdkDropListDropped event'i eski ve yeni index'i veriyor, array'i biz değiştiriyoruz:

<ol class="reading-list" cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="dropped($event)">
  @for (book of books(); track book.isbn; let i = $index, first = $first, last = $last) {
    <li class="row" cdkDrag cdkDragBoundary=".reading-list">
      <span class="handle" cdkDragHandle aria-hidden="true">⠿</span>
      <span class="title">
        <a [routerLink]="['/books', book.isbn]">{{ book.title }}</a>
        <span class="muted">{{ book.author }}</span>
      </span>
      <button #move type="button" class="secondary" [attr.data-isbn]="book.isbn" data-dir="up" [disabled]="first"
              [attr.aria-label]="'Move ' + book.title + ' up'" (click)="moveBy(i, -1, 'up')">↑</button>
      <button #move type="button" class="secondary" [attr.data-isbn]="book.isbn" data-dir="down" [disabled]="last"
              [attr.aria-label]="'Move ' + book.title + ' down'" (click)="moveBy(i, 1, 'down')">↓</button>
      <div class="placeholder" *cdkDragPlaceholder></div>
    </li>
  }
</ol>
<!-- Screen reader kitabın nereye gittiğini duyar -->
<p class="visually-hidden" aria-live="polite">{{ status() }}</p>

Satırdaki diğer ayarlar: cdkDragHandle sürüklemeyi yalnız tutamaktan başlatıyor, böylece başlıktaki link'e tıklamak ya da metni seçmek sürükleme başlatmıyor. cdkDropListLockAxis="y" önizlemeyi dikey eksene kilitliyor, cdkDragBoundary listenin dışına çıkmasını engelliyor. *cdkDragPlaceholder satırın bırakılacağı yerde kesik çizgili bir kutu gösteriyor; vermezsen CDK satırın kopyasını placeholder olarak kullanıyor. Önizlemeyi değiştirmek istersen *cdkDragPreview da var; biz CDK'nın satırdan klonladığı önizlemeyi kullandık.

protected dropped(event: CdkDragDrop<BookSummary[]>): void {
  this.move(event.previousIndex, event.currentIndex);
}

private move(from: number, to: number): void {
  if (from === to) {
    return;
  }
  const previous = this.books();
  // Aynı kitap object'leriyle yeni bir array: @for (track isbn) satırları yeniden oluşturmaz, taşır
  const next = [...previous];
  moveItemInArray(next, from, to);
  this.books.set(next);
  this.status.set(`${next[to].title} moved to position ${to + 1} of ${next.length}`);
  void this.save(next, previous);
}

private async save(next: BookSummary[], previous: BookSummary[]): Promise<void> {
  try {
    await firstValueFrom(this.http.put('/api/reading-list/order', { isbns: next.map(b => b.isbn) }));
  } catch {
    // Optimistic update: API yeni sırayı kabul etmezse eski sırayı geri göster
    this.books.set(previous);
    this.status.set('The new order could not be saved');
  }
}

moveItemInArray array'i yerinde değiştiren bir yardımcı; signal'in içindeki array'i değiştirip set etmek yerine önce kopyalıyoruz, yoksa signal aynı referansı gördüğü için değişikliği bildirmez. Sıra API'ye hemen gidiyor; arayüz cevabı beklemiyor (optimistic update), API reddederse eski sıra geri geliyor. Bookstore.Api tarafında sıralamayı kaydeden endpoint yalnız aynı kitapların yeni bir sırasını kabul ediyor:

// PUT /api/reading-list/order {"isbns": [...]}: yalnız aynı kitapların yeni bir sırası kabul edilir
private static Results<NoContent, ValidationProblem> SaveOrder(ReadingListOrder body, ReadingListStore store)
{
    string[] current = store.Get(() => []);
    if (body.Isbns.Length != current.Length || body.Isbns.Distinct().Count() != body.Isbns.Length
        || !body.Isbns.All(current.Contains))
    {
        return TypedResults.ValidationProblem(new Dictionary<string, string[]>
        {
            ["isbns"] = ["The order must contain every book of the reading list exactly once."]
        });
    }
    store.Set(body.Isbns);
    return TypedResults.NoContent();
}

Okuma listesi demo amaçlı bellekte tutuluyor (müşteri hesapları Identity module'üne gelene kadar). Sırayı index'lerle değil ISBN (International Standard Book Number; kitabın uluslararası numarası) listesiyle göndermek, iki sekmede aynı liste açıkken "3'ü 1'e taşı" gibi bir komutun yanlış kitaba uygulanmasını engelliyor. Listeyi Playwright ile fareyle sürükledik:

start         : 1984 | Anna Karenina | Brave New World | Crime and Punishment | Don Quixote | Dune
while dragging: preview popover=manual :popover-open=true parent=body | placeholder true
after drag    : Anna Karenina | Brave New World | 1984 | Crime and Punishment | Don Quixote | Dune
drag on title : Anna Karenina | Brave New World | 1984 | Crime and Punishment | Don Quixote | Dune

Tutamaktan sürüklenen "1984" üçüncü sıraya geçti; başlıktan başlatılan aynı hareket hiçbir şey değiştirmedi. Sürükleme sırasında önizleme body'nin altında bir popover="manual" element'i, yani top layer'da; CDK bunu 17.3.6'da "use native popover to avoid stacking issues with preview" düzeltmesiyle getirdi. Önizleme bir dialog'un ya da overflow: hidden bir kartın içinden sürüklense de kesilmiyor.

Önizlemenin Genişliği ve box-sizing

İlk ekran görüntüsünde önizleme satırdan geniş çıktı ve sağ kenardan taştı. Ölçtük:

# .row { padding: 0.5rem 0.75rem; border: 1px } — varsayılan content-box
row 552 (box-sizing content-box) | preview 578 (inline width 552px)
# .row { box-sizing: border-box; }
row 552 (box-sizing border-box) | preview 552 (inline width 552px)

CDK önizlemeye satırın ölçülen genişliğini (padding ve border dahil 552px) inline width olarak yazıyor. Satır content-box olunca bu değer içerik genişliği sayılıyor ve padding ile border üstüne bir daha ekleniyor: 26px daha geniş bir önizleme. Sürüklenen element'e box-sizing: border-box vermek yetti. Material component'leri border-box kullanıyor, bu sorun en çok kendi stilini yazdığın listelerde çıkar.

Sürüklemeden Sıralamak: Klavye ve Tek Tıklama

Tahminin ilk sorusu: hayır. CDK Drag and Drop'un kaynak kodunda tek bir keydown handler'ı yok; sürükleme yalnız fare ve dokunmayla başlıyor. WCAG (Web Content Accessibility Guidelines; web erişilebilirlik yönergeleri) 2.2'nin 2.5.7 "Dragging Movements" kriteri AA (üç uygunluk seviyesinin ortancası) seviyesinde ve sürükleyerek yapılan her işin tek bir işaretçiyle, sürüklemeden de yapılabilmesini istiyor; klavye erişimi ayrıca 2.1.1'in konusu. Satırdaki ↑ ve ↓ butonları ikisini birden karşılıyor: fareyle ya da parmakla tıklanıyor, klavyeyle focus alıp Enter ile çalışıyor.

// Sürüklemenin klavye ve tek işaretçi alternatifi (WCAG 2.2, 2.5.7)
protected moveBy(index: number, delta: -1 | 1, direction: 'up' | 'down'): void {
  const target = index + delta;
  if (target < 0 || target >= this.books().length) {
    return;
  }
  const isbn = this.books()[index].isbn;
  this.move(index, target);
  // Angular focus'taki satırın kendisini DOM'da taşıdıysa focus <body>'ye düştü: aynı butona geri koy
  afterNextRender(() => {
    const button = this.moveButtons().find(b => b.nativeElement.dataset['isbn'] === isbn && b.nativeElement.dataset['dir'] === direction);
    (button ?? this.moveButtons().find(b => b.nativeElement.dataset['isbn'] === isbn))?.nativeElement.focus();
  }, { injector: this.injector });
}

Tahminin ikinci sorusunu afterNextRender satırı olmadan ölçtük. İki komşu satırın yer değiştirmesini Angular iki şekilde yapabilir: aşağı inen satırı taşıyarak ya da yukarı çıkanı taşıyarak. Hangisini taşıdığı focus'un kaderini belirliyor:

with focus restore  : down row 1: focus button Move 1984 down | up row 3: focus button Move Anna Karenina up
no focus restore    : down row 1: focus button Move Brave New World down | up row 3: focus body

"Aşağı"da Angular diğer satırı taşıdı ve focus yerinde kaldı; "yukarı"da focus'taki satırın kendisini taşıdı, tarayıcı da DOM'dan çıkan element'in focus'unu body'ye düşürdü. Yarı yarıya çalışan bir klavye desteği, test ederken en kolay atlanan cinsten. Render'dan sonra aynı kitabın aynı yöndeki butonunu focus'lamak ikisini de düzeltti; kitap listenin başına ya da sonuna geldiyse o buton disabled olduğu için diğer butona düşüyoruz. Hareketin sonucunu görmeyen kullanıcı için aria-live="polite" bir paragraf "Anna Karenina moved to position 2 of 6" diyor. Klavye akışının tamamı:

focus         : button Move Anna Karenina down
Enter         : Brave New World | Anna Karenina | 1984 | Crime and Punishment | Don Quixote | Dune
focus after   : button Move Anna Karenina down
live region   : Anna Karenina moved to position 2 of 6
Enter again   : Brave New World | 1984 | Anna Karenina | Crime and Punishment | Don Quixote | Dune
PUT requests  :
  /api/reading-list/order {"isbns":["9780143035008","9780060850524","9780451524935","9780143058144","9780060934347","9780441172719"]}
  /api/reading-list/order {"isbns":["9780060850524","9780143035008","9780451524935","9780143058144","9780060934347","9780441172719"]}
  /api/reading-list/order {"isbns":["9780060850524","9780451524935","9780143035008","9780143058144","9780060934347","9780441172719"]}
after reload  : Brave New World | 1984 | Anna Karenina | Crime and Punishment | Don Quixote | Dune

Sürükleme ve iki buton hareketi üç PUT yaptı ve sayfa yenilenince sıra yerindeydi. Live region'ın ve focus yönetiminin ayrıntısı Accessibility (a11y) serisinin "Dinamik İçerik ve Screen Reader Bildirimleri: Live Region'lar, ariaNotify(), Form Hataları, Toast ve Loading State'leri" ve "Keyboard Navigation ve Focus Yönetimi: Tab Sırası, :focus-visible, Skip Link, Focus Trap, inert ve Roving Tabindex" yazılarında.

Dokunmatik Ekranda

Tahminin üçüncü sorusu. Playwright'ın fare API'si dokunma üretmediği için 390px'lik mobil bir context'te CDP (Chrome DevTools Protocol; tarayıcıyı dışarıdan kontrol eden protokol) ile gerçek touch event'leri gönderdik:

start          : Anna Karenina | 1984 | Brave New World | Crime and Punishment | Don Quixote | Dune | scrollY 0
touch on handle: 1984 | Brave New World | Anna Karenina | Crime and Punishment | Don Quixote | Dune | scrollY 0
touch on text  : 1984 | Brave New World | Anna Karenina | Crime and Punishment | Don Quixote | Dune | scrollY 120

Tutamaktan başlayan hareket kitabı taşıdı ve sayfa kaymadı; satırın metninden başlayan hareket sırayı değiştirmedi, sayfayı 120px kaydırdı. Bunu sağlayan iki şey var: cdkDragHandle sürüklemeyi tutamağa sınırlıyor ve tutamakta touch-action: none tarayıcının o noktada kaydırma başlatmasını engelliyor. Satırın tamamı sürüklenebilir olsaydı uzun bir listede kullanıcı sayfayı kaydırmak isterken kitapları karıştırırdı. Tutamak koyamadığın bir tasarımda cdkDragStartDelay ile dokunmada sürüklemeyi birkaç yüz milisaniyelik basılı tutmaya bağlayabilirsin ({ touch: 300, mouse: 0 } gibi); bunu ölçmedik. Gerçek bir telefonda ve iOS Safari'de denemedik; ölçüm Chromium'un touch emülasyonunda.

/* Sürüklemeyi yalnız tutamak başlatır; touch-action satırın geri kalanında sayfayı kaydırılabilir bırakır */
.handle {
  cursor: grab;
  font-size: 1.25rem;
  color: #6b7280;
  touch-action: none;
}

/* Sürükleme sırasında diğer satırlar yana kayar */
.cdk-drop-list-dragging .row:not(.cdk-drag-placeholder) {
  transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
}

Listeler Arası Taşıma: Kurumsal Siparişte İki Sevkiyat

Kurumsal Sipariş'te kısmi sevkiyat Release 2'nin konusu: okulun siparişindeki kalemlerin bir kısmı bu hafta, kalanı stok gelince gidiyor. Yönetim panelinde #1042 numaralı siparişin kalemlerini iki liste arasında taşıyoruz. cdkDropListGroup içindeki bütün drop list'leri birbirine bağlıyor; tek tek cdkDropListConnectedTo yazmaya gerek kalmıyor:

<!-- cdkDropListGroup içindeki her drop list'i birbirine bağlar: kalemler iki kolon arasında taşınabilir -->
<div class="columns" cdkDropListGroup>
  @for (slot of slots; track slot.id) {
    <section class="column">
      <h2 [id]="slot.id + '-title'">{{ slot.title }}</h2>
      <p class="summary">{{ lists()[slot.id].length }} lines · {{ copies(slot.id) }} copies</p>
      <ul class="lines" cdkDropList [cdkDropListData]="slot.id" [attr.aria-labelledby]="slot.id + '-title'"
          (cdkDropListDropped)="dropped($event)">
        @for (line of lists()[slot.id]; track line.id) {
          <li class="line" cdkDrag>
            <span class="title">{{ line.title }}</span>
            <span class="quantity">× {{ line.quantity }}</span>
            <button matButton type="button" (click)="moveTo(line, slot.id)"
                    [attr.aria-label]="'Move ' + line.title + ' to ' + slot.other + ' shipment'">{{ slot.action }}</button>
          </li>
        } @empty {
          <li class="empty">Drop lines here</li>
        }
      </ul>
    </section>
  }
</div>
protected dropped(event: CdkDragDrop<Slot>): void {
  const from = event.previousContainer.data;
  const to = event.container.data;
  const next = { this: [...this.lists().this], next: [...this.lists().next] };
  if (from === to) {
    moveItemInArray(next[to], event.previousIndex, event.currentIndex);
  } else {
    transferArrayItem(next[from], next[to], event.previousIndex, event.currentIndex);
  }
  this.update(next);
}

// Aynı taşıma sürüklemeden: diğer listenin sonuna
protected moveTo(line: InstitutionalOrderLine, from: Slot): void {
  const to: Slot = from === 'this' ? 'next' : 'this';
  const next = { this: [...this.lists().this], next: [...this.lists().next] };
  transferArrayItem(next[from], next[to], next[from].indexOf(line), next[to].length);
  this.update(next);
}

cdkDropListData'ya array'in kendisini değil, listenin adını ('this', 'next') verdik. Event'te previousContainer.data ve container.data bu adları taşıyor; array'ler signal'in içinde kalıyor ve her değişiklik yeni bir object ile set ediliyor. Array'i data olarak verip transferArrayItem'ı doğrudan onun üstünde çalıştırmak CDK dokümanındaki örneklerin yolu, ama o zaman signal değişikliği görmüyor. Sürükleme, buton ve kaydetme:

start        : This shipment (4 lines · 115 copies): Dune, The Hobbit, Pride and Prejudice, 1984
               Next shipment (2 lines · 40 copies): Middlemarch, Frankenstein
after drag   : This shipment (3 lines · 90 copies): Dune, Pride and Prejudice, 1984
               Next shipment (3 lines · 65 copies): Middlemarch, The Hobbit, Frankenstein
after button : This shipment (2 lines · 50 copies): Dune, Pride and Prejudice
               Next shipment (4 lines · 105 copies): Middlemarch, The Hobbit, Frankenstein, 1984
save enabled : true
snackbar     : Shipment plan saved
 OK
PUT          : /api/admin/institutional-orders/1042/shipments {"thisShipment":[1,3],"nextShipment":[4,2,6,5]}

"The Hobbit" sürüklendiği yere, "Middlemarch"ın altına girdi; buton ise "1984"ü listenin sonuna koydu. Burada sıralama ve listeler arası taşımayı otomatik kaydetmedik; plan birkaç hamlede kuruluyor ve "Save plan" ile tek seferde gidiyor. İlk sevkiyatın boş kalmasına arayüz izin vermiyor (buton disabled), API de aynı kuralı ayrıca uyguluyor:

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"lines":["Every line must be in exactly one shipment and the first shipment cannot be empty."]},"traceId":"00-6e1f5b45ff6d778a1cc62aa0c89c5816-5385822aeea5159a-00"}

06-drag-drop-tr.png

Üretimde Dikkat

Sıralamayı kimin belirlediğini netleştir. Okuma listesinde her hareket hemen kaydediliyor, sevkiyat planında kullanıcı "Save plan" diyene kadar hiçbir şey gitmiyor. İkisi de doğru olabilir; yanlış olan, kullanıcının hangisinde olduğunu bilmemesi. Kaydedilmemiş plan için butonun durumu ya da sayfadan çıkarken bir uyarı bunu gösterir.

Hızlı ardışık hareketlerde PUT'ların sırası. Okuma listesi her harekette tam sırayı gönderiyor, bu yüzden en son gelen istek doğru durumu yazıyor; ama iki istek ağda yer değiştirirse sunucuda eski sıra kalabilir. Ciddi bir listede istekleri sıraya koy ya da sürüm numarasıyla gönder (If-Match); biz bunu yapmadık ve ölçmedik.

Bundle'a etkisi. Okuma listesi lazy bir route; production build'de katalogdan okuma listesine geçen kullanıcı 4 JavaScript dosyası, toplam 73.441 byte indirdi (route'un kendi chunk'ı 4.31 kB, gerisi CDK drag-drop ve ortak CDK kodu). Drag and drop'u eager bir sayfaya koymadan önce bunu ölç.

Testler. Birim testlerde sürüklemeyi taklit etmek yerine cdkDropListDropped event'ini triggerEventHandler ile tetikleyip sonucu kontrol ettik; gerçek sürükleme Playwright'ta. Buton yolu ise DOM'dan tıklanarak test ediliyor:

 ✓ src/app/reading-list/reading-list-page.spec.ts > ReadingListPage > moves a book with the arrow button, saves the new order and keeps focus on the moved book
 ✓ src/app/reading-list/reading-list-page.spec.ts > ReadingListPage > applies a drop from the drop list
 ✓ src/app/reading-list/reading-list-page.spec.ts > ReadingListPage > shows the old order again when the API refuses the new one
 ✓ src/app/admin/shipments/shipment-plan-page.spec.ts > ShipmentPlanPage > moves a line to the next shipment without dragging and saves the plan
 ✓ src/app/admin/shipments/shipment-plan-page.spec.ts > ShipmentPlanPage > moves a line between the lists on drop and does not allow an empty first shipment
      Tests  27 passed (27)

Sık Düşülen Tuzaklar

Birincisi, büyük listede her harekette bütün listeyi yeniden render etmek. Okuma listesini 500 satıra çıkarıp bir satırı ↓ ile taşıdık ve DOM'a eklenen satırları saydık. İkinci satırdaki varyant, her harekette yeni object'ler üretiyor (API'den listeyi yeniden çekmek gibi) ve @for'u object'in kendisiyle track ediyor:

500 rows, track isbn      : rows 500 | li.row added 1 | 8 ms | focus button Move 1984 #0 down
500 rows, new objects+track: rows 500 | li.row added 500 | 45 ms | focus button Move 1984 #0 down

Doğru yolda Angular tek satırı taşıdı; yanlış yolda 500 satırın hepsini silip yeniden oluşturdu ve hareket beş kattan uzun sürdü. Focus'u buton yeniden oluşturulduktan sonra afterNextRender kurtardı; o satır olmasa focus her harekette kaybolurdu. Kaydettikten sonra listeyi sunucudan yeniden çekmen gerekiyorsa track ifadesi kimlik (ISBN, id) olmalı, object referansı değil.

İkincisi, yalnız sürüklemeyle yapılabilen bir işlem bırakmak. Sürükleme klavye kullanıcısı, titreme ya da ince motor becerisi sorunu olan kullanıcı ve bazı screen reader kullanıcıları için yok. Her drop list'in yanına aynı işi yapan bir buton koy.

Üçüncüsü, satırın tamamını dokunmatikte sürüklenebilir yapmak. Tutamak ve touch-action olmadan uzun bir liste telefonda kaydırılamaz hâle gelir.

Dördüncüsü, moveItemInArray'i signal'in içindeki array'e doğrudan uygulamak. Array değişiyor ama signal'in değeri aynı referans; değişikliği görmeyen bir computed eski toplamı göstermeye devam eder.

Beşincisi, sürüklenen element'te content-box. Önizleme padding kadar geniş çıkar.

Küçük Bir Deneme

★☆☆ Okuma listesinde tutamağı kaldırıp cdkDrag'e [cdkDragStartDelay]="{ touch: 300, mouse: 0 }" ver ve touch ölçümünü tekrarla. Metinden başlayan hızlı bir kaydırma artık sayfayı mı kaydırıyor, kitabı mı taşıyor?

★★☆ Sevkiyat planına cdkDropListEnterPredicate ekle: stokta olmayan bir kalem "This shipment" listesine bırakılamasın. Aynı kuralı "Now" butonu için de uygula ve butonu neden çalışmadığını söyleyen bir metinle disabled yap.

★★★ Okuma listesindeki PUT'ları sıraya koy: bir istek bitmeden yenisi gelirse yalnız son sırayı gönder. Ağı yavaşlatıp (Playwright'ta route ile 1 saniyelik gecikme) art arda beş hareket yap ve sunucudaki sıranın ekrandakiyle aynı kaldığını göster.

Drag and drop'ta DOM'daki satır sayısı az olduğu için performans sorun olmadı; 500 satırda bile tek satır taşındı. Sonraki yazıda satır sayısının gerçekten sorun olduğu yere geçiyoruz: yönetim panelinde 5.000 kitaplık bir stok listesini CDK virtual scroll ile gösteriyor, BreakpointObserver, clipboard ve CDK'nın diğer yardımcılarına bakıyoruz.