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ı olayoutput() - Eski
@Input()/@Output()yazımını okuyabilmek; iki dünyanın tablosu - Yaşam döngüsü: kurucu vs
ngOnInit,ngOnDestroyile temizlik - Stil kapsülleme: bileşenin CSS'i neden dışarı sızmaz;
:host ng-contentile 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çinbasvuru()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.requiredderleme 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.
strictTemplatessayesinde 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) | |
|---|---|---|
| Girdi | basvuru = input.required<Basvuru>() | @Input() basvuru!: Basvuru |
| Okuma | basvuru() — fonksiyon çağrısı | this.basvuru — düz alan |
| Zorunluluk | input.required derlemede zorlar | Zorlanamazdı — ! ile susturulurdu (9. hattın "söz" işareti) |
| Çıktı | secildi = output<number>() | @Output() secildi = new EventEmitter<number>() |
| Değişim izleme | Signal doğal izler; computed türetir | ngOnChanges 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:
| An | Kanca | Tipik iş |
|---|---|---|
| Örnek kuruldu | constructor | Yalnız bağımlılık alma (4. ders) — iş yapılmaz |
| Girdiler hazır | ngOnInit | Başlangıç işi: veri isteği başlatma, kurulum |
| Girdi değişti | ngOnChanges (eski dünya) | Signal input'larda yerine computed/effect (9. ders) |
| DOM'a çizildi | afterNextRender | Ölçüm, DOM'a bağımlı üçüncü parti kurulum |
| Yok ediliyor | ngOnDestroy | Temizlik: 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 kurucudaundefined'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şensetInterval, 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 */
:hostbileşenin kendi etiketini (<app-basvuru-karti>) seçer. Özel etiketler varsayılan olarakdisplay: inlinedavrandığı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-deepkapsü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.cssyine 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) | |
|---|---|---|
| İşi | Veriyi getirir, durumu yönetir, servis kullanır | Verilen veriyi çizer, olay yayınlar |
| Bağımlılık | Servisler (4. ders), yönlendirici | Yalnız input()/output() |
| Örnek | BasvuruListesi — veriyi çeker, filtre durumunu tutar | BasvuruKarti — kartı çizer, secildi yayınlar |
| Test | Servis sahteleriyle (15. hat) | Girdi ver, çıktıya bak — kolay |
| Yeniden kullanım | Düşü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
Basvuruarayüzünü tanımla (id,ad,durum— durum üç değerli birleşim tipi olsun, 9. hat usulü).BasvuruKartibileşenini yaz:basvuru = input.required<Basvuru>(), şablonda ad + durum, bir "İncele" butonu,secildi = output<number>().- Kök bileşende 3 elemanlı bir
basvurulardizisi 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. - 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. - Kancaları gözle: karta
ngOnInitvengOnDestroyekleyipconsole.logkoy. 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 birsetIntervalekle, 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. :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."
