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

Angular'da Component İletişimi: @Input, @Output, @ViewChild ve ng-content

Mert Özen 31 Eki 2026 6 dk 1 görüntülenme
Angular'da Component İletişimi: @Input, @Output, @ViewChild ve ng-content

Angular 12'de component'ler nasıl konuşur: @Input ile parent'tan child'a veri, @Output ve EventEmitter ile geri bildirim, kendi two-way binding'in, @ViewChild ve ng-content ile content projection.

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

Component Ağacında Veri Nasıl Akar?

Daha önce gördüğümüz component ağacını hatırla: en tepede AppComponent, altında sayfalar, sayfaların içinde listeler, listelerin içinde kartlar. Şimdiye kadar her component kendi verisini kendi tutuyordu. Gerçek bir uygulamada ise bu parçaların birbiriyle konuşması gerekir. Liste hangi ürünü göstereceğini karta söylemeli, kart da "sepete ekle"ye basıldığını listeye haber vermeli.

Bunu bir şirketteki yönetici ile ekibi gibi düşün. Görev yukarıdan aşağı iner: yönetici ekibe ne yapacağını söyler. Rapor aşağıdan yukarı çıkar: ekip işi bitirince yöneticiye haber verir. Angular'da aşağı inen yolun adı @Input(), yukarı çıkan yolun adı @Output(). Bu yazıda ikisini ve parent'ın child'a ulaşmasının iki yolunu daha göreceğiz.

Örneğimiz küçük bir ürün listesi. Önce ürünün şeklini tanımlayalım:

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Input: Parent'tan Child'a Veri

CLI ile product-list ve product-item adında iki component üret. Child olan ProductItemComponent tek bir ürünü gösterecek:

import { Component, Input } from '@angular/core';
import { Product } from '../product';

@Component({
  selector: 'app-product-item',
  templateUrl: './product-item.component.html',
  styleUrls: ['./product-item.component.css']
})
export class ProductItemComponent {
  @Input() product!: Product;
}
<div class="product">
  <h4>{{ product.name }}</h4>
  <p>{{ product.price }} TL</p>
</div>

@Input() decorator'ı, product property'sini dışarıdan doldurulabilir hâle getirir. Component artık kendi verisini üretmiyor; ona ne verilirse onu gösteriyor. Parent tarafı şöyle:

export class ProductListComponent {
  products: Product[] = [
    { id: 1, name: 'Mekanik Klavye', price: 1450 },
    { id: 2, name: 'Kablosuz Mouse', price: 420 }
  ];
}
<app-product-item *ngFor="let p of products" [product]="p"></app-product-item>

[product]="p" ifadesi daha önce gördüğümüz property binding'in ta kendisi. Tek fark, bu kez bir HTML elemanının değil kendi component'imizin property'sine bağlanıyoruz. Parent'taki ürün değişirse child'daki görüntü de değişir.

Strict Mod ve Ünlem İşareti

product!: Product satırındaki ünlem işareti dikkatini çekmiştir. Onu silersen Angular 12'nin strict modunda şu hatayı alırsın:

error TS2564: Property 'product' has no initializer and is not definitely assigned in the constructor.

TypeScript haklı olarak soruyor: bu property'ye ilk değeri kim veriyor? Değeri Angular dışarıdan atayacak, ama TypeScript bunu bilemez. Ünlem işareti "merak etme, bu alan doldurulacak" demenin yolu. Mantıklı bir varsayılan değer varsa ünlem yerine onu vermek daha da iyi: @Input() discount = 0; gibi.

Strict modun bir güzelliği daha var. Template'teki binding'ler de tip kontrolünden geçer. Yanlışlıkla [product]="'Klavye'" yazarsan derleme durur:

error TS2322: Type 'string' is not assignable to type 'Product'.

@Output ve EventEmitter: Child'tan Parent'a Haber

Şimdi ters yön. Kart, sepete ekle butonuna basıldığını listeye bildirmeli. Child'a bir output ekleyelim:

