Angular 6'da Custom Structural Directive Nasıl Yapılır

"Structural Directive"lerle Veriyi Okunaklı Bir Şekilde Görselleştirin

04.10.2018 Perşembe
Angular
6 dakika

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. 😥

Angular'da
Fotoğraf: Will B, Unsplash

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.
  • ngFor directive adı.
  • let item bir değişken tanımlaması. Bu tanımlamayla şablonda başvurulabilir item 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‘nin index 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ı basan index$ Observable’ını oluşturduk.
  • Sonra map ve takeWhile operatörlerinin yardımıyla countDown 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 ve withArgs 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 yerine ng-container kullanabilir miydik? Peki ya ng-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 ve ViewContainerRef zerk ettik. 💉
  • ViewContainerRef‘in clear metodunu çağırarak daha önce yerleştirilmiş görünümü temizledik.
  • Yine ViewContainerRef‘in createEmbeddedView metodunu kullanarak yeni bir görünüm oluşturduk ve bu görünüme şablonla beraber bağlam (context) atadık. $implicit ve index 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ğerleri projectValue 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?

NgAsyncDirective örnek hareketli ekran görüntüsü.

👾 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. 🧟