Skip to content

Bölüm 2 / 13

Bileşenler

Template, class, yaşam döngüsü, input/output.

12 dakikalık okuma

Bileşen, Angular uygulamasının atomudur: kendi görünümü, durumu ve davranışı olan, yeniden kullanılabilir bir arayüz parçası. Bu ders bileşenin anatomisini, bileşenler arası veri akışını (input/output), yaşam döngüsünü ve stil kapsüllemesini işler. 9. hatta öğrendiğin sınıflar ve dekoratörler burada ete kemiğe bürünür — @Component artık kavram değil, her gün yazacağın kod.

01Ne öğreneceksin

  • Bileşen anatomisi: sınıf + şablon + üst veri; seçici (selector) ve bileşen ağacı
  • Tek yönlü veri akışı: aşağı veri input(), yukarı olay output()
  • Eski @Input()/@Output() yazımını okuyabilmek; iki dünyanın tablosu
  • Yaşam döngüsü: kurucu vs ngOnInit, ngOnDestroy ile temizlik
  • Stil kapsülleme: bileşenin CSS'i neden dışarı sızmaz; :host
  • ng-content ile içerik yansıtma; akıllı / sunumsal bileşen ayrımı

02Anatomi: sınıf, şablon, üst veri

import { Component } from "@angular/core";

@Component({
  selector: "app-basvuru-karti",
  template: `
    <article class="kart">
      <h3>{{ ad }}</h3>
      <p>Durum: {{ durum }}</p>
      <button (click)="incele()">İncele</button>
    </article>
  `,
  styles: `.kart { border: 1px solid #ccc; padding: 1rem; }`,
})
export class BasvuruKarti {
  ad = "A. Yılmaz";
  durum = "beklemede";
  incele() { console.log(`${this.ad} inceleniyor`); }
}