import { Component, EventEmitter, Input, Output } from '@angular/core';

export class ProductItemComponent {
  @Input() product!: Product;
  @Output() addToCart = new EventEmitter<Product>();

  onAdd(): void {
    this.addToCart.emit(this.product);
  }
}
<button (click)="onAdd()">Sepete ekle</button>

EventEmitter, component'in kendi event'ini yayınlamasını sağlayan bir nesne. Generic tipteki Product, bu event'le birlikte ne tür bir veri gönderileceğini söylüyor. emit() çağrıldığı anda event dışarıya fırlatılır.

Parent da bu event'i bir butonun click'ini dinler gibi dinler:

<p>Sepette {{ cart.length }} ürün var.</p>
<app-product-item
  *ngFor="let p of products"
  [product]="p"
  (addToCart)="onAddToCart($event)">
</app-product-item>
cart: Product[] = [];

onAddToCart(product: Product): void {
  this.cart.push(product);
}

Buradaki $event bir DOM event'i değil, child'ın emit() ile gönderdiği üründür. Tipini de Angular bilir, o yüzden onAddToCart metoduna doğrudan Product olarak geçer. Dikkat edersen child sepetten habersiz. Sadece "bana tıklandı, ürün bu" diyor; ne yapılacağına parent karar veriyor. Bu ayrım kartı her yerde yeniden kullanılabilir kılan şey.

Kendi [( )] Binding'imiz

Daha önce gördüğümüz banana in a box sözdizimi, input ile output'un bir isim kuralıyla buluşmasından ibaretti. Artık ikisini de bildiğimize göre kendi two-way binding'imizi yazabiliriz:

export class QuantityPickerComponent {
  @Input() quantity = 1;
  @Output() quantityChange = new EventEmitter<number>();

  change(delta: number): void {
    this.quantity = Math.max(1, this.quantity + delta);
    this.quantityChange.emit(this.quantity);
  }
}
<app-quantity-picker [(quantity)]="amount"></app-quantity-picker>

Kural basit: input'un adı quantity ise output'un adı quantityChange olmalı. Bu ikisi yan yana durduğunda Angular [(quantity)] yazımını kabul eder; parent'taki amount hem child'a iner hem de child'daki her değişiklikle güncellenir.

@ViewChild: Child'a Doğrudan Erişim

Bazen parent'ın child'a bir veri göndermesi değil, ona bir iş yaptırması gerekir. Sayacı sıfırlamak, bir formu temizlemek, bir videoyu durdurmak gibi. Elimizde basit bir sayaç olsun:

export class CounterComponent {
  count = 0;

  increment(): void {
    this.count++;
  }

  reset(): void {
    this.count = 0;
  }
}

Parent, template'indeki sayaç component'ine @ViewChild ile ulaşabilir:

import { Component, ViewChild } from '@angular/core';
import { CounterComponent } from '../counter/counter.component';

export class ScoreboardComponent {
  @ViewChild(CounterComponent) counter!: CounterComponent;

  resetCounter(): void {
    this.counter.reset();
  }
}
<app-counter></app-counter>
<button (click)="resetCounter()">Sıfırla</button>

@ViewChild(CounterComponent), "template'imdeki ilk CounterComponent'i bul ve bu property'ye koy" demek. Artık parent, child'ın public metotlarını doğrudan çağırabiliyor. Sadece template içinde lazımsa daha kısa bir yol da var: <app-counter #counterRef> yazıp butonda counterRef.reset() demek.

Bir uyarı: this.counter constructor çalışırken henüz yok. Angular onu ancak component'in görünümü oluşturulduktan sonra doldurur. Butona tıklandığında sorun çıkmaz, çünkü o ana kadar ekran çoktan hazırdır. Bu zamanlamaya birazdan geri döneceğiz.

Content Projection: ng-content

