メインコンテンツまでスキップ

テンプレート構文とコントロールフロー

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 との比較
// 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 が必要)。

app.component.ts
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 との比較
// 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 との比較
// 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実際に遅延ロードするコンテンツ
@placeholderdefer 前(初期表示)に表示
@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: 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 との比較
// 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 との比較
// 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 }}
jsonJSON文字列化(デバッグ){{ obj | json }}
asyncObservable/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 カスタムパイプの作成

src/app/pipes/truncate.pipe.ts
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 との比較

Reactにはパイプに相当する組み込み機能はありません。通常はユーティリティ関数を定義して JSX 内で呼び出します。

// React: ユーティリティ関数で変換
function truncate(str: string, max = 50) {
return str.length > max ? str.slice(0, max) + '…' : str;
}
<p>{truncate(longText, 30)}</p>

まとめ:構文早見表

機能AngularReact
補間{{ value }}{value}
プロパティバインディング[src]="url"src={url}
イベントバインディング(click)="fn()"onClick={fn}
双方向バインディング[(ngModel)]="val"useState + onChange
条件分岐@if / @else? : / &&
リスト@for ... trackmap() + key
遅延ロード@deferReact.lazy + Suspense
クラス制御[class.active]="bool"className
スタイル制御[style.color]="val"style={{ color: val }}
データ変換| pipeユーティリティ関数