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

Angular Built-in Directive'leri: *ngIf, *ngFor, ngSwitch, ngClass ve Kendi Directive'in

Mert Özen 31 Eki 2026 7 dk 1 görüntülenme
Angular Built-in Directive'leri: *ngIf, *ngFor, ngSwitch, ngClass ve Kendi Directive'in

Angular 12'nin hazır directive'leri: *ngIf ve else, *ngFor ve trackBy, ngSwitch, ngClass, ngStyle, ng-container, yıldızın arkasındaki ng-template ve HostListener ile kendi attribute directive'in.

Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.

Directive: Template'e Davranış Ekleyen Etiket

Düz HTML'de bir elemanı koşula göre gizlemek ya da bir listeyi çoğaltmak için JavaScript yazıp DOM'u elle değiştirmen gerekir. Angular'da bu işleri template'in içinde, elemana bir attribute ekleyerek yaparsın. Bu attribute'ların her birinin arkasında bir directive durur.

Directive, bir elemana davranış kazandıran class'tır. Aslında daha önce gördüğümüz component de bir directive; sadece kendi template'i olan bir directive. Onun dışında iki tür daha var. Structural directive'ler DOM'un yapısını değiştirir: eleman ekler, kaldırır, çoğaltır. Attribute directive'ler ise var olan bir elemanın görünüşünü ya da davranışını değiştirir.

Evdeki elektrik tesisatı gibi düşün. Structural directive bir sigorta gibidir; odaya elektrik gelip gelmeyeceğine o karar verir. Attribute directive ise dimmer; lamba zaten yanıyor, sadece ışığın tonunu ayarlar.

Bu yazıdaki hazır directive'lerin hepsi CommonModule içinde. AppModule'de onu BrowserModule üzerinden zaten almış oluyoruz. Örnek olarak küçük bir görev listesi kullanacağız:

export interface Task {
  id: number;
  title: string;
  status: 'todo' | 'doing' | 'done';
  priority: 'low' | 'high';
}

export class TaskListComponent {
  isLoading = false;
  tasks: Task[] = [
    { id: 1, title: 'Tasarımı gözden geçir', status: 'done', priority: 'low' },
    { id: 2, title: 'Login sayfasını yaz', status: 'doing', priority: 'high' },
    { id: 3, title: 'Testleri çalıştır', status: 'todo', priority: 'low' }
  ];
}

*ngIf: Koşula Göre Göster ya da Kaldır

<p *ngIf="tasks.length === 0">Henüz görev yok.</p>

Koşul true ise paragraf DOM'a eklenir, false ise hiç oluşturulmaz. Burada ince ama önemli bir nokta var: *ngIf elemanı CSS ile gizlemez, DOM'dan tamamen kaldırır. Eleman bir component ise o component yok edilir, koşul tekrar true olduğunda sıfırdan oluşturulur.

Koşul tutmadığında başka bir şey göstermek istersen else kullanırsın:

<div *ngIf="!isLoading; else loading">
  {{ tasks.length }} görev var.
</div>

<ng-template #loading>
  <p>Görevler yükleniyor...</p>
</ng-template>

else'ten sonra gelen loading, daha önce gördüğümüz template reference variable. <ng-template> ise kendi başına ekrana hiçbir şey basmayan bir şablon; içindeki HTML ancak biri onu çağırdığında çizilir. isLoading true olduğunda Angular div'i kaldırır ve bu şablonu onun yerine koyar.

Yıldızın Arkasında Ne Var?

*ngIf'teki yıldız bir kısaltma. Angular derleme sırasında yukarıdaki div'i şuna çevirir:

<ng-template [ngIf]="!isLoading" [ngIfElse]="loading">
  <div>{{ tasks.length }} görev var.</div>
</ng-template>

Yani elemanın kendisi bir ng-template içine sarılır ve ngIf directive'i normal bir property binding ile bu şablona bağlanır. Directive, koşula bakıp şablonu DOM'a basıp basmayacağına karar verir. Yıldızlı bütün structural directive'ler aynı şekilde çalışır. Bunu bilmek, birazdan göreceğimiz ng-container kuralını anlamayı da kolaylaştırıyor.

