Angular 6'da Custom Pipe Nasıl Yapılır

Kendi "Pipe"larınızla Yeniden Kullanılabilir Veri Dönüşümleri

29.09.2018 Cumartesi
Angular
8 dakika

Angular’ın ilk sürümünde filter adıyla ortaya konup, —herhalde işlevini daha iyi yansıttığı düşünülmüş ki— 2’nci sürümden itibaren pipe olarak adlandırılmış bir özellik var. Ne işe yarar bu pipe? Herhangi bir veriyi, bileşen (component) tarafında kod yazmak gerekmeksizin, şablon (template) üzerinde başka biçme dönüştürebilmeye (transform) olanak verir. Hem de bunu, siz aksine uğraşmadığınız sürece, yüksek performansla ve kaynağı değişime uğratmaksızın (immutable) yapabilir. Üstüne üstlük, Angular’ın içerisinde hazır gelen pipelara ek olarak, son derece basit bir arayüzle custom (özel) pipe yaratabilir ve tüm pipeları bileşenlerinizde yeniden kullanabilirsiniz (reusable).

Angular hakkında konuşma şansı bulduğum yazılımcı arkadaşların birçoğunun pipeları pek önemsemiyor oluşu beni hep şaşırtmıştır. Evet, algoritmik işlerin şablonlar yerine bileşen sınıfının (class) içinde yer alması gerektiğine ben de katılıyorum. Yine de, ham veriyi değiştirmeksizin DOM’da istediğiniz gibi gösterebilme fikri oldukça çekici. Hele ki, bunu tüm bileşenlerde yeniden kullanılabilmek, harika! Dolayısıyla, ben pipeları Angular’ın rakiplerine kıyasla en büyük avantajları arasında sayarım. “Kendi pipelarını yazmanın sağladığı güç henüz keşfedilmemiş olabilir.” düşüncesinden hareketle, bu konuda oyuncaklı bir makale yazmaya karar verdim. 🚂

Bakalım bileşen sınıfını tanımladığımız TypeScript dosyasına bulaşmadan, sadece pipe kullanarak servisten veri çekip, cache (önbellek) alıp, ekrana yazdırabilecek miyiz?

Angular
Fotoğraf: Gerrie van der Walt, Unsplash

En Basit Haliyle Pipe Kullanımı

Angular yazıp bunu bilmeyen yoktur; ama kısaca hatırlamaktan zarar gelmez. app.component.html dosyasını açıp içindekileri aşağıdaki kodla değiştirin:

<pre><code>{{

  {data: true} | json

}}</code></pre>

Angular’ın içinde gelen json, kendisine verilen (soluna yazılan) değişken veya JavaScript ifadelerini (expression) JSON olarak değerlendirip güzelleştirir (prettify) ve derleyiciye (compiler) iletir. Dolayısıyla bu kod sayfaya şunu yazdıracak:

{
  "data": true
}

İşte pipe kullanımı bu kadar basit. Angular 6’yla birlikte gelenlerin listesini aşağıda bulabilirsiniz:

  • async: Observable veya Promiselerin yayınladığı son değeri gösterebilmenizi sağlar.
  • currency: Sayıları para birimi olarak gösterebilmenize olanak verir. Dili dikkate alır.
  • date: Date nesnesi, sayı veya ISO tarih metinlerini çeşitli tarih biçimlerinde gösterebilmenizi sağlar. Dili dikkate alır.
  • decimal: Ondalık göstermede kullanılır. Dili dikkate alır.
  • i18nPlural: Çevirilerde tekil/çoğul gösterimlere yardımcı olur.
  • i18nSelect: Çevirilerde koşula göre gösterim yapabilmeyi sağlar.
  • json: Yukarıda açıkladığım gibi… ☝️
  • keyvalue: Object ve Mapleri ngFor ile dönülebilir hale getirir.
  • lowercase: Metnin tümünü küçük harfe çevirmeye yarar.
  • percent: Yüzde göstermek içindir. Dili dikkate alır.
  • slice: Dizi veya metinlerin bir bölümünü kırpıp almakta kullanılır.
  • titlecase: Her kelimenin ilk harfini büyük, diğerlerini küçük gösterir.
  • uppercase: Metnin tümünü büyük harflerle göstermeye yarar.

