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ı directivelerden 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 directiveler 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 directiveimizi 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.ngFordirective adı.let itembir değişken tanımlaması. Bu tanımlamayla şablonda başvurulabiliritemadlı 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$implicitolarak iliştirilen değeri alacağına işaret ediyor.let i = indexde bir değişken tanımlaması ve yukarıdakinden tek farkıi‘ninindexadıyla iliştirilen değeri alacak olması.ofbir anahtar kelime ve veri bağlamada kullanılıyor.itemsise 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
intervalfabrika fonksiyonunu alıp, 1 saniye arayla sayı basanindex$Observable’ını oluşturduk. - Sonra
mapvetakeWhileoperatörlerinin yardımıylacountDownisimli 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 countDownvewithArgs 20olmak üzere üç kez veri bağladık. Yani, tek bağlama ile sınırlı değiliz.index$Observable,countDownfonksiyon,20ise değişken bile değil. Dolayısıyla hemen her tip veriyi bağlayabildiğimizi görüyoruz.ofanahtar kelimesini kullanmadığımız gibi,withArgsgibi rastgele sayılabilecek bir anahtar kelime yerleştirdik. Demek ki anahtar kelimeler bize kalmış.
❓ Şablonda
divyerineng-containerkullanabilir 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
TemplateRefveViewContainerRefzerk ettik. 💉 ViewContainerRef‘inclearmetodunu çağırarak daha önce yerleştirilmiş görünümü temizledik.- Yine
ViewContainerRef‘increateEmbeddedViewmetodunu kullanarak yeni bir görünüm oluşturduk ve bu görünüme şablonla beraber bağlam (context) atadık.$implicitveindexile 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:
ngOnDestroyyaşam döngüsü kancasında (lifecycle hook) var olan aboneliği bitirip belleği serbest bıraktık.ngOnChangesyaş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ğerleriprojectValuemetodumuza 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. 🧟