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

Angular 17 Yenilikleri: @if, @for, @defer ve Yeni Application Builder

Mert Özen 31 Eki 2026 7 dk 1 görüntülenme
Angular 17 Yenilikleri: @if, @for, @defer ve Yeni Application Builder

Angular 17 ile gelen @if, @for ve @switch control flow'unu, @defer ve trigger'larını, esbuild ile Vite tabanlı application builder'ı ve standalone ng new'i Angular 12 karşılaştırmasıyla anlatıyoruz.

Sürüm notu: Bu yazı Angular 17 ile gelen yenilikleri anlatır; kodlar Angular 17 içindir.

Template'in Yüzü Değişiyor

8 Kasım 2023'te çıkan Angular 17, bir Angular template'ine ilk kez bakan birinin gözüne çarpacak en büyük değişikliği getirdi. Yıllardır *ngIf, *ngFor ve [ngSwitch] ile yazdığımız akış kontrolü artık template'in kendi sözdizimi olarak geliyor: @if, @for, @switch. Yanına da bir component'i ancak gerektiğinde yükleyen @defer bloğu ekleniyor.

Template dışında da iki önemli değişiklik var. Yeni projeler artık esbuild ve Vite tabanlı application builder ile geliyor ve ng new varsayılan olarak NgModule'süz, standalone bir proje oluşturuyor. Signal tabanlı input ve output'lar ise ayrı bir konu; onlara bir sonraki yazıda bakacağız.

*ngIf'ten @if'e

Daha önce gördüğümüz built-in directive'lerde koşullu gösterimi böyle yazıyorduk:

<div *ngIf="user; else guest">
  <p>Hoş geldin, {{ user.name }}</p>
</div>
<ng-template #guest>
  <p>Giriş yapmadın.</p>
</ng-template>

Else dalı için ayrı bir ng-template ve ona bir isim vermek gerekiyordu. Üçüncü bir durum eklemek istediğinde iş iç içe *ngIf'lere dönüyordu. Angular 17'de aynı şey:

@if (user) {
  <p>Hoş geldin, {{ user.name }}</p>
} @else if (isLoading) {
  <p>Yükleniyor...</p>
} @else {
  <p>Giriş yapmadın.</p>
}

Okuması tıpkı TypeScript'teki bir if gibi. @else if doğrudan destekleniyor, sarmalayıcı bir div ya da ng-container açmana gerek yok. Bir detay daha: bu bloklar directive değil, template derleyicisinin parçası. Standalone bir component'te NgIf ya da CommonModule import etmeden kullanabiliyorsun.

Observable ile çalışırken alıştığımız as kalıbı da duruyor: @if (user$ | async; as user) { ... } yazdığında bloğun içinde user değişkenini kullanabiliyorsun.

*ngFor ve trackBy'dan @for ve track'e

Liste gösterirken Angular 12'de performans için trackBy eklemeyi unutmamak gerekiyordu:

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

Çoğu projede bu fonksiyon hiç yazılmazdı, kimse de fark etmezdi. Ta ki liste büyüyüp her yenilemede bütün DOM baştan çizilene kadar. Angular 17'de:

<ul>
  @for (task of tasks; track task.id) {
    <li>
      {{ $index + 1 }}. {{ task.title }}
      @if ($last) { <span>(son)</span> }
    </li>
  } @empty {
    <li>Henüz görev yok.</li>
  }
</ul>

track artık zorunlu. Yazmazsan template derlenmiyor. Ayrı bir fonksiyon da gerekmiyor, doğrudan hangi alanın kimlik olduğunu söylüyorsun. Bir otel resepsiyonu düşün: misafirleri oda numarasıyla takip ediyorsan biri çıkınca sadece o odayı boşaltırsın. Kimin kim olduğunu bilmiyorsan, en güvenli yol herkesi çıkarıp yeniden yerleştirmektir. track o oda numarası. Kimliği olmayan basit bir string listesinde track $index da yazabilirsin.

$index, $first, $last, $even, $odd ve $count blok içinde hazır geliyor; let i = index gibi takma ad vermek zorunda değilsin. Liste boşsa @empty bloğu çalışıyor. Angular 12'de bunun için listenin yanına ayrıca bir *ngIf="tasks.length === 0" yazıyorduk.

@switch ve Mevcut Template'leri Taşımak

