Angular 6'da Custom Pipe Nasıl Yapılır
Kendi "Pipe"larınızla Yeniden Kullanılabilir Veri Dönüşümleri
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 pipe
lara ek olarak, son derece basit bir arayüzle custom (özel) pipe
yaratabilir ve tüm pipe
ları bileşenlerinizde yeniden kullanabilirsiniz (reusable).
Angular hakkında konuşma şansı bulduğum yazılımcı arkadaşların birçoğunun pipe
ları 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 pipe
ları Angular’ın rakiplerine kıyasla en büyük avantajları arasında sayarım. “Kendi pipe
ları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?
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
veyaPromise
lerin 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
veMap
leringFor
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 pipe
larımızı oluşturmamız lazım.
❓ AngularJS’ten tanıdığımız
filter
veorderBy
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ü sayesindepluck
isimiyle çağırılabilir birpipe
haline getirilmiş.PipeTransform
arayüzünün (interface) gereği olantransform
metodu, sınıfımızda tanımlanmış durumda.- Bu metod
value
vekey
isimli iki parametre alıyor. İlkipipe
a geçirilen değer, ikincisi isepipe
a 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çıpdeclarations
kısmınaPluckPipe
ı 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, yanipipe
ları 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, pipe
ları alt alta yazabiliyoruz. Hayır, async
i 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, bundanurl
i vedata
yı alıyoruz. - Her ihtimale karşı
forceTrailingSlash
fonksiyonundan faydalanarakurl
in sonuna taksim ekliyoruz. data
yı JSON metnine çeviripurl
ikey
olarak kullanaraklocalStorage
a 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 cache
i 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 pipe
larının gücünü göstermekti. Amacına da ulaştığını düşünüyorum. Sonuç olarak, özel pipe
lar, 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. 🤸