[Angular]カスタムパイプの作り方

## 概要

- Angularにはtemplate上でフォーマットなど簡易な変換ができるPipeという機能があります
    - https://angular.jp/guide/pipes
- フレームワークが提供するPipeもありますが独自に作ることもできるようなのでその手順のメモ

## パイプとは

- Angularではtemplateの中で`{{}}`を使うと変数を埋め込むことができます
- 埋め込んだ変数の後ろに`{{ 変数 | xxx }}`といった形でPipeを適用することでフォーマット変換などができます

```html
<div>
  <p>{{ 1234567 | number }}</p>
  <p>{{ new Date(2020, 1, 5) | date: 'yyyy年MM月dd日' }}</p>
</div>
```

- 上記の例はAngularが用意している`number`と`date`のPipeを使った例です
- 以下のように画面に表示されます

```
1,234,567
2020年02月05日
```

## カスタムパイプを作る

- 今回は文字列の末尾をマスク化するPipeを作ってみます

### 雛形の生成

- AngularCLIでPipeをgenerateします

```zsh
ng generate pipe --name=mask
```

- Pipeファイルが作成されました

```zsh
% ng generate pipe --name=mask
CREATE src/app/mask.pipe.spec.ts (179 bytes)
CREATE src/app/mask.pipe.ts (201 bytes)
UPDATE src/app/app.module.ts (949 bytes)
```
- 雛形の内容の確認します

```ts:src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: any, ...args: any[]): any {
    return null;
  }
}
```

- アノテーションで定義されている`{ name: 'mask' }`はPipeを使うときの名前です
    - この例では`{{ 文字列 | mask }}`といった具合で使うことになります
- `transform`関数はpipeで渡された値を受け取り、returnした内容が画面に表示される値となります
    - 第2引数は`{{ 文字列 | mask: 1 }}`といった感じでPipeの後に任意の値を渡した場合に受け取ることができます

### Pipeの処理を実装

- Pipeの処理を作ります
- マスク化の処理を書いていきます

```ts:src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: string): string {
    return `${value.slice(0, -4)}****`;
  }
}
```

- 渡された文字列の末尾4文字をマスク化して返す実装をしました
    - サンプルなので特殊ケースは考えません

### Pipeを適用する

- 作ったMaskPipeを使ってみます

```ts:src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>{{ cardNumber | mask }}</h1>',
})
export class AppComponent {
  cardNumber = '1111-1111-1111-1111';
}
```

- 画面上に`1111-1111-1111-****`と表示されているはずです！簡単！

### Pipeに引数を渡す

- もう少し機能を加えてみます
- マスク化する文字数を指定できるようにしてみます

```ts:src/app/mask.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
  transform(value: string, _size?: number): string {
    const size = _size || 4;
    return `${value.slice(0, -_size)}${'*'.repeat(size)}`;
  }
}
```

- 第2引数で`size`を受け取るようにしてみました
    - 例によって例外ケースは考慮しません
- 適用してみます

```ts:src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>{{ cardNumber | mask: 2 }}</h1>',
})
export class AppComponent {
  cardNumber = '1111-1111-1111-1111';
}
```

- 画面上に`1111-1111-1111-11**`と表示されているはずです！

### テストコードを書く

- せっかくなのでテストも書いてみます
- Pipeのテストは`transform`関数のテストを行う形になります

```ts:src/app/mask.pipe.spec.ts
import { MaskPipe } from './mask.pipe';

describe('MaskPipe', () => {
  const pipe = new MaskPipe();
  describe('文字数を指定しなかった場合', () => {
    it('末尾4文字がマスク化されること', () => {
      expect(pipe.transform('12345678')).toBe('1234****');
    });
  });
  describe('文字数を指定した場合', () => {
    it('末尾から指定した文字数分だけマスク化されること', () => {
      expect(pipe.transform('12345678', 2)).toBe('123456**');
    });
  });
});
```

- 純粋なJavaScriptのロジックのテストなので書きやすいですね
- 実行してみます

```zsh
npm test -- --include src/app/mask.pipe.spec.ts
```

<img width="825" alt="スクリーンショット 2020-02-05 18.57.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/0c567a85-33e4-a3f6-e0f5-3c9bcdf43f3f.png">

- すべてグリーンになりました！

## まとめ

- Pipeの実装は純粋なJavaScriptの関数なので学習コスト低く作れる
- 同じ理由でテストコードの実装も簡単
- Pipeは用法用量を守って使ったほうが良さそうだけどうまく使いこなすととても便利そうです