*ngFor: Listeyi Tek Satırla Çizmek

<ul>
  <li *ngFor="let task of tasks; let i = index">
    {{ i + 1 }}. {{ task.title }}
  </li>
</ul>

let task of tasks, dizideki her eleman için li'yi bir kez çizer ve o anki elemanı task adıyla template'e verir. Kalıp kesmek gibi: kalıbı bir kez hazırlarsın, hamur ne kadarsa o kadar kurabiye çıkar. Dizi değiştiğinde liste de kendiliğinden güncellenir.

index, ngFor'un sağladığı yerel değişkenlerden biri. Diğerleri de en az onun kadar işe yarar:

<li *ngFor="let task of tasks; let first = first; let last = last; let even = even"
    [class.first]="first" [class.last]="last" [class.even]="even">
  {{ task.title }}
</li>

first ve last ilk ve son elemanda true olur; listenin başına ya da sonuna özel bir stil vermek için birebir. even ve odd ise çift ve tek sıraları ayırır, zebra desenli tablolar için klasik yöntem. Bu değişkenleri index as i şeklinde de yazabilirsin; ikisi de aynı işi görür.

trackBy ile Gereksiz Çizimden Kurtulmak

Görev listesini sunucudan yeniden çektiğini düşün. İçerik aynı olsa bile gelen nesneler yepyeni nesnelerdir. ngFor varsayılan olarak elemanları nesne referansıyla takip ettiği için "bunların hepsi yeni" der ve bütün li'leri silip baştan çizer. Üç görevde fark etmezsin, üç bin görevde fark edersin.

trackBy ile Angular'a elemanları neye göre tanıyacağını söylersin:

trackById(index: number, task: Task): number {
  return task.id;
}
<li *ngFor="let task of tasks; trackBy: trackById">{{ task.title }}</li>

Artık Angular aynı id'ye sahip görevi tanır ve onun DOM elemanını yerinde bırakır. Sadece gerçekten eklenen, silinen ya da yer değiştiren satırlara dokunur. Gerçek veriyle çalışan listelerde trackBy'ı alışkanlık hâline getirmeye değer.

ngSwitch: Birden Fazla Durum

Bir değerin üç dört farklı hâli varsa iç içe *ngIf yazmak hızla dağınık bir görüntü yaratır. Liste içindeki her görevin durumunu göstermek için şöyle yazarız:

<span [ngSwitch]="task.status">
  <span *ngSwitchCase="'todo'">Bekliyor</span>
  <span *ngSwitchCase="'doing'">Devam ediyor</span>
  <span *ngSwitchCase="'done'">Tamamlandı</span>
  <span *ngSwitchDefault>Bilinmiyor</span>
</span>

Yapı TypeScript'teki switch ile aynı. Dikkat edilecek iki şey var. [ngSwitch] yıldızsız yazılır, çünkü kendisi bir attribute directive; sadece hangi değere bakılacağını söyler. Asıl işi yıldızlı *ngSwitchCase'ler yapar. Bir de 'todo' etrafındaki tek tırnaklar şart; tırnak olmazsa Angular todo adında bir property arar.

ng-container: Görünmez Sarmalayıcı

Diyelim ki sadece tamamlanmamış görevleri listelemek istiyorsun. İlk akla gelen, iki yıldızı aynı elemana yazmak:

<li *ngFor="let task of tasks" *ngIf="task.status !== 'done'">...</li>

Angular buna izin vermez:

error NG5002: Can't have multiple template bindings on one element. Use only one attribute prefixed with *

Yıldızın arkasındaki ng-template'i hatırla: her yıldız elemanı ayrı bir şablona sarmak ister ve bir eleman iki şablonun içinde birden olamaz. Çözüm, döngüyü bir dış elemana taşımak. Ama oraya fazladan bir div koyarsan ul'un içinde geçersiz bir HTML ve bozulan bir CSS ile karşılaşırsın. İşte ng-container tam bunun için var:

<ul>
  <ng-container *ngFor="let task of tasks">
    <li *ngIf="task.status !== 'done'">{{ task.title }}</li>
  </ng-container>
</ul>

ng-container DOM'a hiçbir eleman basmaz. Tarayıcının geliştirici araçlarında bakarsan ul'un içinde sadece li'leri görürsün. Bir taşıma kolisi gibi düşün; eşyaları taşır, ama eve yerleştirdiğinde ortada koli kalmaz.

ngClass ve ngStyle

Daha önce gördüğümüz [class.x] tek bir class için idealdi. Birden fazla class'ı koşullara göre yönetirken ngClass daha derli toplu durur:

<li *ngFor="let task of tasks"
    [ngClass]="{ 'done': task.status === 'done', 'urgent': task.priority === 'high' }"
    [ngStyle]="{ 'opacity': task.status === 'done' ? 0.5 : 1 }">
  {{ task.title }}
</li>

ngClass'a verdiğin nesnede her anahtar bir class adı, her değer de o class'ın eklenip eklenmeyeceğini belirleyen koşul. String ya da dizi de verebilirsin, ama koşullu kullanımda nesne yazımı en okunur olanı. ngStyle aynı mantıkla stil özelliklerini ayarlar.

Pratik bir tavsiye: ngStyle'a uzun stil listeleri yazmak yerine bir CSS class'ı tanımla ve onu ngClass ile aç kapat. Stiller CSS dosyasında, karar template'te kalır.

Kendi Attribute Directive'imiz

Hazır directive'ler yetmediğinde kendininkini yazarsın. Üzerine gelinen satırı vurgulayan bir directive üretelim:

ng g directive highlight

CLI directive dosyasını ve test dosyasını oluşturur, directive'i de AppModule'ün declarations listesine ekler. Dosyayı şöyle doldur:

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {

  constructor(private el: ElementRef<HTMLElement>) { }

  @HostListener('mouseenter') onMouseEnter(): void {
    this.highlight('lightyellow');
  }

  @HostListener('mouseleave') onMouseLeave(): void {
    this.highlight('');
  }

  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

Selector köşeli parantez içinde: [appHighlight]. Bu, CSS'teki attribute seçicisiyle aynı mantık; directive, bu attribute'u taşıyan her elemana uygulanır.

Constructor'daki ElementRef, directive'in uygulandığı elemanın kendisine erişim verir. Onu constructor parametresi olarak yazmamız yeterli, Angular bize kendisi veriyor. Bu mekanizmanın adı dependency injection ve ilerleyen yazılarda detaylandıracağız.

@HostListener('mouseenter') ise directive'in bağlı olduğu elemandaki event'i dinler. Fare elemanın üzerine geldiğinde arka plan sarıya döner, ayrıldığında eski hâline gelir. Kullanımı tek kelime:

<li *ngFor="let task of tasks" appHighlight>{{ task.title }}</li>

Küçük Bir Deneme

Görev listesine bir "Tamamlananları gizle" butonu ekle. Butona tıklandığında bir boolean değişsin ve ng-container ile *ngIf kullanarak tamamlanan görevler listeden kaybolsun. Hiç görev kalmazsa else ile "Her şey bitti" mesajı gösterilsin.

Ardından ngSwitch ile her görevin yanına durumuna göre bir etiket koy ve ngClass ile yüksek öncelikli görevleri kalın yazdır. Son olarak appHighlight directive'ini liste satırlarına uygula ve tarayıcının geliştirici araçlarında ng-container'ın DOM'da gerçekten iz bırakmadığını kontrol et.

Directive'imiz şu an hep aynı sarıyı kullanıyor. Rengi dışarıdan, kullanıldığı yerden vermek için bir input'a ihtiyacımız var. Bir sonraki konumuz tam olarak bu: component'ler arasında input ve output ile veri taşımak.