Input ile child'a veri gönderdik. Peki child'a bir parça HTML göndermek istersek? Her yerde aynı çerçeveyi kullanan bir kart düşün: kenarlık, gölge, iç boşluk hep aynı, içerik her seferinde farklı. Bunun için card adında bir component üret ve template'ine şunu yaz:

<div class="card">
  <ng-content></ng-content>
</div>
<app-card>
  <h3>Mekanik Klavye</h3>
  <p>Kırmızı switch, Türkçe Q klavye.</p>
</app-card>

<app-card> etiketlerinin arasına yazdığın her şey, <ng-content> nerede duruyorsa oraya yerleştirilir. Buna content projection denir. Bir resim çerçevesi gibi düşün: çerçeve sabit, içine koyduğun fotoğraf her duvarda başka.

Önemli bir ayrıntı: yansıtılan içerik parent'a aittir. İçeride {{ }} ile bir değer okursan o değer card'ın değil, parent'ın class'ından gelir. Card sadece yeri sağlar.

select ile Birden Fazla Slot

Kartın başlığı üstte, butonları altta, geri kalan içeriği ortada olsun istiyorsan birden fazla ng-content kullanırsın:

<div class="card">
  <header class="card-header">
    <ng-content select="[card-title]"></ng-content>
  </header>
  <div class="card-body">
    <ng-content></ng-content>
  </div>
  <footer class="card-footer">
    <ng-content select=".card-actions"></ng-content>
  </footer>
</div>
<app-card>
  <h3 card-title>Mekanik Klavye</h3>
  <p>Kırmızı switch, Türkçe Q klavye.</p>
  <div class="card-actions">
    <button>Sepete ekle</button>
  </div>
</app-card>

select, CSS seçicisi alır. [card-title] bu attribute'u taşıyan elemanı, .card-actions bu class'a sahip elemanı yakalar. Hiçbir seçiciye uymayan her şey select'siz olan varsayılan slota düşer. Bölmeli bir beslenme kutusu gibi: her yiyecek kendi gözüne, gerisi ortadaki büyük göze.

Kardeş Component'ler Ne Olacak?

Input ve output sadece parent ile child arasında çalışır. Peki aynı parent'ın altındaki iki kardeş birbirine nasıl haber verir? Örneğin ürün kartına tıklandığında, yandaki sepet özetinin güncellenmesi gerekiyorsa?

Yakın kardeşler için bunu ortak parent üzerinden yapabilirsin: birinci kardeş output ile parent'a haber verir, parent da veriyi input ile ikinci kardeşe iletir. Yukarıdaki cart dizisi tam da böyle bir ortak noktada duruyor; bir sepet özeti component'i eklesen, diziyi ona [items]="cart" ile verebilirdin. Ama ağaç derinleştikçe veriyi beş kat yukarı çıkarıp beş kat aşağı indirmek yorucu hâle gelir. Bu durumda veriyi ağacın dışında, bir service içinde tutmak çok daha temiz bir çözüm. Service'leri ve dependency injection'ı ilerleyen yazılarda detaylandıracağız.

Küçük Bir Deneme

Bir görev kartı component'i yaz. Görevin başlığını @Input() ile alsın, "Tamamlandı" butonuna basıldığında @Output() ile parent'a görevin id'sini göndersin. Parent, tamamlanan görevleri listeden çıkarsın. Kartın alt kısmına da ng-content select ile parent'ın istediği ekstra butonları koyabileceği bir slot ekle.

Ardından @ViewChild örneğine dön ve parent'ın constructor'ına console.log(this.counter) yaz. Aynı satırı resetCounter() metodunun içine de koy ve konsoldaki iki çıktıyı karşılaştır.

Constructor'da undefined, tıklamada dolu bir component göreceksin. Peki arada ne oldu, Angular bir component'i hangi adımlardan geçirerek ekrana getiriyor? Bir sonraki konumuz bu adımların her birine kod yazmamızı sağlayan lifecycle hook'lar.