テンプレート構文とコントロールフロー
Angularのテンプレートは独自の構文を持ち、HTML内でロジックを表現します。Angular 17以降の新しいコントロールフロー構文を中心に、データバインディング・パイプ・遅延読み込みまで幅広く解説します。
1. データバインディング
Angularには4種類のデータバインディングがあります。
1.1 補間(Interpolation)
{{ expression }} でコンポーネントのプロパティをテキストとして出力します。
<!-- コンポーネント: title = 'Angular'; count = 42; -->
<h1>{{ title }}</h1>
<p>件数: {{ count }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ user.name | uppercase }}</p>
1.2 プロパティバインディング(Property Binding)
[property]="expression" でDOM属性やコンポーネント @Input() に値をバインドします。
<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">送信</button>
<input [value]="searchText" />
<!-- 子コンポーネントへの入力 -->
<app-user-card [user]="currentUser" [editable]="isAdmin" />
// React: JSX属性に{}で直接埋め込む
<img src={imageUrl} alt={imageAlt} />
<button disabled={isLoading}>送信</button>
Angular では [attr]="expr" 構文で明示的にバインドします。
1.3 イベントバインディング(Event Binding)
(event)="handler($event)" でDOMイベントを処理します。
<button (click)="onSave()">保存</button>
<input (input)="onSearch($event)" (keydown.enter)="onSearch($event)" />
<form (submit)="onSubmit($event)">...</form>
<!-- キーボードショートカット -->
<div (keydown.ctrl.s)="onSave()">...</div>
export class MyComponent {
onSave() { console.log('saved'); }
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log(value);
}
}
1.4 双方向バインディング(Two-way Binding)
[(ngModel)] でフォーム入力とプロパティを同期します(FormsModule が必要)。
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
template: `
<input [(ngModel)]="name" />
<p>こんにちは、{{ name }}さん!</p>
`,
})
export class AppComponent {
name = '';
}
[(ngModel)] は [ngModel]="name" (ngModelChange)="name=$event" の糖衣構文です。
2. 新しいコントロールフロー構文(Angular 17+)
Angular 17 で導入された組み込みの制御フロー構文です。従来の構造ディレクティブ(*ngIf, *ngFor)より高速で直感的に書けます。
2.1 @if / @else if / @else
@if (user.isLoggedIn) {
<p>ようこそ、{{ user.name }} さん!</p>
} @else if (user.isGuest) {
<p>ゲストとしてログイン中</p>
} @else {
<a routerLink="/login">ログイン</a>
}
// React: 三項演算子 or && 演算子
{user.isLoggedIn ? (
<p>ようこそ、{user.name} さん!</p>
) : user.isGuest ? (
<p>ゲストとしてログイン中</p>
) : (
<a href="/login">ログイン</a>
)}
2.2 @for(track 必須)
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>データがありません</li>
}
track はリストの再レンダリングを最適化するために必須です。一意な識別子(id など)を指定します。
<!-- 組み込み変数を活用 -->
@for (product of products; track product.id; let i = $index, last = $last) {
<div [class.last-item]="last">
{{ i + 1 }}. {{ product.name }} — ¥{{ product.price }}
</div>
}
| 変数 | 説明 |
|---|---|
$index | 現在のインデックス(0始まり) |
$first | 最初の要素なら true |
$last | 最後の要素なら true |
$even | 偶数インデックスなら true |
$odd | 奇数インデックスなら true |
$count | リストの総数 |
// React: map() + key prop
{items.map((item, index) => (
<li key={item.id}>{item.name}</li>
))}
// 空リスト表示は手動で条件分岐
{items.length === 0 && <li>データがありません</li>}
2.3 @switch / @case / @default
@switch (user.role) {
@case ('admin') {
<app-admin-panel />
}
@case ('editor') {
<app-editor-toolbar />
}
@default {
<app-viewer />
}
}
3. @defer による遅延読み込み(Angular 17+)
@defer でコンポーネントやテンプレートブロックを遅延読み込みし、初期バンドルサイズを削減できます。
基本構文
@defer {
<app-heavy-chart />
} @placeholder {
<p>読み込み中…</p>
} @loading (minimum 300ms) {
<app-spinner />
} @error {
<p>読み込みに失敗しました。</p>
}
| ブロック | 説明 |
|---|---|
@defer | 実際に遅延ロードするコンテンツ |
@placeholder | defer 前(初期表示)に表示 |
@loading | ダウンロード中に表示 |
@error | 読み込み失敗時に表示 |
トリガー
<!-- ビューポートに入ったときにロード -->
@defer (on viewport) {
<app-analytics-widget />
}
<!-- インタラクション(クリック)でロード -->
@defer (on interaction) {
<app-comments />
} @placeholder {
<button>コメントを表示</button>
}
<!-- アイドル状態になったときにロード(デフォルト) -->
@defer (on idle) {
<app-recommendation />
}
<!-- 条件式が true になったときにロード -->
@defer (when showDetails) {
<app-detail-panel />
}
// React: React.lazy + Suspense
const HeavyChart = React.lazy(() => import('./HeavyChart'));
<Suspense fallback={<p>読み込み中…</p>}>
<HeavyChart />
</Suspense>
Angular の @defer はビューポートやユーザー操作など、より細かいトリガー制御が可能です。
4. 従来の構造ディレクティブ(レガシー)
Angular 17 より前のコードやライブラリでは以下の構造ディレクティブが使われます。既存コードの読み解きに活用してください。
4.1 *ngIf
<p *ngIf="isLoggedIn">ログイン済み</p>
<p *ngIf="!isLoggedIn">未ログイン</p>
<!-- else テンプレート -->
<p *ngIf="isLoggedIn; else guestBlock">ようこそ!</p>
<ng-template #guestBlock>
<p>ゲストです</p>
</ng-template>
4.2 *ngFor
<li *ngFor="let item of items; let i = index; trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
trackById(index: number, item: Item): number {
return item.id;
}
4.3 *ngSwitch
<div [ngSwitch]="status">
<p *ngSwitchCase="'active'">アクティブ</p>
<p *ngSwitchCase="'inactive'">非アクティブ</p>
<p *ngSwitchDefault>不明</p>
</div>
新規開発では @if / @for / @switch を使用してください。
既存コードのマイグレーションには Angular CLI のマイグレーションツールが利用できます。
ng generate @angular/core:control-flow
5. 属性ディレクティブ
5.1 ngClass
条件に応じてCSSクラスを動的に切り替えます。
<!-- 文字列 -->
<p [ngClass]="'active bold'">テキスト</p>
<!-- オブジェクト -->
<p [ngClass]="{ active: isActive, disabled: !isEnabled }">テキスト</p>
<!-- 配列 -->
<p [ngClass]="['base-class', isActive ? 'active' : 'inactive']">テキスト</p>
Angular 17+ では class バインディングも使えます。
<!-- 単一クラスの条件付きバインディング -->
<p [class.active]="isActive" [class.error]="hasError">テキスト</p>
5.2 ngStyle
条件に応じてインラインスタイルを動的に変更します。
<p [ngStyle]="{ color: textColor, fontSize: fontSize + 'px' }">テキスト</p>
Angular 17+ では style バインディングも使えます。
<p [style.color]="textColor" [style.font-size.px]="fontSize">テキスト</p>
// React: className と style prop
<p className={`base ${isActive ? 'active' : ''}`}>テキスト</p>
<p style={{ color: textColor, fontSize: `${fontSize}px` }}>テキスト</p>
6. テンプレート参照変数
#変数名 でテンプレート内のDOM要素やコンポーネントへの参照を取得できます。
<!-- DOM 要素を参照 -->
<input #searchInput type="text" />
<button (click)="search(searchInput.value)">検索</button>
<!-- コンポーネントのメソッドを呼ぶ -->
<app-modal #modal />
<button (click)="modal.open()">モーダルを開く</button>
<!-- ng-template への参照 -->
<ng-template #loadingTpl>
<app-spinner />
</ng-template>
<ng-container *ngIf="isLoading; else loadingTpl">
<app-content />
</ng-container>
TypeScript側から参照するには @ViewChild を使います。
import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({ /* ... */ })
export class SearchComponent {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
focusInput() {
this.searchInput.nativeElement.focus();
}
}
// React: useRef
const searchInput = useRef<HTMLInputElement>(null);
const focusInput = () => searchInput.current?.focus();
<input ref={searchInput} type="text" />
7. パイプ(Pipes)
パイプはテンプレートでデータを変換・整形するための仕組みです。| 演算子で適用します。
7.1 組み込みパイプ
<!-- date: 日付フォーマット -->
<p>{{ today | date:'yyyy/MM/dd' }}</p>
<p>{{ today | date:'short' }}</p>
<!-- currency: 通貨フォーマット -->
<p>{{ price | currency:'JPY':'symbol':'1.0-0' }}</p>
<!-- number: 数値フォーマット -->
<p>{{ 1234567.89 | number:'1.1-2' }}</p>
<!-- uppercase / lowercase / titlecase -->
<p>{{ 'hello world' | uppercase }}</p>
<p>{{ 'HELLO WORLD' | titlecase }}</p>
<!-- json: デバッグ用 -->
<pre>{{ user | json }}</pre>
<!-- async: Observable / Promise を自動購読 -->
<p>{{ user$ | async }}</p>
<!-- keyvalue: オブジェクトをループ -->
@for (entry of config | keyvalue; track entry.key) {
<p>{{ entry.key }}: {{ entry.value }}</p>
}
よく使うパイプ一覧
| パイプ | 説明 | 例 |
|---|---|---|
date | 日付フォーマット | {{ date | date:'yyyy/MM/dd' }} |
currency | 通貨表示 | {{ price | currency:'JPY' }} |
number | 数値フォーマット | {{ num | number:'1.2-2' }} |
uppercase | 大文字変換 | {{ str | uppercase }} |
lowercase | 小文字変換 | {{ str | lowercase }} |
titlecase | タイトルケース | {{ str | titlecase }} |
slice | 配列・文字列を切り取り | {{ items | slice:0:5 }} |
json | JSON文字列化(デバッグ) | {{ obj | json }} |
async | Observable/Promise 購読 | {{ data$ | async }} |
keyvalue | オブジェクトを key-value ペアに変換 | {{ map | keyvalue }} |
percent | パーセント表示 | {{ 0.75 | percent }} |
i18nPlural | 複数形対応 | {{ count | i18nPlural:map }} |
7.2 パイプのチェーン
<p>{{ name | uppercase | slice:0:10 }}</p>
<p>{{ today | date:'full' | uppercase }}</p>
7.3 カスタムパイプの作成
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true,
pure: true, // デフォルト: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength = 50, suffix = '…'): string {
if (value.length <= maxLength) return value;
return value.slice(0, maxLength) + suffix;
}
}
<!-- コンポーネントで imports: [TruncatePipe] が必要 -->
<p>{{ longText | truncate:30 }}</p>
<p>{{ longText | truncate:20:'...' }}</p>
7.4 Pure パイプ vs Impure パイプ
| Pure パイプ | Impure パイプ | |
|---|---|---|
| 実行タイミング | 入力値の参照が変わったときのみ | 毎変更検出サイクルで実行 |
| パフォーマンス | 高い | 低い(注意が必要) |
| ユースケース | 文字フォーマット、計算 | 配列フィルタリング(要素追加時など) |
| 設定 | pure: true(デフォルト) | pure: false |
@Pipe({ name: 'filter', standalone: true, pure: false })
export class FilterPipe implements PipeTransform {
transform(items: any[], keyword: string): any[] {
return items.filter(item => item.name.includes(keyword));
}
}
Impure パイプは変更検出ごとに実行されるため、大量データへの適用はパフォーマンス問題を招く恐れがあります。可能であれば、コンポーネント側でフィルタリングしたプロパティを用意する方が安全です。
Reactにはパイプに相当する組み込み機能はありません。通常はユーティリティ関数を定義して JSX 内で呼び出します。
// React: ユーティリティ関数で変換
function truncate(str: string, max = 50) {
return str.length > max ? str.slice(0, max) + '…' : str;
}
<p>{truncate(longText, 30)}</p>
まとめ:構文早見表
| 機能 | Angular | React |
|---|---|---|
| 補間 | {{ value }} | {value} |
| プロパティバインディング | [src]="url" | src={url} |
| イベントバインディング | (click)="fn()" | onClick={fn} |
| 双方向バインディング | [(ngModel)]="val" | useState + onChange |
| 条件分岐 | @if / @else | ? : / && |
| リスト | @for ... track | map() + key |
| 遅延ロード | @defer | React.lazy + Suspense |
| クラス制御 | [class.active]="bool" | className |
| スタイル制御 | [style.color]="val" | style={{ color: val }} |
| データ変換 | | pipe | ユーティリティ関数 |