Angular 14 ile Gelenler: Typed Reactive Forms, inject() ve Standalone Component'e İlk Bakış
Angular 14 ile reactive forms tip güvenliğine kavuştu: tipli FormControl, nonNullable ve NonNullableFormBuilder. inject() fonksiyonu, developer preview'daki standalone component'ler ve route title.
Sürüm notu: Bu yazı Angular 14 ile gelen yenilikleri anlatır; kodlar Angular 14 içindir.
Değişikliklerin Görünür Hâle Geldiği Sürüm
Angular 13 daha çok kaputun altını temizlemişti. 2 Haziran 2022'de çıkan Angular 14 ise her gün yazdığın koda dokunan değişikliklerle geldi. Reactive forms nihayet tip güvenliğine kavuştu, dependency injection için constructor'a bir alternatif çıktı ve NgModule'süz component fikri ilk kez sahneye adım attı.
Bu yazıda bu üçünü Angular 12'deki karşılıklarıyla yan yana koyacağız. Sonunda route başlıkları ve sürümün diğer küçük ama işe yarar yeniliklerine de kısaca değineceğiz. Önce yükseltmeyi halledelim.
13'ten 14'e Geçiş
Daha önce gördüğümüz akış aynen geçerli: temiz bir commit, doğru Node sürümü ve tek major'lık bir adım.
git commit -am "chore: state before Angular 14 update"
ng update @angular/core@14 @angular/cli@14
Angular 14'ün resmi Node aralığı 14.15 ve üstü ya da 16.10 ve üstü. Node 12 listeden çıktı; seride kullandığımız Node 16 ile devam edebilirsin. TypeScript tarafında en az 4.6 gerekiyor.
Bu yükseltmede migration'lar biraz daha fazla iş yapıyor. package.json'daki Angular paketlerinin önündeki ~ işaretini ^ ile değiştiriyor, tsconfig.json'daki derleme hedefini ES2020'ye çekiyor ve angular.json'dan artık kullanılmayan defaultProject ayarını kaldırıyor. En önemlisi de reactive forms kodunu yeni tip sistemine hazırlayan migration. Ona birazdan döneceğiz.
Angular 12'de Reactive Forms ve any
Daha önce gördüğümüz reactive forms'u hatırla. Form değerlerinin tipi yoktu, her şey any olarak geliyordu:
export class ProfileFormComponent {
form = new FormGroup({
name: new FormControl(''),
age: new FormControl(0)
});
save(): void {
const value = this.form.value; // any
console.log(value.nmae.toUpperCase()); // compiles, fails at runtime
}
}
name yerine nmae yazdık ve TypeScript hiçbir şey demedi. Çünkü form.value bir any; derleyicinin gözünde üzerinde her property olabilir. Hatayı ancak kullanıcı kaydet butonuna bastığında, konsolda undefined ile ilgili bir mesaj olarak görürsün. Etiketsiz kutularla taşınmak gibi: kutuyu açana kadar içinde ne olduğunu bilmiyorsun.
Typed Forms: Form Artık Tipini Biliyor
Angular 14 ile reactive forms varsayılan olarak tipli. Aynı form şimdi şöyle davranıyor:
export class ProfileFormComponent {
form = new FormGroup({
name: new FormControl('', { nonNullable: true }),
age: new FormControl<number | null>(null)
});
save(): void {
console.log(this.form.value.nmae); // compile error
const raw = this.form.getRawValue(); // { name: string; age: number | null }
console.log(raw.name.toUpperCase());
}
}
Artık nmae yazdığın anda editör altını kırmızıyla çiziyor. Angular, FormControl('') gördüğünde başlangıç değerinden tipi çıkarıyor. Ama dikkat: bu tip string değil, string | null. Sebebi reset(). Varsayılan davranışta reset edilen bir control'ün değeri null olur ve TypeScript bu ihtimali senden saklamıyor.
nonNullable: true seçeneği bu davranışı değiştiriyor. Böyle bir control reset edildiğinde null'a değil, başlangıç değerine döner ve tipi düz string olur. Bu bir tip ayarından fazlası, çalışma anındaki davranışı da değiştiriyor; mevcut bir forma eklerken bunu aklında tut.
age için tipi açıkça yazdık. Başlangıç değeri null olan bir control'de Angular tipi sadece null olarak çıkarırdı ve sonra setValue(30) derleme hatası verirdi. FormControl<number | null> yazarak niyetimizi baştan söylüyoruz.
FormGroup, Partial ve NonNullableFormBuilder
Yukarıda neden form.value yerine getRawValue() kullandığımı merak etmiş olabilirsin. Bir FormGroup'taki disabled control'ler value içinde yer almaz. Angular da bunu tipe yansıtıyor: form.value'nun tipi Partial<{ name: string; age: number | null }>, yani her alan undefined olabilir. Disabled alanlar dahil her şeyi kesin tiplerle istiyorsan getRawValue() kullanırsın.
Büyük formlarda her control'e tek tek nonNullable: true yazmak yorucu. Bunun için NonNullableFormBuilder var:
import { Component, inject } from '@angular/core';
import { NonNullableFormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-signup',
templateUrl: './signup.component.html'
})
export class SignupComponent {
private readonly fb = inject(NonNullableFormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.minLength(8)]
});
}
Bu builder'ın ürettiği her control non-nullable. form'un tipi de FormGroup<{ email: FormControl<string>; password: FormControl<string> }> olarak kendiliğinden çıkıyor. Var olan bir FormBuilder'ın nonNullable property'si de aynı builder'ı verir. Constructor yerine kullandığımız inject()'e de birazdan geleceğiz.
Anahtarları önceden bilinmeyen, dinamik gruplar için de FormRecord eklendi. Örneğin kullanıcının istediği kadar etiket ekleyebildiği bir formda FormRecord<FormControl<string>> kullanabilirsin.
Migration Kodunu Nasıl Değiştirdi?
Yükseltmeden sonra mevcut formlarına bakarsan şöyle bir şey görürsün:
form = new UntypedFormGroup({
name: new UntypedFormControl(''),
age: new UntypedFormControl(0)
});
Migration, var olan bütün form class'larını Untyped önekli karşılıklarıyla değiştirdi. UntypedFormGroup, UntypedFormControl, UntypedFormArray ve UntypedFormBuilder, Angular 12'deki davranışın birebir aynısı. Yani kodun yükseltmeden sonra da derlenmeye devam ediyor ve hiçbir şey kırılmıyor.
Tip güvenliğine geçmek senin elinde ve kademeli yapabilirsin. Bir formu ele al, Untyped öneklerini sil, derleyicinin gösterdiği hataları düzelt. Bu hataların bir kısmı büyük ihtimalle yıllardır orada duran gerçek hatalar olacak.
inject(): Constructor'sız Dependency Injection
Daha önce gördüğümüz dependency injection'da service'leri hep constructor'dan alıyorduk:
export class ProductListComponent {
constructor(
private productService: ProductService,
private router: Router
) { }
}
Angular 14 ile aynı şeyi bir field üzerinde yapabiliyorsun:
import { inject } from '@angular/core';
export class ProductListComponent {
private readonly productService = inject(ProductService);
private readonly router = inject(Router);
}
inject() fonksiyonu aslında yeni değil; daha önce de vardı ama yalnızca provider factory'leri gibi dar yerlerde kullanılabiliyordu. Angular 14 onu component, directive, pipe ve service'lerin constructor'ında ve field initializer'larında da kullanılabilir hâle getirdi.
Tek bir kural var: inject() yalnızca bir injection context içinde çalışır. Pratikte bu, constructor ya da field tanımı demek. ngOnInit içinde ya da bir butonun click handler'ında çağırırsan, "inject() must be called from an injection context" diye başlayan bir hata alırsın. Garsona sipariş masadayken verilir; mutfağa sonradan seslenemezsin.
Peki neden uğraşalım? En büyük kazanç kalıtımda. Angular 12'de constructor'ında service alan bir base class'tan türettiğin her component, o service'leri kendi constructor'ında da alıp super(...) ile yukarı geçirmek zorundaydı. inject() ile base class bağımlılıklarını kendisi alıyor, alt class'lar bu yükü taşımıyor. İkinci kazanç da tekrar kullanılabilir yardımcı fonksiyonlar: içinde inject() çağıran küçük fonksiyonlar yazıp farklı component'lerde kullanabiliyorsun.
Standalone Component'e İlk Bakış
Angular 12'de her component bir NgModule'e declare edilmek zorundaydı. Angular 14 bu kurala ilk kez bir istisna getirdi:
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-rating',
standalone: true,
imports: [CommonModule],
template: `
<span *ngFor="let star of stars" [class.filled]="star <= value">★</span>
`
})
export class RatingComponent {
@Input() value = 0;
stars = [1, 2, 3, 4, 5];
}
standalone: true diyen bir component hiçbir module'e declare edilmez. Template'inde ihtiyaç duyduğu her şeyi kendi imports dizisinde söyler; burada *ngFor için CommonModule'ü aldık. Kendi takımını yanında getiren bir usta gibi düşün: şantiyenin ortak deposuna muhtaç değil.
Bu component'i mevcut NgModule tabanlı uygulamanda da kullanabilirsin. Tek fark, onu declarations'a değil imports'a eklemen:
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, RatingComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Standalone bir component'i declarations'a koyarsan derleyici hata verir. CLI'den doğrudan standalone component üretmek için ng g c rating --standalone yeterli. bootstrapApplication ile AppModule'ü tamamen ortadan kaldırmak da bu sürümde mümkün, ama o kısmı Angular 15'te stable olduğunda detaylandıracağız.
Önemli bir uyarı: Angular 14'te standalone API'ler developer preview durumundaydı. Yani denemen için hazırdı ama stable olmadan önce değişebilirdi. Production'da bir sonraki sürümü beklemek mantıklı bir tercihti.
Route Başlıkları ve Diğer Notlar
Angular 12'de her sayfanın tarayıcı sekmesindeki başlığını değiştirmek için Title service'ini her component'te ayrı ayrı çağırırdık:
export class ProductListComponent implements OnInit {
constructor(private title: Title) { }
ngOnInit(): void {
this.title.setTitle('Ürünler');
}
}
Angular 14'te başlık route tanımının bir parçası:
const routes: Routes = [
{ path: 'products', component: ProductListComponent, title: 'Ürünler' },
{ path: 'cart', component: CartComponent, title: 'Sepetim' }
];
Router, navigasyon bitince başlığı kendisi güncelliyor. Bütün başlıklara ortak bir son ek eklemek gibi özel bir kural istiyorsan TitleStrategy'yi genişletirsin:
@Injectable({ providedIn: 'root' })
export class AppTitleStrategy extends TitleStrategy {
private readonly title = inject(Title);
override updateTitle(snapshot: RouterStateSnapshot): void {
const pageTitle = this.buildTitle(snapshot);
this.title.setTitle(pageTitle ? `${pageTitle} | Mağaza` : 'Mağaza');
}
}
buildTitle aktif route'un başlığını bulur, biz de son eki ekleyip yazarız. AppModule'ün providers dizisine { provide: TitleStrategy, useClass: AppTitleStrategy } eklemen yeterli.
Kısa kısa
Template'ten artık component'in protected üyelerine de erişebiliyorsun; template'te kullanılan ama dışarıya açılması gerekmeyen alanlar için iyi bir seçenek. CLI'ye terminalde otomatik tamamlama sağlayan ng completion ve build önbelleğini yönetmek için ng cache komutları eklendi, esbuild tabanlı yeni bir builder da deneysel olarak geldi. 14.1'de lazy loading'de kullanılan CanLoad'a alternatif olarak CanMatch guard'ı, 14.2'de ise düz fonksiyon olarak yazılabilen guard'lar ve NgOptimizedImage'ın ilk hâli geldi. Bu son ikisini Angular 15 yazısında ayrıntılı göreceğiz.
Küçük Bir Deneme
Angular 13'e yükselttiğin projeyi bu kez 14'e taşı. git diff'te form class'larının Untyped önekli hâle geldiğini gör. Sonra bir formu seç: Untyped öneklerini sil, string alanlara nonNullable: true ekle ve derleyicinin ne söylediğine bak. form.value ile form.getRawValue()'nun tiplerini editörde üzerine gelip karşılaştır.
Ardından o formun bulunduğu component'teki constructor injection'ı inject() ile değiştir. Bir kere de bilerek ngOnInit içinde inject() çağırıp hatayı oku. Son olarak route tanımlarına title ekleyip sekme başlığının değiştiğini kontrol et.
Standalone component bu sürümde henüz deneme aşamasındaydı. Bir sonraki adımda Angular 15 ile standalone API'ler stable oluyor: bootstrapApplication, provideRouter, provideHttpClient ve bir uygulamayı AppModule olmadan ayağa kaldırmanın mantığı.