Angular 14 – Standalone Components/Directives/Pipes
Merhaba,
Angular mimarisinde, stratejik temellendirmeleri modüller eşliğinde kuruyor ve uygulamanın genel inşaatını bu modüller çerçevesinde sınırlandırıyoruz. Özellikle component, directive ya da pipe yapılanmalarını hususi modüller ile geliştiriyor ve ihtiyaç noktasında kullanılacak yapıyı, o noktadaki sorumlu modül tarafından import ederek erişilebilir kılıyoruz. Bu duruma en güzel örnek olarak youtube kanalımda yayınladığım mini e-ticaret projesini inceleyebilirsiniz. İlgili projenin kaynak kodlarını inceleyebilmek için github reposuna da göz atabilirsiniz. Halbuki bu durum her bir component, directive ve pipe yapılanması için ayrı bir modül oluşturmanın getirisi olan kod maliyetiyle birlikte, istemsiz bir karmaşıklığa da sebebiyet verebilmektedir. Şimdi gelin bu içeriğimizde bu karmaşıklığın neler olabileceğini masaya yatırıp tartışalım ve alternatif çözüm olarak Angular 14 ile birlikte gelen Standalone Components/Directives /Pipes özelliği üzerine istişare eyleyelim. Hadi başlayalım…
Misal olarak bir component oluşturmak istediğimizde en basitinden bu component’in işlevsel hale gelebilmesi için ne gibi inşalarda bulunmamız gerektiğini gözlemleyerek girizgah yapalım isterim.
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-hello', template: `<h3>Hello</h3>`, styleUrls: ['./hello.component.scss'] }) export class HelloComponent implements OnInit { constructor() { } ngOnInit(): void { } }
Şimdi bu component’i uygulama sürecinde kullanabilmek ve bu component’te kullanılacak diğer yapıları da entegre edebilmek için bu component’i temsil edecek bir modül oluşturmamız gerekecektir.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HelloComponent } from './hello.component'; @NgModule({ declarations: [ HelloComponent ], imports: [ CommonModule ], exports: [ HelloComponent ] }) export class HelloModule { }
Tabi oluşturulan bu modüle bakarsak eğer ‘HelloComponent’ hem declare edilmeli hem de başka bir component’te selector üzerinden kullanılabilmesi için export edilmelidir. Şimdi bu component’i ‘app.component.html’ dosyasında kullanabilmek için aşağıdaki gibi selector’ını çağırmamız gerekecektir.
<app-hello></app-hello>
Tüm bu sürecin sağlıklı bir şekilde işleyebilmesi için oluşturulan ‘HelloModule’ü uygulamanın ana modülü olan ‘AppModule’e import etmemiz gerekmektedir.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HelloModule } from './hello/hello.module'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HelloModule ], bootstrap: [AppComponent] }) export class AppModule { }
Peki bu kod maliyetinden ve karmaşıklıktan nasıl kurtulabiliriz la hoca? şeklinde sorunuzu duyar gibiyim… İşte bu makalenin muhtevası bu sorunun cevabı içindir… El-cevap; Standalone Components/Directives/Pipes…
Standalone Components/Directives/Pipes
Angular Standalone Components/Directives/Pipes, uygulamayı modüler bağımlılıklardan kurtarmayı hedefleyen ya da bir başka deyişle modül ihtiyacını azaltarak uygulamanın inşa sürecini kolaylaştırmayı amaçlayan yeni bir niteliktir.
Standalone Components/Directives/Pipes özelliği, v14’te developer preview seviyesindedirler. Kararlı bir yenilik olmadıkları için bu içerikte sunulacak olanların dışında potansiyel olarak değişikliklere uğrayabilirler.
Şimdi yine yukarıda oluşturduğumuz component’i standalone olarak tasarlamak istersek bu sefer de aşağıdaki gibi basit bir konfigürasyonel ayar yapmamız yeterli olacaktır.
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-hello', template: `<h3>Hello</h3>`, styleUrls: ['./hello.component.scss'], standalone: true }) export class HelloComponent implements OnInit { constructor() { } ngOnInit(): void { } }
7. satırda görüldüğü üzere ‘Component’ dekoratörünün içerisinde ‘standalone’ parametresine ‘true’ değerini vererek ilgili component’in bir standalone component olduğunu bildiriyoruz. Böylece artık bu component’in declare edilebilir bir component’ten ziyade bağımsız bir component olduğunu ifade etmiş oluyoruz. Tabi bu durumda da ‘selector’ üzerinden bir kullanım sergilemek istiyorsak eğer bunun için artık hedef component’in bulunduğu modülde ya da component’in ta kendisinde bu standalone component’in import edilmesi gerekmektedir! Evet, yanlış okumadınız, standalone component’ler kullanılacakları noktalarda import edilmek zorundadırlar!
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HelloComponent ], bootstrap: [AppComponent] }) export class AppModule { }
Yukarıdaki kod bloğuna bakarsanız eğer, standalone component olan ‘HelloComponent’i ‘AppComponent’te kullanabilmek için ‘AppComponent’in declare edildiği ‘AppModule’de import ediyoruz. Böylece selector özelliğini ‘AppComponent’te kullanmaya devam edebiliyoruz.
<app-hello></app-hello>
Aynı durum directive ve pipe yapılanmaları için de geçerlidir. Misal olarak aşağıdaki çalışmaları inceleyebilirsiniz;
@Directive({ selector: '[appHello]', standalone: true }) export class HelloDirective { constructor(private element: ElementRef) { element.nativeElement.innerHTML = "Hello"; } }
@Pipe({ name: 'hello', standalone: true }) export class HelloPipe implements PipeTransform { transform(value: string) { return "Hello"; } }
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HelloComponent, HelloDirective, HelloPipe ], bootstrap: [AppComponent] }) export class AppModule { }
<h3 appHello></h3> {{ "" | hello }}
Bir Standalone Yapıda Başka Bir Standalone Yapıyı Kullanmak
Standalone yapılar, herhangi bir modüle bağımlılıkları olmadığı için kendi aralarında kullanılırken import tanımlamasını direkt ‘Component’ dekoratörünün ‘imports’ parametresi üzerinden gerçekleştirmektedirler.
import { Component, OnInit } from '@angular/core'; import { HelloPipe } from '../pipes/hello.pipe'; @Component({ selector: 'app-hello', template: ` <h3>Hello</h3> {{ "" | hello }} `, styleUrls: ['./hello.component.scss'], standalone: true, imports: [HelloPipe] }) export class HelloComponent implements OnInit { constructor() { } ngOnInit(): void { } }
Misal olarak yukarıdaki kod bloğunu incelerseniz; standalone component olan ‘HelloComponent’, standalone pipe olan ‘HelloPipe’ı kullanmak istediğinde bunu 12. satırda olduğu gibi ‘imports’ parametresi eşliğinde bildirmektedir.
Standalone Yapılarda Modüler Yapıları Kullanmak
Aynı şekilde standalone bir yapıda modüler bir yapılanmayı kullanabilmek için de ‘imports’ parametresinde gerekli tanımlamanın yapılması gerekmektedir.
import { Component, OnInit } from '@angular/core'; import { HelloPipe } from '../pipes/hello.pipe'; import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'app-hello', template: ` <h3>Hello</h3> {{ "" | hello }} <button mat-raised-button color="primary">Primary</button> `, styleUrls: ['./hello.component.scss'], standalone: true, imports: [HelloPipe, MatButtonModule] }) export class HelloComponent implements OnInit { constructor() { } ngOnInit(): void { } }
Misal olarak yukarıdaki örneği incelerseniz standalone component olan ‘HelloComponent’ içerisinde material button kullanabilmek için ‘MatButtonModule’ modülünün import edilmesi gerekmektedir.
AppModule’den Kurtulmak
Standalone component’ler de amacın mümkün mertebe modüler yapılanmadan kurtulmak olduğunu söylemiştik. Hatta bunu biraz daha ilerleterek uygulamayı ‘AppModule’den de kurtarmak şeklinde düşünebiliriz. Bu niyeti gerçekleştirmek için uygulamada ‘AppModule’ü silerek başlayabiliriz. 🥸🥸‘AppModule’ü silmek mi? İyide hoca o zaman uygulama takla atmamız mı lo? dediğinizi duyar gibiyim 🙂 Tabi ki de uygulama takla atar. Lakin uygulamada ‘AppModule’ün getirdiği bağımlılıkları ortadan kaldıracak ama bir yandan da onun tüm sorumluluğunu üstlenecek bir noktaya ihtiyacımız olacaktır. Bu da ‘main.ts’ dosyasıdır.
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser'; . . . if (environment.production) { enableProdMode(); } //platformBrowserDynamic().bootstrapModule(AppModule) // .catch(err => console.error(err)); bootstrapApplication(AppComponent, { providers: [ { provide: "url", useValue: "https://..." }, importProvidersFrom( BrowserModule, AppRoutingModule, BrowserAnimationsModule ) ] })
Yukarıda ‘main.ts’ dosyasının son halini görmektesiniz. İçeriğine bakarsanız eğer 10 ile 11. satır aralığında yorum satırı olarak ayarlanan platformBrowserDynamic
fonksiyonu devamında ana modülü ‘AppModule’ olarak belirlememizi sağlayan bootstrapModule
fonksiyonu ile uygulamayı ‘AppModule’e bağımlı bir hale getiriyorduk. Haliyle ilgili kod kısmı silinerek bu bağımlılık ortadan kaldırılmıştır. Bunun yerine, 13 ile 22. satır aralığında olduğu gibi ‘@angular/platform-browser’ path’in de olan bootstrapApplication
metodu sayesinde artık uygulamanın ana modülünden ziyade direkt ana component’i bildirilmekte ve ardından ‘AppModule’de ki importlar ikinci parametre olarak ‘providers’a karşılık verilmektedir. Böylece artık uygulama modülerliğe olan bağımlılığından tamamen izole olmuş bir vaziyette temellendirilmektedir.
Burada özellikle dikkat etmenizi istediğim nokta 16. satırdaki importProvidersFrom
fonksiyonudur. Şimdi normal şartlarda bizler ‘providers’a singleton olarak tutulacak olan nesneleri vermeye, modülleri ise import etmeye alışkınız. Amma velakin burada import edilecek modüller de ‘providers’ üzerinden verilmektedir. Hah işte buradaki ayrımı yaratabilmek için modüllerin importProvidersFrom
metodu aracılığı ile verilmesi gerekmektedir.
Routing ve Lazy Loading
Uygulama standalone altyapısı sayesinde modüler yapılanmadan soyutlandığına göre bu durum routing işlemleri içinde ayrı bir nitelik kazandırmaktadır. Nihayetinde lazy loading süreçlerinde modüler yapılanmaları loadChildren
ile bildiriyorken, standalone component’leri ise modülleri olmadığı için loadComponent
fonksiyonu ile bildirmemiz gerekmektedir.
. . . const routes: Routes = [ { path: "hello", loadComponent: () => import("./hello/hello.component") .then(component => component.HelloComponent) }, { path: "example", loadChildren: () => import("./example/example.module") .then(module => module.ExampleModule) } ]; . . .
Tabi bu tanımlamadan sonra ilgili routing işlemlerinin yapılacağı component hangisi olursa olsun ‘imports’ özelliğine ‘RouterModule’ün eklenmesi gerekmektedir. Misal olarak routing işleminin ‘AppComponent’te yapılacağını varsayarsak eğer aşağıdaki gibi ismi geçen modülün bildirilmesi zaruridir.
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, imports: [HelloComponent, HelloPipe, HelloDirective, RouterModule] }) export class AppComponent { title = 'standaloneExample'; }
Nihai olarak,
Angular mimarlarının, uygulama süreçlerinde ana aktörlerden biri olan component yapılanmasının herhangi bir modüle bağımlı olmaksızın geliştirilmesini ve böylece kod maliyetini düşürmek ile birlikte kullanım kolaylığını hedeflediğini görmekteyiz. Nacizane fikir olarak güzel ve umut verici bir gelişme olduğunun altını çizerek yazımızı noktalıyorum.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek çalışmayı aşağıdaki github adresinden inceleyebilirsiniz.
https://github.com/gncyyldz/StandaloneComponentsExample
Açık konuşmak gerekirse standalone componenti ilk duyduğumda x modülünün state’inden beslenen componentimi import etmeden y modülümün içinde de selector ile kullanırım diyordum ama yine import etmek gerektiğini görünce hayal kırıklığına uğradım açıkçası.
Anguların atomik mimarisine uygun bir adım olmuş.