サンプルコードで身につける: カスタムパイプ — @Pipe で独自の整形を作る
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1最小のカスタムパイプ
@Pipe デコレータと transform メソッドだけで作る、カスタムパイプの最小構成です。name に書いた 'yen' がテンプレートでの名前になり、{{ price | yen }} で「1,280円」のような表示になります。金額の表記のようにアプリ全体で統一したい整形は、パイプにしておくと使い回せます。
import { Pipe, PipeTransform } from '@angular/core';
// name の 'yen' がテンプレートでのパイプ名になる
@Pipe({ name: 'yen' })
export class YenPipe implements PipeTransform {
// 第1引数が | の左側の値、戻り値がそのまま表示される
transform(value: number): string {
return value.toLocaleString('ja-JP') + '円';
}
}
// 使う側のテンプレート: {{ 1280 | yen }} -> 「1,280円」
2引数を取るパイプ
transform の第2引数以降が、テンプレートで : に続けて渡した値になります。デフォルト値を持たせておけば {{ text | truncate }} と引数なしでも使え、画面ごとに文字数を変えたいときだけ指定する柔軟な作りにできます。null や短い文字列をそのまま返すガードを最初に書いておくのが、実務で崩れないパイプのコツです。
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
// : で渡した引数が limit, suffix の順に入る
transform(value: string, limit = 30, suffix = '...'): string {
// 空や制限以下のときはそのまま返す(ガードを先に書く)
if (!value || value.length <= limit) {
return value;
}
return value.slice(0, limit) + suffix;
}
}
// {{ longTitle | truncate:10 }} -> 先頭10文字 + ...
// {{ longTitle | truncate:10:'…' }} -> 末尾の記号も差し替えられる
3日本語の相対時刻パイプ
投稿時刻を「3分前」「2時間前」のような日本語の相対表示に変換する、実務でよく作られるパイプです。経過ミリ秒を分に直し、しきい値ごとに表現を切り替えていくだけの素直なロジックで書けます。SNS風のタイムラインや通知一覧など、複数の画面で同じ見せ方を使い回せるのがパイプ化の利点です。
import { Pipe, PipeTransform } from '@angular/core';
// {{ post.createdAt | timeAgo }} で「3分前」のような表示にする
@Pipe({ name: 'timeAgo' })
export class TimeAgoPipe implements PipeTransform {
transform(value: Date): string {
const diffMs = Date.now() - value.getTime();
const minutes = Math.floor(diffMs / 60000);
if (minutes < 1) {
return 'たった今';
}
if (minutes < 60) {
return minutes + '分前';
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return hours + '時間前';
}
return Math.floor(hours / 24) + '日前';
}
}
4配列の絞り込みはパイプにしない
パイプは既定で「純粋」、つまり引数の参照が変わったときだけ再計算されるため、配列の中身だけを書き換えても表示が更新されません。そのため配列のフィルタリングをパイプで行うのは公式にも非推奨で、クラス側の computed に寄せるのが現在の定石です。この例は「絞り込みパイプを作りたくなったら computed」という置き換え先を示しています。
import { Component, computed, signal } from '@angular/core';
// 絞り込みパイプは非推奨: パイプは引数の参照が変わったときだけ
// 再計算されるので、配列の中身の変化に気づけないことがある。
// 代わりに computed で絞り込み、テンプレートには結果だけを渡す。
@Component({
selector: 'app-task-list',
template:
'<ul>@for (task of activeTasks(); track task.id) {' +
' <li>{{ task.title }}</li>' +
'}</ul>',
})
export class TaskListComponent {
tasks = signal([
{ id: 1, title: '設計レビュー', done: false },
{ id: 2, title: 'リリース作業', done: true },
]);
// tasks が更新されれば自動で再計算される
activeTasks = computed(() => this.tasks().filter((t) => !t.done));
}
5standalone パイプを imports して使う
作ったパイプは、DatePipe などの組み込みパイプと同じように、使う側コンポーネントの imports に登録するだけで使えます。パイプの定義と利用側を1ファイルに並べて、「定義 → imports → テンプレートで | 名前」という一連の流れを示した例です。imports への登録を忘れると「パイプが見つからない」というエラーになるので、まずここを疑うのが実務のコツです。
import { Component, Pipe, PipeTransform } from '@angular/core';
// メールアドレスの先頭1文字以外を伏せ字にするパイプ
@Pipe({ name: 'mask' })
export class MaskPipe implements PipeTransform {
transform(value: string): string {
const at = value.indexOf('@');
if (at <= 1) {
return value;
}
return value[0] + '*'.repeat(at - 1) + value.slice(at);
}
}
// 使う側: 組み込みパイプと同じく imports に登録するだけ
@Component({
selector: 'app-member-row',
imports: [MaskPipe],
template: '<p>{{ email | mask }}</p>', // -> t*****@example.com
})
export class MemberRowComponent {
email = 'tanaka@example.com';
}