Duruma göre farklı içerik göstermek için Angular 12'deki kalıp:

<div [ngSwitch]="task.status">
  <span *ngSwitchCase="'done'">Tamamlandı</span>
  <span *ngSwitchCase="'in-progress'">Devam ediyor</span>
  <span *ngSwitchDefault>Bekliyor</span>
</div>

Angular 17'de:

@switch (task.status) {
  @case ('done') { <span>Tamamlandı</span> }
  @case ('in-progress') { <span>Devam ediyor</span> }
  @default { <span>Bekliyor</span> }
}

Switch'i taşımak için bir sarmalayıcı element gerekmiyor ve JavaScript'teki gibi bir case'den diğerine düşme yok; her @case kendi bloğunu çalıştırıp bitiyor.

Elindeki onlarca template'i tek tek elle çevirmek zorunda değilsin. Angular bunun için bir şematik veriyor:

ng generate @angular/core:control-flow

Komut projedeki *ngIf, *ngFor ve ngSwitch kullanımlarını yeni bloklara çeviriyor. Çıkan farkı mutlaka gözden geçir, özellikle trackBy olmayan listelerde neyin track olarak seçildiğine bak. Durum notu: Angular 17'de yeni control flow developer preview. Eski directive'ler çalışmaya devam ediyor, ikisi aynı projede, hatta aynı template'te yan yana yaşayabiliyor.

@defer: Görünümü Gerektiğinde Yüklemek

Angular 12'de lazy loading neredeyse sadece route seviyesindeydi. Bir sayfanın içindeki ağır bir grafik component'ini sonradan yüklemek istersen işi elle yapıyordun:

async showChart(): Promise<void> {
  const { SalesChartComponent } = await import('./sales-chart/sales-chart.component');
  const factory = this.resolver.resolveComponentFactory(SalesChartComponent);
  this.container.createComponent(factory);
}

Dinamik import, factory, container referansı ve component'in ihtiyaç duyduğu module'lerin hesabı. Çoğu ekip bu zahmete girmez, grafiği ana bundle'a koyup geçerdi. Angular 17'de aynı iş template'te çözülüyor:

@defer (on viewport) {
  <app-sales-chart [data]="sales" />
} @placeholder {
  <div class="chart-box">Grafik burada görünecek</div>
} @loading (minimum 500ms) {
  <p>Grafik yükleniyor...</p>
} @error {
  <p>Grafik yüklenemedi.</p>
}

Derleyici @defer bloğunun içindeki component'i ayrı bir JavaScript dosyasına ayırıyor. Sayfa açıldığında kullanıcı @placeholder içeriğini görüyor. O alan ekrana girdiği anda dosya indiriliyor, inerken @loading gösteriliyor, bir sorun çıkarsa @error devreye giriyor. minimum 500ms, yükleme mesajının ekranda bir an yanıp sönmesini engelliyor.

İki şartı var. Ertelenen component standalone olmalı ve aynı dosyada başka bir yerden, örneğin bir @ViewChild ile referans alınmamalı. Aksi hâlde derleyici onu ayrı dosyaya ayıramıyor ve component normal şekilde ana bundle'a giriyor. Restoranda tatlıyı baştan masaya koymamak gibi: istenince mutfaktan geliyor, ama tatlıyı ana yemeğin tabağına koymuşsan ayrıca getirmenin anlamı kalmıyor.

Trigger'lar: Ne Zaman Yüklensin?

@defer'in parantez içindeki kısmı, yüklemenin ne zaman başlayacağını söylüyor. Bir şey yazmazsan varsayılan on idle, yani tarayıcı boşa çıktığında. Diğer seçenekler:

@defer (on interaction; prefetch on idle) {
  <app-comment-list [postId]="postId" />
} @placeholder {
  <button>Yorumları göster</button>
}

@defer (when showDetails) {
  <app-product-details [product]="product" />
}

@defer (on timer(2s)) {
  <app-newsletter-box />
}

on interaction, kullanıcı placeholder'a tıkladığında ya da klavyeyle onunla etkileşime geçtiğinde yüklüyor. prefetch on idle ise dosyayı önceden, tarayıcı boştayken indirip hazır tutuyor; tıklandığında gösterim anında oluyor. on hover fare üzerine geldiğinde, on immediate sayfa çizilir çizilmez, on timer verilen süre sonunda tetikliyor.

