Skip to content

Bölüm 3 / 13

Template sözdizimi

Binding türleri, @if/@for, pipe.

12 dakikalık okuma

Şablon, bileşen durumunun ekrana izdüşümüdür — ve Angular şablonu, HTML'in üstüne kurulmuş küçük ama disiplinli bir dildir. Bu ders o dilin tamamını verir: dört bağlama türü, @if/@for kontrol akışı, pipe'lar ve güvenlik. 8. hatta textContent/innerHTML ayrımıyla ve listeyi elle çizerek kurduğun kaslar burada çerçeve sözdizimine dönüşüyor.

01Ne öğreneceksin

  • Dört bağlama türü: {{ }}, [özellik], (olay), [( )] — ve hangisi ne zaman
  • Özellik / öznitelik ayrımı: [attr.] ne zaman zorunlu
  • Interpolation'ın otomatik kaçışı; [innerHTML] tehlikesi ve sanitizer
  • @if/@else, @for + zorunlu track, @empty, @switch
  • Eski *ngIf/*ngFor yazımını okuyabilmek
  • Pipe'lar: yerleşikler, kendi pipe'ını yazmak, saf/saf olmayan ayrımı
  • Şablon referans değişkeni # ve ng-container

02Interpolation ve güvenlik: {{ }}

<h3>{{ basvuru().ad }}</h3>
<p>Puan: {{ puan() * 2 }} · {{ acikMi() ? "Açık" : "Kapalı" }}</p>

Çift süslü parantez, içindeki ifadeyi değerlendirir ve sonucu metin olarak yerleştirir. İfade dili bilinçli olarak kısıtlıdır: alan okuma, metot çağrısı, aritmetik, üçlü operatör olur; atama (=), new, ++ gibi yan etkili işlemler yasaktır. Bu kısıt felsefenin özetidir: şablon durumu gösterir, değiştirmez — durum değişikliği sınıf metotlarının işidir.

Güvenlik — 8. hattın kancası ödeniyor: 16. derste XSS'e karşı kuralın "kullanıcı verisi textContent'e, asla innerHTML'e değil" idi. Interpolation tam olarak textContent gibi davranır: değerin içinde <script> olsa bile metin olarak görünür, çalışmaz. Angular bunu otomatik yapar — doğru yol, varsayılan yoldur. HTML basmak istediğinde ise:

<div [innerHTML]="aciklamaHtml()"></div>
<!-- Angular değeri sanitizer'dan geçirir: script, olay öznitelikleri ayıklanır.
     Yine de kural: kullanıcıdan gelen ham HTML'i basmak İSTİSNADIR, gerekçe ister. -->

Sanitizer'ı bypassSecurityTrustHtml ile devre dışı bırakan API'ler vardır; adındaki "trust" ciddiyetindedir — yalnız kaynağını senin ürettiğin içerikte, kod incelemesinde savunabileceğin gerekçeyle kullanılır. Mülakat sorusu klasiği: "Angular XSS'e karşı ne yapar?" — cevap bu bölümdür.

03Özellik bağlama: [hedef]="ifade"

<img [src]="foto()" [alt]="basvuru().ad">
<button [disabled]="kaydediliyor()">Kaydet</button>
<app-basvuru-karti [basvuru]="secili()" />   <!-- 2. dersin input'u da budur -->

Köşeli parantez, DOM özelliğine (property) ifade sonucunu atar — 8. hatta el.disabled = true yazmanın bildirimsel hâli. Parantezsiz yazım ise düz HTML özniteliğidir ve metin atar; klasik tuzak şudur:

<button disabled="{{ kaydediliyor() }}">   <!-- YANLIŞ: "false" METNİ de truthy —
                                                buton hep pasif kalır -->
<button [disabled]="kaydediliyor()">       <!-- doğru: boolean özelliğe boolean değer -->

8. hattın 15. dersindeki özellik/öznitelik ayrımı burada sınava girer: bazı şeylerin DOM özelliği yoktur — colspan, aria-*, data-*. Onlara [attr.] önekiyle bağlanılır:

<td [attr.colspan]="sutunSayisi()">…</td>
<button [attr.aria-expanded]="acik()">…</button>

Sınıf ve stil için özel kısayollar vardır — class özniteliğini komple ezmeden tek parçayı yönetirler:

<article [class.vurgulu]="seciliMi()">      <!-- sınıfı koşulla ekle/çıkar -->
<div [style.width.px]="genislik()">         <!-- birimli stil bağlama -->

04Olay bağlama ve iki yön

<button (click)="sil(basvuru().id)">Sil</button>
<input (input)="ara($event)">
<form (submit)="gonder(); $event.preventDefault()">…</form>

Normal parantez olay dinler — 8. hattın addEventListener'ının şablon hâli; üstelik temizliği düşünmezsin, bileşen yok edilince dinleyiciyi Angular söker (14. dersteki sızıntı derdinin çerçevece çözümü). $event, DOM olay nesnesidir (bileşen output'unda ise emit edilen değer — 2. ders). Tip güvenliği için yaygın kalıp, olayı bir metoda verip orada daraltmaktır:

ara(e: Event) {
  const deger = (e.target as HTMLInputElement).value;  // 9. hattın bilinçli "as"i
}

İki yönlü bağlama [( )] ("muz kalıbı" — banana in a box), özellik bağlama + olay bağlamanın kısayoludur; en çok form kontrollerinde ngModel ile görülür:

<input [(ngModel)]="aramaMetni" />
<!-- eşdeğeri: [ngModel]="aramaMetni" (ngModelChange)="aramaMetni = $event" -->

Sözdizimi sihir değil, iki bağlamanın birleşimidir — bunu bilmek, kendi bileşenine iki yönlü bağlama eklerken (model() input'u, 9. derste) ve mülakatta işine yarar. Form yoğun işlerde Angular'ın asıl aracı Reactive Forms'tur (8. ders); ngModel basit, yerel durumlar içindir.

YazımYönNe zaman
{{ ifade }}Sınıf → metinİçerik gösterme
[ozellik]="ifade"Sınıf → DOM/bileşenÖzellik, input, class/style
(olay)="metot()"DOM/bileşen → sınıfKullanıcı etkileşimi, output
[(x)]="alan"Çift yön (kısayol)Form kontrolü + yerel durum
[attr.x]="ifade"Sınıf → öznitelikDOM özelliği olmayanlar: colspan, aria-*

05Kontrol akışı: @if ve @switch

@if (yukleniyor()) {
  <p>Yükleniyor…</p>
} @else if (hata()) {
  <p class="hata">{{ hata() }}</p>
} @else {
  <app-basvuru-listesi [basvurular]="basvurular()" />
}

Bu üçlü — yükleniyor / hata / veri — 8. hattın 17. dersinde elle kurduğun durumların şablon karşılığıdır ve gerçek uygulamaların en sık kalıbıdır. Önemli ayrıntı: @if bloğu sahte (falsy) olunca içerik gizlenmez, DOM'dan tamamen kaldırılır — içindeki bileşenler ngOnDestroy yaşar, tekrar doğrunca sıfırdan kurulur. "Sadece görünmez olsun, durumu kaybolmasın" istiyorsan CSS ile gizlersin ([class.gizli]); bu bilinçli bir karardır, refleks değil.

Daraltmayla da dosttur: @if (secili(); as b) { {{ b.ad }} } — koşulun sonucunu yerel değişkene bağlar; | null tiplerle çalışırken (5. derste bolca) bu yazım standarttır. Çok dallı ayrımlar için @switch vardır ve 9. hattın ayırt edici birleşimleriyle güzel oturur:

@switch (basvuru().durum) {
  @case ("beklemede")  { <span class="rozet">Beklemede</span> }
  @case ("onaylandi")  { <span class="rozet ok">Onaylandı</span> }
  @case ("reddedildi") { <span class="rozet ret">Reddedildi</span> }
}

06Liste çizimi: @for ve track zorunluluğu

@for (b of basvurular(); track b.id) {
  <app-basvuru-karti [basvuru]="b" (secildi)="detayAc($event)" />
} @empty {
  <p>Kayıt yok.</p>
}

8. hattın 15. dersinde listeyi silip yeniden çizmenin bedelini konuşmuştuk: odak kaybolur, seçim gider, DOM boşa yıkılıp kurulur; çözüm "her satıra kimlik ver, yalnız değişeni güncelle" idi. track tam olarak o kimliktir ve Angular'da zorunludur — çerçeve, dizinin yeni hâliyle eskisini bu anahtar üzerinden eşleştirir ve yalnız gerçekten değişen satırların DOM'una dokunur.

Yanlış track ölçülebilir zarardır: track $index yazarsan (ya da eski dünyada trackBy hiç vermezsen) diziyi filtrelediğinde veya sıraladığında kimlikler kayar: Angular satırları eşleştiremez, kartları yıkıp yeniden kurar — ngOnDestroy/ngOnInit logları yağar, satır içindeki input odaklarını ve yerel durumları kaybeder. Doğrusu her zaman kalıcı, benzersiz kimliktir: track b.id. $index yalnız gerçekten kimliksiz, yeniden sıralanmayan statik listelerde kabul edilir.

Blok içinde hazır değişkenler vardır: $index, $first, $last, $even, $count — @for (b of liste; track b.id; let i = $index).

Eski yazım — sahada okuyacaksın: yıllarca kontrol akışı yapısal yönergelerle (structural directive) yazıldı; * öneki "bu elemanı şablona çevir" demekti:

Modern blokEski yönerge (okumayı bil)Not
@if (x) { } @else { }*ngIf="x; else bosSablon" + <ng-template #bosSablon>else için ayrı şablon gerekirdi
@for (b of liste; track b.id)*ngFor="let b of liste; trackBy: izle"izle sınıfta fonksiyondu; çoğu kodda unutulmuştur — gördüğünde performans şüphesi
@switch[ngSwitch] + *ngSwitchCase
—CommonModule import'uYeni bloklar import istemez; eski yönergeler isterdi

07Pipe: görüntüleme dönüşümü

Pipe, şablonda değeri gösterim için dönüştürür — veriyi değiştirmez. Sözdizimi Linux borusundan tanıdık (2. hat):

{{ basvuru().tarih | date:"dd.MM.yyyy" }}
{{ ucret() | currency:"TRY":"symbol":"1.0-0" }}   <!-- ₺12.500 -->
{{ ad() | uppercase }} · {{ oran() | percent }}
{{ nesne() | json }}                               <!-- hata ayıklamada birebir -->

Yerleşiklerin en önemlileri: date, currency, number, percent, uppercase/lowercase, json, slice — ve bu hattın yıldızı olacak async (5. ders). Yerelleştirme uyarısı: date ve currency çıktısı uygulamanın LOCALE_ID'sine bağlıdır; Türkçe biçim istiyorsan tr yerel verisini kaydetmen gerekir — "tarih neden İngilizce?" sorusunun cevabı.

Kendi pipe'ın küçük bir sınıftır:

import { Pipe, PipeTransform } from "@angular/core";

@Pipe({ name: "durumAdi" })
export class DurumAdiPipe implements PipeTransform {
  transform(durum: Basvuru["durum"]): string {      // 9. hat: indeksli erişim tipi
    switch (durum) {
      case "beklemede":  return "Beklemede";
      case "onaylandi":  return "Onaylandı";
      case "reddedildi": return "Reddedildi";
    }
  }
}
// Şablon: {{ b.durum | durumAdi }}  (bileşenin imports'una eklenir)

Saflık tuzağı: pipe'lar varsayılan olarak saftır (pure): Angular, pipe'ı yalnız girdi referansı değişince yeniden çalıştırır. Bir diziyi push ile mutasyona uğratırsan referans aynı kalır — sıralayan/filtreleyen pipe güncellenmez. Çözüm pipe'ı impure yapmak değil (her denetimde çalışır, performans düşmanı), veriyi değişmez tutmaktır: [...dizi, yeni] — 8. ve 9. hatta kurduğun değişmezlik alışkanlığının çerçevedeki karşılığı. Aynı kural OnPush ve signals dünyasının da temeli; bu cümleyi 9. derste yeniden göreceksin.

08Şablon araçları: #ref, ng-container, @let

<input #arama (input)="filtrele(arama.value)">
<!-- #arama = bu input elemanının kendisi; sınıfa taşımadan şablon içinde kullan -->

Şablon referans değişkeni (#ad), elemanı şablon içinde adlandırır — küçük etkileşimlerde sınıfa alan açmaktan kurtarır. Bir bileşen etiketine konursa bileşen örneğini verir.

<ng-container>
  <!-- DOM'a hiçbir eleman basmayan sarmalayıcı -->
</ng-container>

ng-container, DOM'da iz bırakmayan gruplama elemanıdır. Modern bloklarda ihtiyaç azaldı (blok zaten elemansız), ama eski kodda "iki yapısal yönerge aynı elemana konamaz" kuralını aşmanın standart yolu olarak her yerdedir; tabloların içinde geçersiz HTML üretmeden satır grubu çizmek gibi işlerde bugün de kullanılır (8. hattın "tablo içine div koyma" uyarısını hatırla).

@let toplam = basvurular().length;
@let bekleyen = basvurular().filter(b => b.durum === "beklemede").length;
<p>{{ bekleyen }} / {{ toplam }} başvuru bekliyor</p>

@let, şablon içi yerel değişken tanımlar — aynı ifadeyi üç yerde tekrar etmekten (ve üç kez hesaplatmaktan) kurtarır. Yine de sınır koy: şablonda üç satırı geçen mantık, sınıfta (yakında: computed signal'de) daha doğru yaşar. Şablon aptal kalmalı — test edilebilirlik ve okunabilirlik oradadır.

09Mini alıştırma — Başvuru listesi ekranı

Yaklaşık 20 dakika · kurulum yok — angular.dev/playground ya da StackBlitz; 2. dersteki kart bileşeninin üstüne kur

  1. Kök bileşende 5-6 elemanlı basvurular dizisi tut (id, ad, durum, tarih alanlı). @for (b of basvurular; track b.id) ile kartları çiz; @empty bloğu ekle ve diziyi boşaltıp mesajı gör.
  2. @switch ile durum rozetini üç dalda çiz; tarihi | date:"dd.MM.yyyy" ile biçimle.
  3. Track deneyi: her karta ngOnInit/ngOnDestroy logu koy. Bir "Sırala" butonu diziyi tersine çevirsin ([...dizi].reverse()). Önce track b.id ile tıkla — konsol sessiz; sonra track $index yap — yıkım/kurulum loglarının yağdığını gör. Farkı bir cümleyle not et: bu, mülakatta "trackBy ne işe yarar" sorusunun canlı cevabıdır.
  4. Öznitelik tuzağı: bir tabloda [attr.colspan]="n" ile [colspan]="n"'i sırayla dene; hangisi neden çalışmıyor?
  5. Güvenlik deneyi: bir başvurunun adını <img src=x onerror=alert(1)> yap. {{ b.ad }} ile bas — zararsız metin. Sonra [innerHTML]="b.ad" ile bas — Angular'ın sanitizer'ı ne yaptı, konsolda uyarı var mı? 8. hattın 16. dersiyle bağını kur.
  6. Bir arama kutusu ekle: #arama referansı + (input)="filtrele(arama.value)"; filtreyi yeni dizi üreterek uygula (mutasyon değil) ve @let ile "N sonuç" satırı göster.

10Kendini sına

Angular'daki dört bağlama türünü, yönlerini ve ne zaman kullanılacaklarını anlat.

Interpolation ({{ ifade }}) sınıftan şablona metin taşır: ifade değerlendirilir, sonuç metin olarak basılır — içerik göstermenin yoludur. Özellik bağlama ([hedef]="ifade") sınıftan DOM özelliğine ya da alt bileşenin input'una değer taşır; boolean ve nesne değerlerinde tek doğru yoldur, çünkü öznitelik yazımı (disabled="{{ x }}") her şeyi metne çevirir ve "false" metni de truthy olduğundan buton hep pasif kalır gibi tuzaklar doğurur. Olay bağlama ((olay)="metot()") ters yöndür: DOM olayını ya da alt bileşenin output'unu sınıf metoduna bağlar; $event olay nesnesini (output'ta emit edilen değeri) taşır ve dinleyici temizliğini çerçeve üstlenir. İki yönlü bağlama ([(x)]="alan") ise ilk ikisinin kısayoludur — [x] + (xChange) birleşimi; en çok ngModel ile form kontrollerinde görülür. Ek olarak DOM özelliği olmayan şeylere (colspan, aria-*) [attr.] önekiyle, tek sınıf/stile [class.x]/[style.x] kısayollarıyla bağlanılır.

@for'da track neden zorunludur? Yanlış track neye mal olur, doğrusu nedir?

track, listenin her satırına kalıcı bir kimlik verir; dizi değiştiğinde Angular eski ve yeni elemanları bu anahtarla eşleştirir ve yalnız gerçekten değişen satırların DOM'unu günceller — diğer satırlar, içlerindeki bileşen örnekleri, odak ve yerel durumlarıyla yerinde kalır. Yanlış anahtar — tipik olarak track $index — sıralama, filtreleme veya araya ekleme yapıldığında kimlikleri kaydırır: Angular satırları eşleştiremez, bileşenleri yıkıp yeniden kurar. Bunun ölçülebilir belirtileri vardır: ngOnDestroy/ngOnInit logları yağar, satır içi input odağı kaybolur, satırın yerel durumu sıfırlanır ve büyük listelerde gecikme hissedilir. Doğru anahtar, elemanın kalıcı ve benzersiz kimliğidir — veritabanı id'si gibi; $index yalnız kimliksiz ve yeniden sıralanmayan statik listelerde savunulabilir. Eski kodda karşılığı *ngFor'un trackBy fonksiyonudur ve isteğe bağlı olduğu için çoğu kodda unutulmuştur; modern @for'un track'i zorunlu kılması bu saha yarasının dersidir.

Angular XSS'e karşı ne yapar? [innerHTML] ve bypassSecurityTrustHtml nerede devreye girer?

Varsayılan yol güvenlidir: interpolation değeri her zaman metin olarak basar — textContent davranışı — içindeki etiketler çalışmaz, görüntülenir; kullanıcı verisi için doğru yol budur ve otomatiktir. Bilerek HTML basmak istendiğinde [innerHTML] bağlaması kullanılır; Angular bu değeri DOM'a vermeden önce sanitizer'dan geçirir: <script> etiketleri, onerror gibi olay öznitelikleri, javascript: URL'leri ayıklanır. Yani [innerHTML] bile ham yapıştırma değildir — ama yine de kullanıcı kaynaklı HTML basmak istisna olmalı ve gerekçe istemelidir. Sanitizer'ı aşmak için DomSanitizer.bypassSecurityTrustHtml gibi API'ler vardır; bunlar "bu değere ben kefilim" beyanıdır ve yalnız kaynağı tamamen senin denetiminde olan içerik için, kod incelemesinde savunulabilecek gerekçeyle kullanılır — kullanıcı girdisine uygulanırsa XSS kapısını kendi elinle açmış olursun. Özet cümle: çerçevenin güvenliği varsayılanların güvenliğidir; tehlike, varsayılanı bilinçli olarak devre dışı bıraktığın yerde başlar (13. hatta OWASP ile derinleşecek).

Pipe nedir? Saf pipe'ın mutasyonla imtihanını ve çözümünü anlat.

Pipe, şablonda bir değeri gösterim amacıyla dönüştüren araçtır — {{ tarih | date:"dd.MM.yyyy" }} gibi; veri değişmez, yalnız ekrandaki biçimi değişir. Yerleşikler (date, currency, number, json, async) çoğu ihtiyacı karşılar; kendi pipe'ın PipeTransform uygulayan küçük bir sınıftır ve alan adı eşlemesi gibi projeye özgü dönüşümleri şablondan okunur kılar. Kritik ayrıntı saflıktır: pipe'lar varsayılan olarak saftır — Angular, dönüşümü yalnız girdi referansı değiştiğinde yeniden çalıştırır. Bir diziye push yapıldığında referans aynı kaldığından, o diziyi filtreleyen/sıralayan saf pipe güncellenmez; ekran eski kalır ve hata "bazen çalışıyor" görünümlü, sinsi olur. Yanlış çözüm pipe'ı impure işaretlemektir — her değişiklik denetiminde çalışır ve performansı yer. Doğru çözüm veriyi değişmez tutmaktır: değişiklikte yeni dizi/nesne üretilir ([...dizi, yeni]), referans değişir, saf pipe da OnPush ve signals dünyası da bunu doğal algılar. Değişmezlik burada üslup değil, çerçevenin değişiklik algısıyla yapılan sözleşmedir.

11Sınav köşesi

Yazılıda nasıl sorulur

(1) Angular'daki binding türlerini yazıp birer örnek veriniz. (2) Property binding ile attribute binding farkı nedir; [attr.] ne zaman kullanılır? (3) *ngIf/@if elemanı gizler mi, kaldırır mı; [hidden]'dan farkı? (4) *ngFor'da trackBy/track ne işe yarar? (5) Pipe nedir; pure/impure farkı? (6) async pipe ne yapar? (7) Angular XSS'i nasıl önler; DomSanitizer nedir? (8) İki yönlü bağlama nasıl çalışır; hangi iki bağlamanın birleşimidir? (9) ng-container ile ng-template ne işe yarar? (10) Şablon referans değişkeni (#ref) nedir?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Çerçeve — "Şablon dili dört bağlamadan oluşur: interpolation metin basar, [ ] özelliğe değer taşır, ( ) olay dinler, [( )] ikisinin kısayoludur; DOM özelliği olmayanlara attr. ile bağlanırım." (2) Kontrol akışı — "@if/@for/@switch bloklarını kullanırım; @for'da track zorunludur ve kalıcı id veririm — index ile track, sıralamada DOM'u yıktırır. Eski *ngIf/*ngFor'u okurum." (3) Güvenlik — "Interpolation otomatik kaçar; innerHTML sanitizer'dan geçer; bypass API'lerini yalnız kendi ürettiğim içerikte savunurum." (4) Pipe — "Gösterim dönüşümü şablonda pipe'la; saf pipe referans değişimi ister, bu yüzden veriyi değişmez tutarım." (5) Kıdem vuruşu — "Şablonu aptal tutarım: üç satırı geçen mantık sınıfa/computed'a iner; boolean'a öznitelikle bağlamam, 'false' metni truthy'dir."