Gördüğünüz gibi çok sayıda pipe var; ancak bunlar yetersiz. Kendi pipelarımızı oluşturmamız lazım.

AngularJS’ten tanıdığımız filter ve orderBy yok. Neden acaba?

Custom Pipe Örneği

Hemen ister CLI’ı kullanarak, ister elle aşağıdaki gibi yeni bir pipe oluşturalım. CLI ile şöyle yapabilirsiniz:

npm run ng g pipe pluck

Şimdi pluck.pipe.ts dosyasını açıp içerisine şu değişikliği yapın:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'pluck'
})
export class PluckPipe implements PipeTransform {

  transform(value: any, key: string): any {
    return value == null ? undefined : value[key];
  }

}

Kodu kısaca açıklayayım:

  • PluckPipe isimli sınıf (class) oluşturulmuş ve bu sınıf üzerinde kullanılan @Pipe dekoratörü sayesinde pluck isimiyle çağırılabilir bir pipe haline getirilmiş.
  • PipeTransform arayüzünün (interface) gereği olan transform metodu, sınıfımızda tanımlanmış durumda.
  • Bu metod value ve key isimli iki parametre alıyor. İlki pipea geçirilen değer, ikincisi ise pipea verilebilen parametre.
  • Metodumuz kendisine verilen değerden bir özelliği (property) ayıklıyor ve onu dönüyor. Evet, biliyorum, iki “eşittir” kullandım. 😱

☠️ Eğer pipeı elle oluşturduysanız, app.module.ts‘i açıp declarations kısmına PluckPipeı eklemeniz gerekiyor. Bunu atlarsanız, derleyiciden şahane bir hata alırsınız.

Şimdi bu pipeı alıp app.component.html‘e ekleyelim.

<pre><code>{{

  {data: true} | pluck:'data' | json

}}</code></pre>

Ekrana artık sadece true yazdırılacak; çünkü pluck ile data özelliğini çekip aldık.

🔎 Gördüğünüz gibi, bir pipeı diğerinin sonucuyla besleyebiliyor, yani pipeları art arda dizebiliyoruz. Bu, gayet okunaklı şekilde ham veriyi dönüştürebilmemizi sağlayan, çok önemli ve kullanışlı bir özellik.

HTTP İsteği Yapan Custom Pipe Oluşturma

Haydi işleri biraz daha ilginç hale getirelim: Pipe kullanarak HTTP isteği yapmayı deneyelim. Önce modeli kurgulayalım:

// result.model.ts

export interface Result {
  url: string;
  data: any;
}

Şimdi bu arayüzle dönecek bir pipe oluşturalım:

// http.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Result } from './result.model';

@Pipe({
  name: 'http'
})
export class HttpPipe implements PipeTransform {

  constructor(private http: HttpClient) {}

  transform(url: string, options = {}): Observable<Result> {
    return this.http.get<Result>(url, options).pipe(
      map(data => ({url, data})),
      catchError(() => of({url, data: ''})),
    );
  }

}

Öncekinden farklı olarak sınıf constructorına http adıyla HttpClient‘ı verdik ve transform metodunda bir HTTP isteği yapıp, cevabı RxJS‘in operatörlerinden yararlanarak Observable<Result> tipine dönüştürdük. (Angular 6 kullandığımız için RxJS’in de 6’ncı sürümünü kullandık.)

Bakalım http işimizi görecek mi? app.component.html‘i açıp değiştirelim:

<pre><code>{{

  'https://swapi.co/api/starships/9'
    | http
    | async
    | pluck:'data'
    | json

}}</code></pre>

Evet, pipeları alt alta yazabiliyoruz. Hayır, asynci kullanmazsak çalışmıyor. Evet, http Observable dönüyor da ondan. 😎

Bu kod, ekrana aşağıdakinin basılmasını sağlayacak:

{
  "name": "Death Star",
  "model": "DS-1 Orbital Battle Station",
  "manufacturer": "Imperial Department of Military Research, Sienar Fleet Systems",
  "cost_in_credits": "1000000000000",
  "length": "120000",
  "max_atmosphering_speed": "n/a",
  "crew": "342953",
  "passengers": "843342",
  "cargo_capacity": "1000000000000",
  "consumables": "3 years",
  "hyperdrive_rating": "4.0",
  "MGLT": "10",
  "starship_class": "Deep Space Mobile Battlestation",
  "pilots": [],
  "films": [
    "https://swapi.co/api/films/1/"
  ],
  "created": "2014-12-10T16:36:50.509000Z",
  "edited": "2014-12-22T17:35:44.452589Z",
  "url": "https://swapi.co/api/starships/9/"
}

Nefis… 😋

Lakin hala eksiğimiz var: Cache. Cevabı bir yerde saklayalım da, daha sonra adresi bir değişkene bağlarsak, aynı id için tekrar tekrar istek yapmayalım, değil mi?

Cache İçin Bir Custom Pipe Oluşturma

Çok daha gelişmiş bir önbellek servisi yazılıp kullanılabilir; ama konumuz bu olmadığı için biz şimdilik localStorage ile yetineceğiz.

// cache.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { Result } from './result.model';

@Pipe({
  name: 'cache'
})
export class CachePipe implements PipeTransform {

  transform(value: Result): Result {
    if (value) {
      window.localStorage.setItem(
        forceTrailingSlash(value.url),
        JSON.stringify(value.data),
      );
    }

    return value;
  }

}

function forceTrailingSlash(url) {
  return `${url}/`.replace(/\/\/$/, '/');
}

Hemen ne yaptığımızı açıklayayım:

  • Eğer bir value verilirse, bundan urli ve datayı alıyoruz.
  • Her ihtimale karşı forceTrailingSlash fonksiyonundan faydalanarak urlin sonuna taksim ekliyoruz.
  • datayı JSON metnine çevirip urli key olarak kullanarak localStoragea kaydediyoruz.
  • Değer olsa da olmasa da verilen değeri olduğu gibi dönüyoruz.

🔎 Pipe parametre almak zorunda değil. Burada da almıyor; ama parametrelerin çok kullanışlı olduğunu da belirtmek gerek. Bu örnekte mesela, daha gelişmiş bir önbellek servisi kullansaydık, önbellek ömrü gibi özellikleri parametreler aracılığıyla belirleyebilirdik. Üstelik, parametre olarak fonksiyon dahi verebilirdik!

Haydi app.component.html‘te cachei kullanalım:

<pre><code>{{

  'https://swapi.co/api/starships/9'
    | http
    | async
    | cache
    | pluck:'data'
    | json

}}</code></pre>

Tabi, bu kadarla bitmiyor. Şu an kayıt ediyor, ancak kayıtlı veriyi kullanmıyoruz. HttpPipe sınıfında ufak bir değişikliğe ihtiyaç var:

// http.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Result } from './result.model';

@Pipe({
  name: 'http'
})
export class HttpPipe implements PipeTransform {

  constructor(private http: HttpClient) {}

  transform(url: string, options = {}): Observable<Result> {
    const cached = window.localStorage.getItem(url);
    
    if (cached) {
      return of({url, data: JSON.parse(cached)});
    }

    return this.http.get<Result>(url, options).pipe(
      map(data => ({url, data})),
      catchError(() => of({url, data: ''})),
    );
  }

}

İşte şimdi oldu. 🙌

Artık önce lokalde kayıt var mı diye kontrol ediyor, bulursak hiç istek yapmadan kayıtlı değerleri dönüyoruz.

👾 Yazıdaki örneğe StackBlitz üzerinden ulaşabilirsiniz.

Kapanış

Elbette, HTTP isteklerini veya önbelleklemeyi pipe kullanarak yapacak değiliz. Zaten, bu yazının hedefi de bunu değil Angular pipelarının gücünü göstermekti. Amacına da ulaştığını düşünüyorum. Sonuç olarak, özel pipelar, veri gösterimi açısından çok yararlı. Servis ve bileşenlerin üzerindeki oluşabilecek kargaşayı da bir miktar engelliyorlar. Öte yandan, tüm bunları getter ve metotlar yardımıyla yaparak şablon kodunu temiz tutmak da geçerli bir yöntem. Tabi bu başka bir yazının konusu. O yüzden…

Bitti. 🤸