İş bölümü nettir: sınıf durumu (alanlar) ve davranışı (metotlar) tutar; şablon o durumun ekrandaki izdüşümünü tanımlar; üst veri (@Component'in nesnesi — 9. hatta gördüğün "seçenek nesnesi" kalıbının ta kendisi) ikisini bağlar. Şablon, sınıfın alanlarına ve metotlarına doğrudan erişir: {{ ad }} sınıftaki ad'ı okur, (click)="incele()" sınıftaki metodu çağırır. this yazılmaz; şablonun bağlamı zaten bileşen örneğidir.

Dosya düzeni: kısa şablonlar template ile satır içi yazılabilir; pratikte CLI her bileşene üç dosya üretir (.ts, .html, .css) ve templateUrl/styleUrl ile bağlar. İçerik aynıdır; ekip geleneğine uy.

Seçici ve ağaç: selector, bileşenin şablonlarda hangi etiketle kullanılacağını söyler. Kök bileşenden başlayarak her bileşen başka bileşenleri etiketleriyle içerir ve ortaya bir bileşen ağacı çıkar — sayfa dediğin şey bu ağacın çizilmiş hâlidir:

App
└── BasvuruListesi          <app-basvuru-listesi>
    ├── FiltreCubugu        <app-filtre-cubugu>
    ├── BasvuruKarti  (× N) <app-basvuru-karti>
    └── DetayPaneli         <app-detay-paneli>

Parçalama ölçütü tek cümledir: bir arayüz parçası kendi başına anlamlı bir işi varsa ve birden çok yerde ya da birden çok durumla kullanılacaksa bileşendir. Aşırı parçalamak da maliyettir — üç satırlık, tek yerde kullanılan bir blok için bileşen açmak dosya trafiği üretir.

03Veri aşağı: input()

Yukarıdaki kartta ad sabitti; gerçekte kartı liste çizer ve her karta farklı başvuru verir. Bileşenin dışarıdan parametre alması input() ile olur:

import { Component, input } from "@angular/core";

export interface Basvuru {          // 9. hattın arayüzleri iş başında
  id: number;
  ad: string;
  durum: "beklemede" | "onaylandi" | "reddedildi";
}

@Component({
  selector: "app-basvuru-karti",
  template: `
    <article class="kart">
      <h3>{{ basvuru().ad }}</h3>
      <p>Durum: {{ basvuru().durum }}</p>
    </article>
  `,
})
export class BasvuruKarti {
  basvuru = input.required<Basvuru>();   // dışarıdan gelmek ZORUNDA
  vurgulu  = input(false);                // isteğe bağlı, varsayılanı false
}

Kullanan taraf değeri köşeli parantezle bağlar (sözdiziminin tamamı 3. derste):

<app-basvuru-karti [basvuru]="secili" [vurgulu]="true" />

Üç ayrıntı önemli:

  • input() bir signal döndürür — değeri okumak için basvuru() diye çağırırsın. Signal mekanizmasının derinliği 9. dersin konusu; şimdilik "çağrılarak okunan, değişimi Angular'ın izlediği kutu" yeter.
  • input.required derleme güvencesidir: kullanan şablon [basvuru] vermezse derleme hatası alırsın. 9. hattın "hatayı derleme anına çek" ilkesinin bileşen sözleşmesine uygulanışı.
  • Bu, 9. hattın 3. dersindeki seçenek nesnesi fikrinin bileşen hâlidir: bileşenin dış yüzü, adlandırılmış ve tiplenmiş parametrelerden oluşan bir sözleşmedir. strictTemplates sayesinde yanlış tipte değer bağlamak da derlemede yakalanır.

04Olay yukarı: output()

Veri yukarıdan aşağı iner; peki kart tıklanınca listenin haberi nasıl olur? Çocuk bileşen asla ebeveyninin durumunu doğrudan değiştirmez — bir olay yayınlar, ebeveyn dinler:

import { Component, input, output } from "@angular/core";

export class BasvuruKarti {
  basvuru = input.required<Basvuru>();
  secildi = output<number>();          // dışarıya duyurulan olay

  incele() { this.secildi.emit(this.basvuru().id); }
}

// Ebeveyn şablonu:
// <app-basvuru-karti [basvuru]="b" (secildi)="detayAc($event)" />
// $event = emit edilen değer, burada number

Kalıbın adı tek yönlü veri akışıdır: veri aşağı, olay yukarı. DOM olaylarıyla aynı zihin modeli (8. hat 14. ders: buton click yayınlar, sen dinlersin) — fark, olayın senin tanımladığın anlamsal bir olay olması (secildi, silindi, kaydedildi). Bu disiplinin değeri hata ayıklamada ortaya çıkar: durumu kimin değiştirdiği bellidir, çünkü durum yalnız sahibinde değişir; çocuklar yalnız "şunu istiyorum" diye seslenir.

Eski yazım — sahada okuyacaksın: signal tabanlı input()/output() gelmeden önce aynı iş dekoratörlerle yapılırdı; mevcut kod tabanları bunlarla dolu:

Modern (signal tabanlı)Eski (dekoratörlü — okumayı bil)
Girdibasvuru = input.required<Basvuru>()@Input() basvuru!: Basvuru
Okumabasvuru() — fonksiyon çağrısıthis.basvuru — düz alan
Zorunlulukinput.required derlemede zorlarZorlanamazdı — ! ile susturulurdu (9. hattın "söz" işareti)
Çıktısecildi = output<number>()@Output() secildi = new EventEmitter<number>()
Değişim izlemeSignal doğal izler; computed türetirngOnChanges kancası + SimpleChanges

Eski @Input()'taki ! işaretine dikkat: 9. hatta öğrendiğin kesin atama sözü. Alan kurucuda atanmaz — değeri Angular şablondan bağlar — bu yüzden derleyiciye "güven bana" denirdi. input.required bu sözü gerçek bir derleme denetimine çevirdi; modern yazımın somut kazancı budur.

05Yaşam döngüsü: doğum, değişim, ölüm

Bileşen örneğini sen kurmazsın — new BasvuruKarti() yazan kod yoktur. Şablonda etiketi beliren bileşeni Angular örnekler, ekrana bağlar, güncel tutar ve etiketi kaybolunca yok eder. Bu sürecin belirli anlarında Angular, senin sınıfına "şimdi sırası" diye seslenir — bunlar yaşam döngüsü kancalarıdır:

AnKancaTipik iş
Örnek kurulduconstructorYalnız bağımlılık alma (4. ders) — iş yapılmaz
Girdiler hazırngOnInitBaşlangıç işi: veri isteği başlatma, kurulum
Girdi değiştingOnChanges (eski dünya)Signal input'larda yerine computed/effect (9. ders)
DOM'a çizildiafterNextRenderÖlçüm, DOM'a bağımlı üçüncü parti kurulum
Yok ediliyorngOnDestroyTemizlik: abonelik, zamanlayıcı, dinleyici bırakma
import { Component, OnInit, OnDestroy } from "@angular/core";

export class DetayPaneli implements OnInit, OnDestroy {  // 9. hat: implements = denetim
  private sayac?: number;

  ngOnInit()    { this.sayac = setInterval(() => this.yenile(), 30_000); }
  ngOnDestroy() { clearInterval(this.sayac); }            // sızıntının panzehiri
}

İki klasik tuzak:

  • Kurucuda iş yapmak. Kurucu çalıştığında bileşen henüz şablona bağlanmamıştır; eski dünyada @Input() değerleri kurucuda undefined'dı — "neden input'um boş?" sorusunun ezeli cevabı: erken bakıyorsun, ngOnInit'i bekle. Modern signal input'lar kurucuda okunursa hata fırlatarak seni zaten uyarır. Kural: kurucu yalnız bağımlılık alır, iş ngOnInit'te başlar.
  • ngOnDestroy'u boş bırakmak. 8. hattın 14. dersindeki dinleyici sızıntısının bileşen hâli: bileşen setInterval, global olay dinleyicisi ya da abonelik (5. ders) kurar da bırakmazsa, bileşen ekrandan kalkar ama işi çalışmaya devam eder — bellek ve CPU sızıntısı. Ölçülebilir davranış: listede gezinip aynı paneli 20 kez aç-kapa; sızıntı varsa konsola 20 zamanlayıcının logu düşer, DevTools bellek profili basamak basamak yükselir (8. hat 19. dersin araçları).

06Stil kapsülleme: CSS'in sınırları

8. hatta CSS'in globalliğiyle boğuşmuştun: bir seçici her yeri etkiler, adlar çakışır. Angular'da bileşenin styles'ına yazdığın CSS yalnız o bileşenin şablonunu etkiler. Mekanizma (ViewEncapsulation.Emulated): derleme, bileşenin DOM düğümlerine üretilmiş bir öznitelik ekler (_ngcontent-xyz) ve seçicilerini .kart[_ngcontent-xyz] biçimine çevirir — DevTools'ta açınca bunu göreceksin; artık ne olduğunu biliyorsun.

:host { display: block; border-radius: 8px; }   /* bileşenin KENDİ etiketi */
:host(.vurgulu) { border-color: gold; }         /* etikete sınıf gelirse */
  • :host bileşenin kendi etiketini (<app-basvuru-karti>) seçer. Özel etiketler varsayılan olarak display: inline davrandığından :host { display: block } neredeyse standart bir ilk satırdır — "bileşenime verdiğim genişlik neden işlemiyor" sorusunun sık cevabı.
  • ::ng-deep kapsüllemeyi delip alt bileşenlerin içine stil sızdırır. Kullanımdan kaldırılmıştır (deprecated) ve kaçınılır: sızdırdığın stil, alt bileşenin iç yapısına bağımlılıktır ve o bileşen değişince sessizce kırılır. Doğru yol, alt bileşene stil girdisi (input, CSS özel özelliği) tanımlamaktır.
  • Kapsülleme koruma değil düzendir: global styles.css yine her yere işler; bileşen stilleri onun üstüne biner.

İçerik yansıtma: bileşenin etiketleri arasına yazılan içeriği şablonda <ng-content /> gösterir — 8. hatta gördüğün <details>/<summary> gibi "kabuk" bileşenler (kart, modal, sekme) böyle yazılır:

@Component({
  selector: "app-panel",
  template: `
    <section class="panel">
      <h2>{{ baslik() }}</h2>
      <ng-content />              <!-- kullanıcının içeriği buraya -->
    </section>
  `,
})
export class Panel { baslik = input.required<string>(); }

// Kullanım:
// <app-panel [baslik]="'Notlar'"><p>Serbest içerik…</p></app-panel>

07Üretim pratiği: akıllı ve sunumsal bileşen

Büyüyen kod tabanlarında bileşenler iki role ayrışır; bu ayrım resmi bir Angular özelliği değil, ekiplerin yerleşik mimari kararıdır:

Akıllı (container)Sunumsal (presentational)
İşiVeriyi getirir, durumu yönetir, servis kullanırVerilen veriyi çizer, olay yayınlar
BağımlılıkServisler (4. ders), yönlendiriciYalnız input()/output()
ÖrnekBasvuruListesi — veriyi çeker, filtre durumunu tutarBasvuruKarti — kartı çizer, secildi yayınlar
TestServis sahteleriyle (15. hat)Girdi ver, çıktıya bak — kolay
Yeniden kullanımDüşük — bağlama özgüYüksek — her yerde

Pratik kural: servis enjekte eden bileşen sayısını az tut. Ağacın üst katındaki birkaç akıllı bileşen veriyi indirir; altındaki sunumsal bileşenler saf girdi-çıktı sözleşmesiyle çalışır. Böylece arayüzün büyük bölümü, uygulamanın geri kalanından habersiz, tek başına test edilebilir ve taşınabilir kalır. Bir de performans notu: üretim kodunda bileşenlere changeDetection: ChangeDetectionStrategy.OnPush verildiğini göreceksin — "girdilerim değişmedikçe beni yeniden kontrol etme" bildirimi. Mekanizmasını signals dersinde (9. ders) tam olarak kuracağız; şimdilik varlığını ve amacını bil.

08Mini alıştırma — Kart + liste sözleşmesi

Yaklaşık 20 dakika · kurulum yok — angular.dev/playground ya da StackBlitz Angular şablonu

  1. Basvuru arayüzünü tanımla (id, ad, durum — durum üç değerli birleşim tipi olsun, 9. hat usulü).
  2. BasvuruKarti bileşenini yaz: basvuru = input.required<Basvuru>(), şablonda ad + durum, bir "İncele" butonu, secildi = output<number>().
  3. Kök bileşende 3 elemanlı bir basvurular dizisi tut; şimdilik şablona üç kartı elle yaz (liste çizimini 3. derste öğreneceksin) ve (secildi)="konsolaYaz($event)" bağla. Tıklayınca doğru id'nin konsola düştüğünü gör.
  4. Sözleşmeyi kır: karta [basvuru] vermeden kullan — derleme hatasını oku. Sonra [basvuru]="'yanlış'" gibi yanlış tipte değer bağla — strictTemplates'in mesajını gör. İki hata da çalışma anına kalmadı; bunu not et.
  5. Kancaları gözle: karta ngOnInit ve ngOnDestroy ekleyip console.log koy. Kök şablonda kartlardan birini geçici olarak kaldırıp geri ekle (bir alanla, ya da yorum satırıyla) — doğum/ölüm loglarını izle. ngOnDestroy'suz bir setInterval ekle, kartı kaldır, konsolun akmaya devam ettiğini gör: sızıntıyı kendi gözünle üretmiş oldun; temizliği ekleyip farkı doğrula.
  6. :host { display: block } yazmadan karta kenarlık + genişlik ver, davranışı gör; sonra ekleyip farkı karşılaştır.

09Kendini sına

Tek yönlü veri akışı nedir? input() ve output() bu akışta hangi rolü oynar?

Tek yönlü veri akışı, bileşen ağacında verinin yalnız yukarıdan aşağı inmesi, değişiklik isteğinin ise olay olarak aşağıdan yukarı çıkması kuralıdır: veri aşağı, olay yukarı. Ebeveyn, çocuğun input()'una değer bağlar ([basvuru]="secili"); çocuk bu veriyi okur ama sahibi değildir ve değiştirmez. Kullanıcı çocukta bir şey yaptığında çocuk, output() ile anlamsal bir olay yayınlar (this.secildi.emit(id)); ebeveyn (secildi)="detayAc($event)" ile dinler ve durumu kendisi günceller. Güncellenen durum input üzerinden tekrar aşağı iner ve döngü kapanır. Kazanç hata ayıklanabilirliktir: her durumun tek sahibi vardır, "bu değeri kim değiştirdi" sorusunun cevabı her zaman durumun sahibi bileşendir; çocuklar yalnız istekte bulunur. Çocuk ebeveyn durumunu doğrudan değiştirebilseydi, değişikliğin kaynağı ağacın herhangi bir yeri olabilirdi ve akıl yürütme çökerdi.

Kurucu ile ngOnInit arasındaki fark nedir? "Input'um kurucuda neden boş?" sorusunu açıkla.

Kurucu, sınıfın örneklendiği andır ve Angular bu noktada henüz bileşeni şablona bağlamamış, girdi değerlerini yerleştirmemiştir — kurucunun meşru tek işi bağımlılıkları almaktır. ngOnInit ise Angular'ın "girdilerin hazır, bileşen kuruldu" dediği yaşam döngüsü kancasıdır; veri isteği başlatmak, kurulum yapmak gibi işler buraya yazılır. Eski dekoratörlü dünyada @Input() alanları kurucuda henüz atanmamış olduğundan undefined okunurdu — "input'um boş" şikâyetinin cevabı budur: değer yok değil, erken bakıyorsun. Modern signal input'lar bu hatayı daha da görünür kılar: kurucuda okumaya kalkarsan hata alırsın. Ayrıca bileşeni asla elle new'lemediğini söylemek gerekir: örnekleme, bağlama, güncelleme ve yok etme çerçevenin işidir; senin kodun kancalarla bu sürece eklemlenir. Simetri kuralı da eklenir: ngOnInit'te kurulan her kaynak (zamanlayıcı, abonelik, global dinleyici) ngOnDestroy'da bırakılır; yoksa bileşen ekrandan kalkar ama işi çalışmaya devam eder — bellek sızıntısının bileşen biçimi.

Angular'da bileşen stilleri neden birbirine karışmaz? :host ne işe yarar, ::ng-deep'ten neden kaçınılır?

Varsayılan kapsülleme kipi (Emulated) derleme sırasında her bileşenin DOM düğümlerine üretilmiş benzersiz bir öznitelik ekler ve bileşenin CSS seçicilerini bu öznitelikle sınırlar — .kart seçicisi .kart[_ngcontent-xyz] hâline gelir. Böylece bileşenin stili yalnız kendi şablonuna işler; iki bileşen aynı sınıf adını kullansa da çakışmaz. Bu düzen globalliği kaldırmaz: styles.css'teki kurallar her yere uygulanır, bileşen stilleri üstüne biner. :host, bileşenin kendi etiketini seçer; özel etiketler varsayılan inline davrandığı için :host { display: block } yaygın bir ihtiyaçtır ve "bileşenime genişlik işlemiyor" sorununun sık çözümüdür. ::ng-deep ise kapsüllemeyi delerek alt bileşenlerin içine stil sızdırır; kullanımdan kaldırılmıştır ve kaçınılır, çünkü sızdırılan stil alt bileşenin iç DOM yapısına gizli bir bağımlılıktır — o bileşen yapısını değiştirdiğinde stil sessizce kırılır. Doğru tasarım, alt bileşene açık bir özelleştirme yüzeyi (input, CSS özel özellikleri) tanımlamaktır.

Akıllı / sunumsal bileşen ayrımı nedir, ne kazandırır? input.required bu tasarımda neden değerlidir?

Ayrım, bileşenleri iki role bölen bir mimari disiplindir. Akıllı (container) bileşen veriyi getirir, durumu tutar, servisleri enjekte eder — bağlama gömülüdür. Sunumsal bileşen yalnız input() ile aldığı veriyi çizer ve output() ile olay yayınlar; uygulamanın geri kalanından habersizdir. Kazançlar: sunumsal bileşenler kolay test edilir (girdi ver, çıktıya bak — servis sahtesi gerekmez), projeler arası taşınır ve tasarım sisteminin yapı taşlarına dönüşür; durum yönetimi ise az sayıda akıllı bileşende toplandığı için veri akışı izlenebilir kalır. Pratik kural "servis enjekte eden bileşen sayısını az tut"tur. input.required bu tasarımda sözleşmenin dişleridir: sunumsal bileşenin çalışmak için neye ihtiyacı olduğu tipiyle birlikte bildirilir ve eksik bağlanırsa hata çalışma anına değil derlemeye düşer — 9. hattın "hatayı sebebine ve derleme anına yaklaştır" ilkesinin bileşen sözleşmesindeki karşılığı. Eski @Input() bunu zorlayamıyordu; alan ! ile susturulur, unutulan bağlama çalışma anında undefined olarak patlardı.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Angular bileşeni hangi parçalardan oluşur? (2) @Input / @Output (ya da input()/output()) ne işe yarar; veri akış yönü nedir? (3) Yaşam döngüsü kancalarını sırala; ngOnInit ile constructor farkı nedir? (4) ngOnDestroy hangi durumda kritik önem taşır? (5) ViewEncapsulation nedir; kaç kipi vardır? (6) :host seçicisi neyi hedefler? (7) ng-content ne sağlar? (8) EventEmitter nedir, nasıl kullanılır? (9) Bileşenler arası iletişim yolları nelerdir? (10) OnPush değişiklik algılama stratejisi ne vadeder?

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

Düzen: (1) Tanım — "Bileşen; sınıf (durum + davranış), şablon (görünüm) ve dekoratör üst verisinden oluşur; uygulama bileşen ağacıdır." (2) Sözleşme — "Dış yüz input/output'tur: veri aşağı, olay yukarı; input.required eksik bağlamayı derlemede yakalar, strictTemplates tip denetimini şablona sokar." (3) Döngü — "Kurucu yalnız bağımlılık alır; iş ngOnInit'te başlar, kurulan her kaynak ngOnDestroy'da bırakılır — yoksa sızıntı." (4) Stil — "Kapsülleme emulated'dır; :host ile kök etiketi stillerim, ::ng-deep'ten kaçınırım." (5) Kıdem vuruşu — "Akıllı/sunumsal ayrımıyla servis bağımlılığını üst katta toplarım; sunumsal bileşenler saf girdi-çıktıyla test edilir; üretimde OnPush kullanılır, mekanizması signals dersinin konusu."