when ise bir koşula bakıyor. Dikkat: bir kez doğru olup yüklendikten sonra koşul tekrar yanlış olsa da içerik geri kaldırılmıyor. Göster-gizle davranışı istiyorsan içeride ayrıca bir @if kullanmalısın. on viewport, on interaction ve on hover bir template referansı verilmezse placeholder'ı izliyor; bu yüzden placeholder'ın tek bir kök elementi olmalı. @defer da bu sürümde developer preview.

Yeni Application Builder: esbuild ve Vite

Angular 12 projesinde angular.json'daki build hedefi webpack tabanlı builder'ı gösteriyordu: @angular-devkit/build-angular:browser. Angular 17 ile oluşturulan yeni bir projede ise şöyle:

"build": {
  "builder": "@angular-devkit/build-angular:application",
  "options": {
    "outputPath": "dist/task-board",
    "index": "src/index.html",
    "browser": "src/main.ts",
    "polyfills": ["zone.js"],
    "tsConfig": "tsconfig.app.json"
  }
}

Arkada build için esbuild, ng serve için Vite'ın geliştirme sunucusu çalışıyor. Bir önceki sürümde önizleme olarak denediğimiz hız artık varsayılan. Giriş dosyası seçeneğinin adının main yerine browser olduğuna dikkat et. Çıktı da artık doğrudan dist/task-board içine değil, dist/task-board/browser klasörüne yazılıyor. Daha önce gördüğümüz deploy adımlarında sunucuya kopyaladığın klasör yolunu buna göre güncellemen gerekiyor.

Mevcut projelerde builder kendiliğinden değişmiyor; webpack tabanlı builder çalışmaya devam ediyor. Geçmek istersen builder satırını ve birkaç seçeneği elle güncelliyorsun.

ng new Artık Standalone Başlıyor

Angular 12'de ng new ilk iş bir app.module.ts oluştururdu. Angular 17'de aynı komut, daha önce gördüğümüz standalone yapıyı varsayılan olarak kuruyor ve routing de baştan açık geliyor:

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};
// src/app/app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'task-board';
}

Uygulama genelindeki provider'lar artık app.config.ts'te, route'lar app.routes.ts'te duruyor. Küçük ama hoş bir detay: tek stil dosyası için dizi yerine styleUrl yazabiliyorsun. Kurulumda bir de server-side rendering isteyip istemediğin soruluyor; bunu --ssr bayrağıyla da belirtebilirsin. NgModule tabanlı bir projeye ihtiyacın varsa ng new task-board --no-standalone hâlâ çalışıyor.

Angular 17'ye Geçiş: ng update

16'daki bir projeyi bir üst sürüme taşımak için:

ng update @angular/core@17 @angular/cli@17

Angular 17, Node.js ^18.13.0 ya da ^20.9.0 istiyor; Node 16 desteği bitti. TypeScript en az 5.2 olmalı; 17.3 itibarıyla 5.4'e kadar destekleniyor. zone.js de ~0.14.0'a yükseliyor.

Bir önceki sürümde developer preview olarak gördüğümüz signal'larda da durum değişti: signal ve computed Angular 17'de stable oldu, effect ise hâlâ developer preview. Signal'ın mutate() metodu kaldırıldı; 16'da kullandıysan yerine update() yazman gerekiyor. Yeni control flow'a geçmek ise güncellemenin parçası değil, onu istediğin zaman şematikle yapabilirsin.

Küçük Bir Deneme

İçinde *ngIf, trackBy'lı bir *ngFor ve [ngSwitch] bulunan küçük bir görev listesi component'i yaz. Önce bir commit at, sonra ng generate @angular/core:control-flow çalıştır ve oluşan farkı satır satır incele. Ardından listenin altına ağır olduğunu varsaydığın ayrı bir standalone component koy, onu @defer (on viewport) ile sar ve sayfanın altında bir placeholder bırak. Tarayıcının Network sekmesini açıp sayfayı aşağı kaydırdığında yeni bir JavaScript dosyasının indiğini gör.

Template'ler yenilendi ama component'lerin input'ları, output'ları ve @ViewChild'ları hâlâ decorator'larla yazılıyor. Bir sonraki yazıda Angular 17'nin ara sürümleriyle gelen signal tabanlı input(), model(), output() ve signal query'lere bakacağız.