この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの9日目です。
Structiveについて詳しくはこちらより
前回のおさらい
Day 8では、forブロックとループコンテキストを学びました。今日は、条件によってUIの表示/非表示を制御する「ifブロック」を詳しく見ていきます。
ifブロックの基本
ifブロックは、指定した条件が真のときだけUIを描画する構文です。
基本的な使い方
{{ if:user.isLoggedIn }}
<div class="welcome">
<p>ようこそ、{{ user.name }}さん!</p>
<button data-bind="onclick:logout">ログアウト</button>
</div>
{{ endif: }}
export default class {
user = {
name: "Alice",
isLoggedIn: true // boolean値
};
logout() {
this.user.isLoggedIn = false;
}
}
動作:
-
user.isLoggedInがtrueのとき → ブロック内が描画される -
user.isLoggedInがfalseのとき → ブロック内が削除される
構文の要素
{{ if:条件パス }}
<!-- 条件が真のときに表示される内容 -->
{{ endif: }}
else と elseif
ifブロックはelseとelseifをサポートしています。
else の使い方
{{ if:user.isLoggedIn }}
<div class="user-panel">
<p>ようこそ、{{ user.name }}さん</p>
<button data-bind="onclick:logout">ログアウト</button>
</div>
{{ else: }}
<div class="login-prompt">
<p>ログインしてください</p>
<button data-bind="onclick:showLogin">ログイン</button>
</div>
{{ endif: }}
動作:
-
user.isLoggedInがtrue→ 上のブロックを表示 -
user.isLoggedInがfalse→ 下のブロック(else)を表示
elseif の使い方
{{ if:user.role.isAdmin }}
<div class="admin-panel">
<h2>管理者パネル</h2>
<button data-bind="onclick:manageUsers">ユーザー管理</button>
</div>
{{ elseif:user.role.isModerator }}
<div class="moderator-panel">
<h2>モデレーターパネル</h2>
<button data-bind="onclick:reviewContent">コンテンツ審査</button>
</div>
{{ else: }}
<div class="user-panel">
<h2>一般ユーザー</h2>
<p>閲覧のみ可能です</p>
</div>
{{ endif: }}
export default class {
user = {
role: {
isAdmin: false,
isModerator: true,
isUser: true
}
};
}
動作の流れ:
-
user.role.isAdminがtrue→ 管理者パネルを表示 - そうでなく、
user.role.isModeratorがtrue→ モデレーターパネルを表示 - どちらも
false→ 一般ユーザーパネルを表示
複数の elseif
{{ if:status.isPending }}
<span class="badge pending">保留中</span>
{{ elseif:status.isApproved }}
<span class="badge approved">承認済み</span>
{{ elseif:status.isRejected }}
<span class="badge rejected">却下</span>
{{ else: }}
<span class="badge unknown">不明</span>
{{ endif: }}
条件の評価:boolean値が必須
重要: このフレームワークでは、条件は必ずboolean型でなければなりません。
export default class {
// ✅ 正しい - boolean値
isLoggedIn = true;
hasPermission = false;
// ❌ 間違い - 文字列や数値は使えない
status = "active"; // if:statusはエラー
count = 5; // if:countはエラー
name = "Alice"; // if:nameはエラー
}
なぜboolean値が必須なのか?
明示性と安全性のためです:
- 条件の意図が明確になる
- 意図しない型変換によるバグを防ぐ
- コードの可読性が向上する
フィルターでboolean値に変換
boolean値でない値を条件として使いたい場合、フィルターを使って変換します。
truthyフィルター
値が「真と評価される」かをbooleanに変換します:
{{ if:items.length|truthy }}
<p>アイテムがあります({{ items.length }}件)</p>
{{ else: }}
<p>アイテムがありません</p>
{{ endif: }}
export default class {
items = [1, 2, 3]; // lengthは3(数値)
}
変換される値:
-
truthyフィルター:0,"",null,undefined→false、それ以外 →true
falseyフィルター
値が「偽と評価される」かをbooleanに変換します:
{{ if:errorMessage|falsey }}
<p>エラーはありません</p>
{{ else: }}
<p class="error">{{ errorMessage }}</p>
{{ endif: }}
export default class {
errorMessage = ""; // 空文字列
}
変換される値:
-
falseyフィルター:0,"",null,undefined→true、それ以外 →false
実用例:配列の長さチェック
<div class="cart">
{{ if:cart.items.length|truthy }}
<h3>カート内のアイテム ({{ cart.items.length }})</h3>
<ul>
{{ for:cart.items }}
<li>{{ cart.items.*.name }}</li>
{{ endfor: }}
</ul>
{{ else: }}
<p class="empty">カートは空です</p>
{{ endif: }}
</div>
export default class {
cart = {
items: []
};
}
派生状態(getter)での条件定義
複雑な条件は、getterでboolean値として定義するのがベストプラクティスです。
基本パターン
{{ if:canCheckout }}
<button data-bind="onclick:checkout" class="btn-primary">
購入する
</button>
{{ else: }}
<p class="error">{{ checkoutError }}</p>
{{ endif: }}
export default class {
cart = {
items: [],
totalPrice: 0
};
user = {
isLoggedIn: false
};
get canCheckout() {
// 複雑な条件をbooleanで返す
return this.user.isLoggedIn &&
this.cart.items.length > 0 &&
this.cart.totalPrice > 0;
}
get checkoutError() {
if (!this.user.isLoggedIn) {
return "ログインが必要です";
}
if (this.cart.items.length === 0) {
return "カートが空です";
}
return "最低購入金額を満たしていません";
}
}
利点:
- UIがシンプルになる
- ビジネスロジックが集約される
- テストしやすい
ループコンテキスト内での派生状態
{{ for:users }}
<div class="user-card">
<h3>{{ users.*.name }}</h3>
{{ if:users.*.canEdit }}
<button data-bind="onclick:editUser">編集</button>
{{ endif: }}
{{ if:users.*.isActive }}
<span class="badge active">アクティブ</span>
{{ else: }}
<span class="badge inactive">非アクティブ</span>
{{ endif: }}
</div>
{{ endfor: }}
export default class {
users = [
{ name: "Alice", role: "admin", lastLogin: Date.now() },
{ name: "Bob", role: "user", lastLogin: Date.now() - 86400000 * 10 }
];
currentUser = {
role: "admin"
};
get "users.*.canEdit"() {
return this.currentUser.role === "admin" ||
this["users.*.role"] === "admin";
}
get "users.*.isActive"() {
const lastLogin = this["users.*.lastLogin"];
const daysSinceLogin = (Date.now() - lastLogin) / (86400000);
return daysSinceLogin < 7; // 7日以内がアクティブ
}
}
DOMの追加と削除
ifブロックは、条件が変わったときにDOMを追加/削除します。
プレースホルダーの仕組み
<!-- 初期状態(条件が偽) -->
<div id="container">
<!--if-block-->
</div>
<!-- 条件が真になったとき -->
<div id="container">
<!--if-block-->
<div class="content">表示される内容</div>
</div>
<!-- 条件が偽に戻ったとき -->
<div id="container">
<!--if-block-->
</div>
コメントノード(<!--if-block-->)がプレースホルダーとして残り、「どこに要素を戻すか」を記憶します。
実践例:ステータス管理
ifブロックとelse/elseifを使った実用的な例:
<template>
<div class="order-status">
<h2>注文状況</h2>
{{ if:order.status.isPending }}
<div class="status pending">
<h3>⏳ 処理中</h3>
<p>ご注文を処理しています</p>
<button data-bind="onclick:cancelOrder">注文をキャンセル</button>
</div>
{{ elseif:order.status.isShipped }}
<div class="status shipped">
<h3>🚚 発送済み</h3>
<p>追跡番号: {{ order.trackingNumber }}</p>
<a data-bind="attr.href:trackingUrl" target="_blank">配送状況を確認</a>
</div>
{{ elseif:order.status.isDelivered }}
<div class="status delivered">
<h3>✅ 配達完了</h3>
<p>{{ order.deliveredDate }}に配達されました</p>
{{ if:order.canReview }}
<button data-bind="onclick:writeReview">レビューを書く</button>
{{ else: }}
<p>レビューありがとうございました</p>
{{ endif: }}
</div>
{{ elseif:order.status.isCancelled }}
<div class="status cancelled">
<h3>❌ キャンセル済み</h3>
<p>この注文はキャンセルされました</p>
</div>
{{ else: }}
<div class="status unknown">
<h3>⚠️ ステータス不明</h3>
<p>サポートにお問い合わせください</p>
</div>
{{ endif: }}
{{ if:order.hasNotes|truthy }}
<div class="notes">
<h4>メモ</h4>
<p>{{ order.notes }}</p>
</div>
{{ endif: }}
</div>
</template>
<script type="module">
export default class {
order = {
id: "ORD-12345",
trackingNumber: "TRK-9876543210",
deliveredDate: "2024-12-01",
notes: "玄関前に置いてください",
hasReview: false,
status: {
isPending: false,
isShipped: false,
isDelivered: true,
isCancelled: false
}
};
get canReview() {
return this.order.status.isDelivered && !this.order.hasReview;
}
get trackingUrl() {
return `https://tracking.example.com/${this.order.trackingNumber}`;
}
get hasNotes() {
return this.order.notes.length > 0;
}
cancelOrder() {
if (confirm("本当にキャンセルしますか?")) {
this.order.status = {
isPending: false,
isShipped: false,
isDelivered: false,
isCancelled: true
};
}
}
writeReview() {
console.log("レビュー画面を開く");
}
}
</script>
この例では:
- 複数のステータスを
elseifで分岐 - ネストした
ifでさらに条件分岐 -
truthyフィルターで文字列の存在チェック - getterで複雑な条件を定義
パフォーマンスの考慮
頻繁に切り替わる場合
<!-- ❌ パフォーマンスが悪い可能性 -->
{{ if:isVisible }}
<div class="heavy-component">
<!-- 複雑なコンテンツ -->
</div>
{{ endif: }}
毎回DOM要素を削除・再作成するとコストが高い場合があります。
CSSで制御する代替案
<!-- ✅ 表示/非表示だけならCSSが効率的 -->
<div data-bind="attr.hidden:isHidden" class="heavy-component">
<!-- 複雑なコンテンツ -->
</div>
export default class {
isVisible = true;
get isHidden() {
return !this.isVisible;
}
}
使い分け:
- ifブロック: コンテンツが不要なときは完全に削除したい場合
- CSS(hidden属性): 頻繁に切り替わる、または初期化コストが高い場合
まとめ
今日は、ifブロックで条件付きレンダリングを学びました:
ifブロックの構文:
{{ if:条件 }}...{{ endif: }}{{ if:条件 }}...{{ else: }}...{{ endif: }}{{ if:条件 }}...{{ elseif:条件 }}...{{ else: }}...{{ endif: }}
重要なルール:
- 条件は必ずboolean型
- boolean以外は
truthy/falseyフィルターで変換 - 複雑な条件はgetterで定義
ベストプラクティス:
- ビジネスロジックをgetterに集約
- UIはシンプルに保つ
- 頻繁に切り替わる場合はCSSを検討
次回予告:
明日は、「属性バインディングとイベントハンドラ」を詳しく解説します。data-bind属性を使った様々なバインディング方法と、イベントハンドラの引数について学びます。
次回: Day 10「属性バインディングとイベントハンドラ」
ifブロックについて質問があれば、コメントでぜひ!