Angular 6'da Custom Structural Directive Nasıl Yapılır
"Structural Directive"lerle Veriyi Okunaklı Bir Şekilde Görselleştirin
Angular projelerinde, DOM üzerinde değişiklik yapmak amacıyla, structural directive (yapısal direktif) adı verilen bir özellikten geniş ölçüde faydalanılmaktadır. Belki ağdalı ismiyle tanıdık gelmedi; fakat ngIf
, ngFor
ve ngSwitch
gibi aslında iyi bilinen ve oldukça sık kullanılan bazı directive
lerden bahsediyoruz. Anlayacağınız, koşula dayalı bir şekilde bileşenleri DOM’a ekleyip kaldırabilen, şablonları döngüye sokup çoklayabilen veya şablona bağlam (context) iliştirmeyi sağlayan özel directive
ler bunlar.
Angular ekibi incelik yapıp, kendi ekledikleri sınırlı sayıdaki structural directive yetersiz gelir diye düşünerek, bizim de geliştirme yapabileceğimiz araçları sunmuş, nasıl çalıştığını da belgelemişler. Bu makalede, async
pipe
ının kullanışlılığını uygulamada hayli azaltan bir özelliğinden kaçınmamıza yarayacak basit ama etkili bir structural directive yazacağız. Çok yaratıcı olduğum için, adı da… Eee… Buldum, ngAsync
olacak. 😥
Etki Alanına Özgü Dil (Domain-specific Language)
Kendi directive
imizi yazmaya başlamadan önce, Angular’a bu amaçla eklenmiş şablon mikro-sözdizimine (microsyntax) bir göz atalım.
*ngFor="let item of items; let i = index"
*
bu sözdiziminin bir structural directive olduğunu gösteriyor.ngFor
directive adı.let item
bir değişken tanımlaması. Bu tanımlamayla şablonda başvurulabiliritem
adlı bir değişkenimiz oluyor. Değişkenin değeri, birazdan nasıl iliştirileceğini göreceğimiz bağlamdan (context) geliyor. Tanımlamada herhangi bir eşitlik bulunmaması da$implicit
olarak iliştirilen değeri alacağına işaret ediyor.let i = index
de bir değişken tanımlaması ve yukarıdakinden tek farkıi
‘ninindex
adıyla iliştirilen değeri alacak olması.of
bir anahtar kelime ve veri bağlamada kullanılıyor.items
ise bağladığı veri.
Basit ve okunaklı, öyle değil mi? Directive sınıfını (class) oluştururken, bu sözdiziminin veriyi bağlama biçimini ifade etmede ne denli başarılı olduğunu göreceğiz. 🧐
Bileşenler Üzerinde Hazırlık
İster Angular CLI, ister StackBlitz veya CodeSandbox gibi bir çevrimiçi editör aracılığıyla yeni bir Angular 6 projesi oluşturun. app.component.ts
dosyasını açıp içeriğini şöyle güncelleyin:
import { Component } from '@angular/core';
import { interval, Observable, pipe, MonoTypeOperatorFunction } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';
type CountDownOperator = (max: number) => MonoTypeOperatorFunction<number>;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
index$: Observable<number> = interval(1000);
countDown: CountDownOperator = (max: number = 10) => pipe(
map(v => max - v),
takeWhile(v => v >= 0),
);
}
Ne yaptık?
- RxJS‘ten
interval
fabrika fonksiyonunu alıp, 1 saniye arayla sayı basanindex$
Observable’ını oluşturduk. - Sonra
map
vetakeWhile
operatörlerinin yardımıylacountDown
isimli yeni bir RxJS operatörü oluşturduk.
Şimdi app.component.html
dosyasını açıp aşağıdaki gibi değiştirin:
<div *ngAsync="let n from index$ through countDown withArgs 20; let i = index">
<h1>{{ n }}</h1>
{{ i }}s
</div>
Webpack iş başındaysa, ngAsync
henüz tanımlanmadığı için hata almışsınızdır. Sorun değil, birazdan bu hatayı çözeceğiz. Burada dikkatinizi çekmek istediğim birkaç konu var:
from index$
,through countDown
vewithArgs 20
olmak üzere üç kez veri bağladık. Yani, tek bağlama ile sınırlı değiliz.index$
Observable,countDown
fonksiyon,20
ise değişken bile değil. Dolayısıyla hemen her tip veriyi bağlayabildiğimizi görüyoruz.of
anahtar kelimesini kullanmadığımız gibi,withArgs
gibi rastgele sayılabilecek bir anahtar kelime yerleştirdik. Demek ki anahtar kelimeler bize kalmış.
❓ Şablonda
div
yerineng-container
kullanabilir miydik? Peki yang-template
?
Custom Structural Directive: ngAsync
Önce app
dizinine ng-async.directive.ts
dosyası ekleyip, içerisinde aşağıdaki gibi bir Directive sınıfı oluşturalım.
import {
Directive,
Input,
} from '@angular/core';
import { Observable, pipe, Subscription, UnaryFunction } from 'rxjs';
@Directive({
selector: '[ngAsync]'
})
export class NgAsyncDirective {
@Input('ngAsyncFrom')
source: Observable<any>;
@Input('ngAsyncThrough')
operator: UnaryFunction<any, any> = _ => pipe;
@Input('ngAsyncWithArgs')
args: any;
private index: number;
private subscription: Subscription;
}
Hmmm… 🤔 from
olmuş ngAsyncFrom
, through
olmuş ngAsyncThrough
ve withArgs
olmuş ngAsyncWithArgs
. Veri bağlama ve anahtar kelimelerin nasıl çalıştığı çok açık, öyle değil mi?
Sonraki aşama, şablona başvurarak görünüm (view) oluşturma. İşaretli yerleri kodumuza ekleyelim:
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { Observable, pipe, Subscription, UnaryFunction } from 'rxjs';
@Directive({
selector: '[ngAsync]'
})
export class NgAsyncDirective {
@Input('ngAsyncFrom')
source: Observable<any>;
@Input('ngAsyncThrough')
operator: UnaryFunction<any, any> = _ => pipe;
@Input('ngAsyncWithArgs')
args: any;
private index: number;
private subscription: Subscription;
constructor(
private tempRef: TemplateRef<any>,
private vcRef: ViewContainerRef,
) { }
private projectValue(value: any): void {
this.vcRef.clear();
this.vcRef.createEmbeddedView(
this.tempRef,
{
$implicit: value,
index: this.index++,
},
);
}
}
Yaptığımız aslında basit:
- Sınıfımıza
TemplateRef
veViewContainerRef
zerk ettik. 💉 ViewContainerRef
‘inclear
metodunu çağırarak daha önce yerleştirilmiş görünümü temizledik.- Yine
ViewContainerRef
‘increateEmbeddedView
metodunu kullanarak yeni bir görünüm oluşturduk ve bu görünüme şablonla beraber bağlam (context) atadık.$implicit
veindex
ile sınırlı değildik, başka özellikler de katabilirdik.
Artık tek yapmamız gereken projectValue
metodunu çağırmak.
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
OnChanges,
OnDestroy,
} from '@angular/core';
import { Observable, pipe, Subscription, UnaryFunction } from 'rxjs';
@Directive({
selector: '[ngAsync]'
})
export class NgAsyncDirective implements OnChanges, OnDestroy {
@Input('ngAsyncFrom')
source: Observable<any>;
@Input('ngAsyncThrough')
operator: UnaryFunction<any, any> = _ => pipe;
@Input('ngAsyncWithArgs')
args: any;
private index: number;
private subscription: Subscription;
constructor(
private tempRef: TemplateRef<any>,
private vcRef: ViewContainerRef,
) { }
private projectValue(value: any): void {
this.vcRef.clear();
this.vcRef.createEmbeddedView(
this.tempRef,
{
$implicit: value,
index: this.index++,
},
);
}
ngOnChanges() {
this.ngOnDestroy();
this.index = 0;
this.subscription = this.source.pipe(
this.operator(this.args)
).subscribe(value => this.projectValue(value));
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
}
Bu kısım oldukça açık:
ngOnDestroy
yaşam döngüsü kancasında (lifecycle hook) var olan aboneliği bitirip belleği serbest bıraktık.ngOnChanges
yaşam döngüsü kancasıyla, önce bileşen durumunu sıfırladık, sonra operatörümüz tarafından dönüşüme uğratılan kaynak Observable’ımıza abone olup (subscribe), gelen değerleriprojectValue
metodumuza aktardık.
Bütün bunların çalışması için son bir iş daha yapmamız lazım. app.module.ts
dosyasını açıp aşağıdaki satırları ekleyin:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NgAsyncDirective } from './ng-async.directive';
@NgModule({
imports: [ BrowserModule ],
declarations: [
AppComponent,
NgAsyncDirective,
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Şimdi bakalım sonuç nasıl?
👾 Yazıda örnek olarak verilen projeye StackBlitz üzerinden ulaşabilirsiniz.
Kapanış
Makalenin başında, async
pipe
ın kullanışlılığını hayli azaltan bir konudan bahsetmiştim hatırlarsanız. Sorun şu: Bir Observable’ı, şablonun içerisinde defalarca async
kullanarak çağırırsanız, hem çok sayıda abonelik (subscription) kurulacağından sizi şaşırtacak sonuçlar doğabilir, hem de okunaklılık açısından zorlayıcı olmaya başlayabilir. Öte yandan, kendi yazdığımız, aynı işi görebilen structural directive sayesinde buna gerek kalmıyor. Diğer bir deyişle, pipe
ın aksine, şablona bağlam ile aktardığı değerlere doğrudan başvurabiliyoruz ve bahsi geçen sorunlarla uğraşmak durumunda kalmıyoruz.
Bu tabi sadece bir örnek. Structural directive için bulabileceğiniz daha birçok kullanım alanı mevcut. Mesela, önümüzdeki günlerde size sıralama ve filtreleme yapabilen ngFor
‘u nasıl yazdığımı anlatabilirim. Adı ngList
bu arada. Çünkü, dedim ya, çok yaratıcıyımdır… Çok.
Bitti. 🧟