Angular Routing Rehberi: RouterModule, Route Parametreleri ve Child Route'lar
Angular 12'de routing: app-routing.module.ts, router-outlet, routerLink, ActivatedRoute ile route parametreleri, query params, Router.navigate, child route'lar, redirectTo ve 404 için wildcard.
Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.
Tek Sayfa, Birden Fazla Ekran
Angular uygulaması bir single-page application, yani tarayıcıya tek bir index.html gelir ve bir daha yeni sayfa istenmez. Ama kullanıcı yine de ana sayfa, ürün listesi, ürün detayı gibi farklı ekranlar görmek ister. Üstelik adres çubuğundaki URL'i kopyalayıp birine gönderdiğinde karşı tarafın aynı ekranı görmesini bekler.
Bunu televizyon gibi düşün. Kanal değiştirdiğinde televizyon değişmez, sadece ekrandaki yayın değişir. Angular'da televizyon AppComponent, kanallar da diğer component'ler. Kumandayı tutan şey ise router. URL'e bakar, eşleştirme tablosundan hangi component'in gösterileceğini bulur ve onu ekrana koyar. Sunucuya tek bir yeni sayfa isteği gitmez.
app-routing.module.ts ve RouterModule.forRoot
Projeyi ng new ile oluştururken "Would you like to add Angular routing?" sorusuna evet dediysen src/app/app-routing.module.ts dosyası zaten hazırdır. Hayır dediysen aynı dosyayı kendin oluşturup AppModule'ün imports dizisine AppRoutingModule olarak ekleyebilirsin. İçine üç route yazalım:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './product-list/product-list.component';
import { AboutComponent } from './about/about.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'about', component: AboutComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Routes bir dizi ve her eleman bir eşleştirme: şu path gelirse şu component'i göster. Path'ler başında eğik çizgi olmadan yazılır; boş string '' ise kök adres demek. Bu üç component'i önceden ng g c home, ng g c product-list ve ng g c about ile ürettiğimizi varsayıyorum.
RouterModule.forRoot(routes) iki iş yapar: route tablosunu kaydeder ve uygulamanın tamamında kullanılacak tek Router service'ini oluşturur. Bu yüzden yalnızca kök module'de, bir kez çağrılır. Alttaki exports: [RouterModule] satırı da önemli. Daha önce gördüğümüz gibi bir module'ün export ettiği şeyleri onu import eden module kullanabilir. Bu satır sayesinde AppModule'deki component'ler template'lerinde router-outlet ve routerLink gibi router directive'lerini tanır.
router-outlet: Ekranın Değiştiği Yer
Route tablosu hazır ama router'a component'leri nereye koyacağını henüz söylemedik. app.component.html dosyasını şöyle düzenle:
<header>
<h1>Kitap Dükkânı</h1>
</header>
<router-outlet></router-outlet>
<footer>Her gün 09.00 - 18.00 arası açığız.</footer>
<router-outlet> bir yer tutucu. Header ve footer her ekranda sabit kalır, aradaki alan ise URL'e göre değişir. Tarayıcıda localhost:4200/about adresine git; AboutComponent tam bu noktada belirir. Geliştirici araçlarıyla bakarsan component'in router-outlet etiketinin içine değil, hemen arkasına kardeş olarak eklendiğini görürsün.
routerLink ve routerLinkActive
Kullanıcı adres çubuğuna URL yazmayacak elbette. Menü için ilk akla gelen klasik href olur ama Angular'da onun yerine routerLink kullanırız:
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Ana Sayfa</a>
<a routerLink="/products" routerLinkActive="active">Ürünler</a>
<a routerLink="/about" routerLinkActive="active">Hakkında</a>
</nav>
Farkı şu: href tarayıcıya sayfayı baştan yükletir. Uygulama sıfırdan başlar, bellekteki bütün veri kaybolur. routerLink ise tıklamayı yakalar, URL'i tarayıcının History API'si ile değiştirir ve sadece outlet'teki component'i değiştirir. Geri ve ileri butonları da sorunsuz çalışmaya devam eder.
routerLinkActive, link aktif route'u gösterdiği sürece elemana verdiğin class'ı ekler. CSS'te .active için bir stil tanımlarsan kullanıcı hangi sayfada olduğunu menüden anlar. Ana sayfa linkindeki exact: true ayarının sebebi basit: her URL / ile başladığı için bu ayar olmadan "Ana Sayfa" linki her sayfada aktif görünürdü.
Route Parametreleri: ActivatedRoute ve paramMap
Her ürün için ayrı bir route yazamayız. Bunun yerine path'te bir parametre tanımlarız:
{ path: 'products/:id', component: ProductDetailComponent }
İki nokta üst üste ile başlayan :id bir yer tutucu. /products/7 de /products/42 de bu route'a düşer. Listeden detaya link verirken diziyle yazmak en temizi:
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>
Detay component'inde parametreyi ActivatedRoute üzerinden okuruz:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html'
})
export class ProductDetailComponent implements OnInit {
productId = 0;
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
this.productId = Number(params.get('id'));
});
}
}
ActivatedRoute, o an ekranda olan route hakkındaki her şeyi taşıyan bir service. paramMap ise parametreleri içeren bir observable. get('id') her zaman string ya da null döndürür; URL'de sayı diye bir şey yoktur, o yüzden Number ile çeviriyoruz.
Peki neden düz bir değer değil de observable? Kullanıcı /products/7 sayfasındayken /products/8 linkine tıklarsa Angular aynı component'i yeniden kullanır. Constructor da ngOnInit de tekrar çalışmaz, ama paramMap yeni değeri yayınlar. Component'in yeniden kullanılmayacağından eminsen this.route.snapshot.paramMap.get('id') ile anlık değeri de alabilirsin. Bu observable'ları router kendisi temizlediği için burada unsubscribe yazmana gerek yok.
Gerçek hayatta id'yi alıp ürünü sunucudan çekersin. Daha önce gördüğümüz switchMap bunun için birebir:
this.product$ = this.route.paramMap.pipe(
switchMap(params => this.productService.getProduct(Number(params.get('id'))))
);
Parametre değiştiğinde switchMap bir önceki HTTP isteğini iptal eder ve yenisini başlatır. Template'te de async pipe ile product$'ı gösterirsin.
Query Params
Path parametresi bir kaynağı tanımlar: 7 numaralı ürün. Sıralama, sayfa numarası, filtre gibi isteğe bağlı bilgiler ise soru işaretinden sonra query param olarak gider: /products?sort=price&page=2.
<a routerLink="/products" [queryParams]="{ sort: 'price', page: 2 }">Fiyata göre sırala</a>
Okuma tarafı paramMap ile aynı mantıkta, sadece adı farklı:
this.route.queryParamMap.subscribe(params => {
this.sort = params.get('sort') ?? 'name';
this.page = Number(params.get('page') ?? 1);
});
Query param yoksa get yine null döner, ?? ile varsayılan değer veriyoruz. Bu yaklaşımın güzel tarafı, kullanıcının filtrelediği listeyi URL'i kopyalayarak aynı hâliyle paylaşabilmesi.
Programatik Navigasyon: Router.navigate
Her yönlendirme bir linke tıklamayla olmaz. Form kaydedildikten sonra detay sayfasına gitmek ya da bir butonla sayfa değiştirmek istediğinde Router service'ini inject edersin:
import { Router } from '@angular/router';
constructor(private router: Router) { }
openProduct(id: number): void {
this.router.navigate(['/products', id]);
}
goToPage(page: number): void {
this.router.navigate(['/products'], { queryParams: { page } });
}
navigate ilk parametre olarak routerLink'e verdiğimiz dizinin aynısını alır. İkinci parametreyle query param gibi ek ayarlar geçebilirsin. Elinde hazır bir URL string'i varsa this.router.navigateByUrl('/products/5') da aynı işi görür. İkisi de geriye, navigasyonun başarılı olup olmadığını söyleyen bir Promise döndürür.
Child Routes
Ürün detay sayfasında "Özellikler" ve "Yorumlar" diye iki sekme olsun ve her sekmenin kendi URL'i bulunsun. Bunun için route'a children ekleriz:
{
path: 'products/:id',
component: ProductDetailComponent,
children: [
{ path: 'specs', component: ProductSpecsComponent },
{ path: 'reviews', component: ProductReviewsComponent }
]
}
Child component'lerin nereye çizileceğini parent'ın template'i belirler. product-detail.component.html içine ikinci bir outlet koyuyoruz:
<h2>Ürün #{{ productId }}</h2>
<nav>
<a routerLink="specs" routerLinkActive="active">Özellikler</a>
<a routerLink="reviews" routerLinkActive="active">Yorumlar</a>
</nav>
<router-outlet></router-outlet>
Linklerin başında eğik çizgi olmadığına dikkat et. Bu linkler relative; bulunduğun route'a göre çözülür, yani /products/7 sayfasındaysan specs linki /products/7/specs adresine gider. Sekme değiştiğinde detay component'i yerinde kalır, sadece içteki outlet değişir. Child component'in :id değerine ihtiyacı varsa onu this.route.parent üzerinden okur, çünkü parametre parent route'a aittir.
Bir eksik var: /products/7 adresine doğrudan gelindiğinde içteki outlet boş kalıyor. Onu bir sonraki bölümde çözeceğiz.
Wildcard ve redirectTo
Router, route tablosunu yukarıdan aşağı okur ve ilk eşleşen route'u kullanır. Postanedeki ayırma rafları gibi; mektup sırayla raflara bakılarak ilk uyan göze girer, en altta da "adres bulunamadı" kutusu durur. Tablonun son hâline bakalım:
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'products', component: ProductListComponent },
{
path: 'products/:id',
component: ProductDetailComponent,
children: [
{ path: '', redirectTo: 'specs', pathMatch: 'full' },
{ path: 'specs', component: ProductSpecsComponent },
{ path: 'reviews', component: ProductReviewsComponent }
]
},
{ path: 'about', component: AboutComponent },
{ path: '**', component: NotFoundComponent }
];
redirectTo gelen adresi başka bir adrese yönlendirir. Kök adrese gelen kullanıcı artık /home'a, detay sayfasına doğrudan gelen kullanıcı da specs sekmesine düşer. Önceki bölümde boş kalan outlet sorunu böylece çözüldü.
pathMatch: 'full' boş path ile yönlendirme yaparken şarttır. Varsayılan değer olan 'prefix' "URL bu path ile başlıyor mu?" diye bakar ve boş string her URL'in başında vardır. Bu ayar olmadan her adres ana sayfaya yönlenirdi.
En alttaki '**' wildcard route'tur ve her şeyle eşleşir. Hiçbir route'a uymayan adresler NotFoundComponent'e düşer. Sıra yüzünden bu satır mutlaka en sonda olmalı; en üste koyarsan uygulamadaki her sayfa 404 gösterir.
Küçük Bir Deneme
Şimdi sıra sende. Yeni bir projede JSONPlaceholder'ın /posts adresini kullanarak küçük bir blog kur: /posts listesi, /posts/:id detayı, bir "Hakkında" sayfası ve bir 404 sayfası. Detay component'inde id'yi paramMap ile al, daha önce gördüğümüz HttpClient ve switchMap ile yazıyı çek.
Detay sayfasına bir "Sonraki yazı" butonu ekle ve Router.navigate ile bir sonraki id'ye git. ngOnInit içine ve paramMap aboneliğinin içine birer console.log koy; butona bastığında hangisinin tekrar çalıştığına bak. Ardından '**' route'unu tablonun en üstüne taşı ve ne olduğunu gör.
Şu an her sayfa herkese açık. Giriş yapmamış birinin yönetim paneline girmesini engellemek, form yarım kalmışken sayfadan çıkılmasını önlemek ya da büyük bölümleri ilk açılışta indirmemek istediğinde router'ın daha gelişmiş özelliklerine ihtiyaç duyacaksın. Bir sonraki yazıda guard'ları ve lazy loading'i konuşacağız.