この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの12日目です。
Structiveについて詳しくはこちらより
前回のおさらい
Day 11では、派生状態(getter)を学びました。今日は、値の変換やフォーマットを行う「パイプフィルター」を詳しく見ていきます。
パイプフィルターとは?
パイプフィルターは、構造パスの値を変換・整形するための機能です。
基本構文
{{ パス|フィルター名 }}
例:
<p>{{ user.name|uc }}</p>
<!-- "alice" → "ALICE" -->
<p>${{ product.price|fix,2 }}</p>
<!-- 999 → "999.00" -->
パイプ(|)の意味
パイプは「値を次の処理に渡す」という意味です:
値 → フィルター1 → フィルター2 → 結果
フィルターの特徴
1. 純粋関数
フィルターは純粋関数として実装されています:
- 同じ入力には常に同じ出力
- 副作用なし
- 状態を持たない
// ✅ 純粋関数
function uc(value) {
return value.toUpperCase();
}
// ❌ 状態を持つことはできない
let counter = 0;
function withState(value) {
return value + counter++; // NG
}
2. 単一値の変換
フィルターは単一の値だけを変換します。複数のパス値を合成することはできません。
<!-- ✅ OK: 単一値の変換 -->
{{ user.name|uc }}
<!-- ❌ NG: 複数値の合成はできない -->
{{ user.firstName + user.lastName|uc }}
複数値を扱いたい場合は、getterを使います:
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
{{ fullName|uc }}
3. パラメータ
フィルターはパラメータを持てます。カンマ(,)で区切ります:
{{ value|fix,2 }}
<!-- 小数点以下2桁 -->
{{ text|slice,0,10 }}
<!-- 0文字目から10文字目まで -->
{{ number|round,1 }}
<!-- 小数点第1位で四捨五入 -->
4. チェーン
複数のフィルターをパイプで連結できます:
{{ text|trim|uc }}
<!-- 1. 前後の空白を削除 → 2. 大文字に変換 -->
{{ price|fix,2|defaults,0.00 }}
<!-- 1. 小数点2桁 → 2. falsyなら0.00を使用 -->
フィルターの種類
比較フィルター
値を比較してboolean値を返します。
eq / ne(等価・不等価)
{{ if:status|eq,active }}
<span class="badge active">アクティブ</span>
{{ endif: }}
{{ if:user.role|ne,admin }}
<p>管理者権限がありません</p>
{{ endif: }}
export default class {
status = "active";
user = { role: "user" };
}
not(否定)
{{ if:user.isLoggedIn|not }}
<a href="/login">ログイン</a>
{{ endif: }}
数値比較フィルター
数値の大小を比較します。
lt / le / gt / ge(未満・以下・より大きい・以上)
{{ if:product.stock|lt,5 }}
<span class="warning">残りわずか</span>
{{ endif: }}
{{ if:user.age|ge,18 }}
<p>成人向けコンテンツを表示</p>
{{ endif: }}
{{ if:score|gt,80 }}
<span class="grade-a">A評価</span>
{{ elseif:score|gt,60 }}
<span class="grade-b">B評価</span>
{{ else: }}
<span class="grade-c">C評価</span>
{{ endif: }}
算術フィルター
数値演算を行います。
inc / dec / mul / div / mod
<!-- 加算 -->
<li>{{ $1|inc,1 }}. {{ items.*.name }}</li>
<!-- インデックス0を1始まりに -->
<!-- 減算 -->
<p>割引後: ${{ price|mul,0.9 }}</p>
<!-- 10%オフ -->
<!-- 乗算・除算 -->
<p>半額: ${{ price|div,2 }}</p>
<!-- 余り -->
{{ if:$1|mod,2|eq,0 }}
<tr class="even">...</tr>
{{ else: }}
<tr class="odd">...</tr>
{{ endif: }}
数値フォーマットフィルター
数値を整形して表示します。
fix(小数点固定)
<p>価格: ${{ product.price|fix,2 }}</p>
<!-- 999 → "999.00" -->
<!-- 29.5 → "29.50" -->
locale(地域化された数値)
<p>金額: {{ amount|locale }}円</p>
<!-- 1234567 → "1,234,567" -->
percent(パーセント表示)
<p>進捗: {{ progress|percent }}%</p>
<!-- 0.856 → "86%" -->
<p>進捗: {{ progress|percent,1 }}%</p>
<!-- 0.856 → "85.6%" (小数第1位まで) -->
round / floor / ceil(丸め)
<!-- 四捨五入 -->
<p>{{ value|round }}</p>
<!-- 3.7 → 4 -->
<!-- 小数第1位で四捨五入 -->
<p>{{ value|round,1 }}</p>
<!-- 3.67 → 3.7 -->
<!-- 切り捨て -->
<p>{{ value|floor }}</p>
<!-- 3.9 → 3 -->
<!-- 切り上げ -->
<p>{{ value|ceil }}</p>
<!-- 3.1 → 4 -->
文字列フィルター
文字列を変換・整形します。
uc / lc / cap(大文字・小文字変換)
<h2>{{ product.name|uc }}</h2>
<!-- "laptop" → "LAPTOP" -->
<p>{{ user.email|lc }}</p>
<!-- "Alice@EXAMPLE.COM" → "alice@example.com" -->
<p>{{ title|cap }}</p>
<!-- "hello world" → "Hello world" -->
trim(空白削除)
<p>{{ input|trim }}</p>
<!-- " hello " → "hello" -->
slice / substr(部分文字列)
<!-- 最初の10文字 -->
<p>{{ description|slice,0,10 }}...</p>
<!-- 3文字目から5文字分 -->
<p>{{ text|substr,3,5 }}</p>
pad(パディング)
<!-- 5桁に0埋め -->
<p>注文番号: {{ orderId|pad,5,0 }}</p>
<!-- 123 → "00123" -->
rep(繰り返し)
<p>{{ star|rep,5 }}</p>
<!-- "★" → "★★★★★" -->
rev(逆順)
<p>{{ text|rev }}</p>
<!-- "hello" → "olleh" -->
型変換フィルター
int / float(数値変換)
<p>{{ stringNumber|int }}</p>
<!-- "123" → 123 -->
<p>{{ stringFloat|float }}</p>
<!-- "3.14" → 3.14 -->
boolean / number / string(型変換)
{{ if:value|boolean }}
<p>真です</p>
{{ endif: }}
日付フィルター
Date型の値を整形します。
date / time / datetime
<p>日付: {{ order.createdAt|date }}</p>
<!-- "2024/12/12" -->
<p>時刻: {{ order.createdAt|time }}</p>
<!-- "14:30:00" -->
<p>日時: {{ order.createdAt|datetime }}</p>
<!-- "2024/12/12 14:30:00" -->
ymd(YYYY-MM-DD形式)
<p>{{ date|ymd }}</p>
<!-- "2024-12-12" -->
<p>{{ date|ymd,/ }}</p>
<!-- "2024/12/12" (区切りを変更) -->
export default class {
order = {
createdAt: new Date("2024-12-12T14:30:00")
};
}
真偽値判定フィルター
truthy / falsy
{{ if:errorMessage|truthy }}
<p class="error">{{ errorMessage }}</p>
{{ endif: }}
{{ if:items.length|truthy }}
<p>{{ items.length }}件のアイテム</p>
{{ else: }}
<p>アイテムがありません</p>
{{ endif: }}
defaults(デフォルト値)
<p>{{ user.nickname|defaults,匿名ユーザー }}</p>
<!-- nicknameが空なら"匿名ユーザー"を表示 -->
<p>{{ product.discount|defaults,0 }}%オフ</p>
<!-- discountが未設定なら0を使用 -->
実践例:商品リスト
フィルターを活用した実用的な例:
<template>
<div class="product-list">
<h2>商品一覧</h2>
<div class="stats">
<p>全{{ products.length }}件</p>
<p>平均価格: ${{ averagePrice|fix,2 }}</p>
</div>
<table>
<thead>
<tr>
<th>No.</th>
<th>商品名</th>
<th>価格</th>
<th>在庫</th>
<th>割引率</th>
<th>ステータス</th>
</tr>
</thead>
<tbody>
{{ for:products }}
<tr data-bind="class.low-stock:products.*.stock|lt,5">
<td>{{ $1|inc,1 }}</td>
<td>{{ products.*.name|cap }}</td>
<td>${{ products.*.price|fix,2 }}</td>
<td>
{{ products.*.stock }}
{{ if:products.*.stock|lt,5 }}
<span class="badge">残りわずか</span>
{{ endif: }}
</td>
<td>{{ products.*.discount|percent }}%</td>
<td>
{{ if:products.*.isAvailable }}
<span class="status available">販売中</span>
{{ else: }}
<span class="status unavailable">販売終了</span>
{{ endif: }}
</td>
</tr>
{{ endfor: }}
</tbody>
</table>
</div>
</template>
<script type="module">
export default class {
products = [
{ name: "laptop pro", price: 1299.99, stock: 8, discount: 0.15, isAvailable: true },
{ name: "wireless mouse", price: 29.99, stock: 3, discount: 0, isAvailable: true },
{ name: "keyboard", price: 79.99, stock: 0, discount: 0.25, isAvailable: false },
{ name: "monitor 4k", price: 499.99, stock: 12, discount: 0.1, isAvailable: true }
];
get averagePrice() {
const sum = this.products.reduce((acc, p) => acc + p.price, 0);
return sum / this.products.length;
}
}
</script>
この例では:
-
inc,1: インデックスを1始まりに -
cap: 商品名の先頭を大文字に -
fix,2: 価格を小数点2桁表示 -
lt,5: 在庫5未満をチェック -
percent: 割引率をパーセント表示
実践例:ユーザーダッシュボード
複数のフィルターを組み合わせた例:
<template>
<div class="dashboard">
<h2>ダッシュボード</h2>
<div class="user-info">
<h3>{{ user.name|trim|cap }}</h3>
<p>メール: {{ user.email|lc }}</p>
<p>登録日: {{ user.registeredAt|date }}</p>
<p>最終ログイン: {{ user.lastLogin|datetime }}</p>
</div>
<div class="stats">
<div class="stat-card">
<h4>総購入額</h4>
<p class="amount">${{ user.totalSpent|locale }}</p>
</div>
<div class="stat-card">
<h4>ポイント</h4>
<p class="points">{{ user.points|locale }}pt</p>
</div>
<div class="stat-card">
<h4>会員ランク</h4>
<p class="rank">{{ user.rank|uc }}</p>
</div>
</div>
<div class="orders">
<h3>注文履歴</h3>
{{ if:orders.length|truthy }}
<table>
<thead>
<tr>
<th>注文番号</th>
<th>日付</th>
<th>金額</th>
<th>ステータス</th>
</tr>
</thead>
<tbody>
{{ for:orders }}
<tr>
<td>{{ orders.*.id|pad,8,0 }}</td>
<td>{{ orders.*.date|ymd }}</td>
<td>${{ orders.*.amount|fix,2 }}</td>
<td>
{{ if:orders.*.status|eq,delivered }}
<span class="badge success">配達完了</span>
{{ elseif:orders.*.status|eq,shipped }}
<span class="badge info">配送中</span>
{{ else: }}
<span class="badge warning">処理中</span>
{{ endif: }}
</td>
</tr>
{{ endfor: }}
</tbody>
</table>
{{ else: }}
<p class="empty">注文履歴がありません</p>
{{ endif: }}
</div>
<div class="activity">
<h3>最近の活動</h3>
<ul>
{{ for:activities }}
<li>
<span class="time">{{ activities.*.timestamp|time }}</span>
<span class="text">{{ activities.*.text|trim }}</span>
</li>
{{ endfor: }}
</ul>
</div>
</div>
</template>
<script type="module">
export default class {
user = {
name: " alice smith ",
email: "ALICE@EXAMPLE.COM",
registeredAt: new Date("2024-01-15"),
lastLogin: new Date("2024-12-12T14:30:00"),
totalSpent: 1234567.89,
points: 12345,
rank: "gold"
};
orders = [
{ id: 123, date: new Date("2024-12-01"), amount: 299.99, status: "delivered" },
{ id: 124, date: new Date("2024-12-08"), amount: 149.99, status: "shipped" },
{ id: 125, date: new Date("2024-12-12"), amount: 599.99, status: "processing" }
];
activities = [
{ timestamp: new Date("2024-12-12T14:30:00"), text: " 商品をカートに追加 " },
{ timestamp: new Date("2024-12-12T14:25:00"), text: "商品を閲覧" },
{ timestamp: new Date("2024-12-12T14:20:00"), text: " ログイン " }
];
}
</script>
この例で使用したフィルター:
-
trim|cap: 空白削除してから先頭大文字化 -
lc: メールアドレスを小文字化 -
date,datetime,time: 日時の表示形式 -
locale: 数値の3桁区切り -
uc: ランクを大文字表示 -
pad,8,0: 注文番号を8桁に0埋め -
ymd: 日付をYYYY-MM-DD形式 -
fix,2: 金額を小数点2桁 -
eq: ステータスの比較
チェーンの活用
複数のフィルターを連携させた高度な例:
<!-- 価格を10%割引してから小数点2桁表示 -->
<p>${{ product.price|mul,0.9|fix,2 }}</p>
<!-- テキストをトリムして大文字化し、最初の20文字を取得 -->
<p>{{ description|trim|uc|slice,0,20 }}</p>
<!-- 数値を四捨五入してから文字列化し、0埋め -->
<p>{{ score|round|string|pad,3,0 }}</p>
<!-- 空の場合デフォルト値を使い、大文字化 -->
<p>{{ nickname|defaults,ゲスト|uc }}</p>
まとめ
今日は、パイプフィルターの活用方法を学びました:
フィルターの特徴:
- 純粋関数(状態なし、副作用なし)
- 単一値の変換のみ
- パラメータをカンマで指定
- パイプでチェーン可能
主なフィルター分類:
- 比較:
eq,ne,not,lt,le,gt,ge - 算術:
inc,dec,mul,div,mod - 数値:
fix,locale,percent,round,floor,ceil - 文字列:
uc,lc,cap,trim,slice,pad,rep,rev - 型変換:
int,float,boolean,number,string - 日付:
date,time,datetime,ymd - 真偽値:
truthy,falsy,defaults
使い分け:
- 単純な変換 → フィルター
- 複雑なロジック → getter
- 複数値の合成 → getter
次回予告:
明日は、「実践:Todoアプリを作る」として、Week 2で学んだすべての機能を使って実用的なアプリケーションを構築します。CRUD操作、フィルタリング、ローカルストレージとの連携を実装します。
次回: Day 13「実践:Todoアプリを作る」
パイプフィルターについて質問があれば、コメントでぜひ!