- Angular
Angular @if・@for・@switchの使い方|ngIfとの対応表
Angularの @if・@for・@switch とは、テンプレートの中に直接書ける「条件分岐・繰り返し・多分岐」の組み込み構文(ビルトイン制御フロー)です。 *ngIf・*ngFor・ngSwitch と同じことを、import なしで、JavaScript に近い見た目で書けます。@for では track(各行を見分けるキー)の指定が必須になり、0件のときの表示を @empty で書けるようになりました。
この記事では、Angular 22.2.1 でコードをビルドし、Chromium で表示・クリックした結果をそのまま載せています。*ngIf/else・*ngFor/trackBy・ngSwitch との新旧対応表、$index・$first・$last・$even・$count などの暗黙変数の一覧に加えて、track を書かないときのコンパイルエラー文と、track $index と track item.id で DOM の再利用がどう変わるかを、入力欄の値と DOM ノードの追跡で比べました。移行コマンド ng generate @angular/core:control-flow の実行ログと変換前後の差分、@if (x; as y) や @let との組み合わせも扱います。
確認環境: Linux(x64)/Node.js 24.21.0/Angular CLI 22.2.1・Angular 22.2.1/TypeScript 6.0.3/Playwright 1.56.0(Chromium)。出力中のパスは /home/taro/ に置き換えています。
新旧対応の早見表(ngIf・ngFor・ngSwitchと@if・@for・@switch)
| やりたいこと | 従来の書き方 | 組み込み構文 |
|---|---|---|
| 条件が真のときだけ表示 | <p *ngIf="ok">...</p> |
@if (ok) { <p>...</p> } |
| そうでなければ別の表示 | *ngIf="ok; else other" + <ng-template #other> |
@if (ok) { ... } @else { ... } |
| 条件を3つ以上に分ける | ng-template を入れ子にする |
@if (a) { ... } @else if (b) { ... } @else { ... } |
| 条件の結果に名前を付ける | *ngIf="user as u" |
@if (user; as u) { ... } |
| 繰り返し | *ngFor="let item of items" |
@for (item of items; track item.id) { ... } |
| 行を見分けるキー | trackBy: trackById(関数・省略可) |
track item.id(式・必須) |
| 0件のときの表示 | *ngIf="items.length === 0" を別に書く |
@for (...) { ... } @empty { ... } |
| 何番目か | let i = index |
$index(または let i = $index) |
| 最初・最後 | let f = first; let l = last |
$first・$last |
| 偶数・奇数 | let e = even; let o = odd |
$even・$odd |
| 件数 | let c = count |
$count |
| 多分岐 | [ngSwitch]="v" |
@switch (v) { ... } |
| 分岐の1つ | *ngSwitchCase="'a'" |
@case ('a') { ... } |
| どれにも当たらないとき | *ngSwitchDefault |
@default { ... } |
| テンプレート内の変数 | (対応する構文なし) | @let total = price * qty; |
| 必要な import | CommonModule または NgIf・NgFor など |
不要 |
公式 API リファレンスでは、NgIf と NgForOf に「v20.0 から非推奨。@if/@for ブロックを使うこと。将来のメジャーリリースで削除する意向」と記載されています。22.2.1 では従来の書き方もビルドでき、同じ表示になることを確認しました(後半の移行の章)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular @if・@for・@switchの使い方|ngIfとの対応表」
コピペで動く完成形:@if・@for・@switch・@letを1つのコンポーネントで
まず全体像です。次のファイルを src/app/cf/basic.ts に置き、App の imports に CfBasic を入れて <app-cf-basic /> と書けば動きます。CommonModule の import はありません。
import { Component, signal } from '@angular/core';
type Status = 'draft' | 'review' | 'published';
interface Task { id: number; title: string; done: boolean; }
@Component({
selector: 'app-cf-basic',
template: `
<section id="if">
@if (stock() > 10) {
<p>在庫あり({{ stock() }})</p>
} @else if (stock() > 0) {
<p>残りわずか({{ stock() }})</p>
} @else {
<p>入荷待ち</p>
}
<button id="dec" (click)="stock.update(n => Math.max(0, n - 6))">-6</button>
</section>
<ul id="for">
@for (task of tasks(); track task.id; let i = $index) {
<li>
{{ i + 1 }}/{{ $count }} {{ task.title }}
[first={{ $first }} last={{ $last }} even={{ $even }} odd={{ $odd }}]
</li>
} @empty {
<li>タスクはありません</li>
}
</ul>
<button id="clear" (click)="tasks.set([])">空にする</button>
<section id="switch">
@switch (status()) {
@case ('draft') { <span>下書き</span> }
@case ('review') { <span>確認中</span> }
@default { <span>公開済み</span> }
}
<button id="next" (click)="next()">次へ</button>
</section>
<section id="as">
@if (user(); as u) {
<p>{{ u.name }} さん({{ u.email }})</p>
} @else {
<p>ログインしていません</p>
}
<button id="login" (click)="user.set({ name: '佐藤', email: 'sato@example.com' })">ログイン</button>
</section>
<section id="let">
@let total = price() * qty();
@let taxed = Math.round(total * 1.1);
<p>小計 {{ total }} 円 / 税込 {{ taxed }} 円</p>
@if (total >= 3000) {
<p>送料無料</p>
}
<button id="qty" (click)="qty.update(q => q + 1)">+1</button>
</section>
`,
})
export class CfBasic {
protected readonly Math = Math;
protected readonly stock = signal(12);
protected readonly tasks = signal<Task[]>([
{ id: 1, title: '見積もり', done: true },
{ id: 2, title: '設計', done: false },
{ id: 3, title: 'レビュー', done: false },
]);
protected readonly status = signal<Status>('draft');
protected readonly user = signal<{ name: string; email: string } | null>(null);
protected readonly price = signal(1200);
protected readonly qty = signal(2);
protected next(): void {
const order: Status[] = ['draft', 'review', 'published'];
this.status.update(s => order[(order.indexOf(s) + 1) % order.length]);
}
}
Chromium で開き、ボタンを順に押しながら表示を読み取った結果です。
@if : 在庫あり(12)
@if : 残りわずか(6) ← 「-6」を1回クリック
@if : 入荷待ち ← もう1回クリック
@for : 1/3 見積もり [first=true last=false even=true odd=false]
@for : 2/3 設計 [first=false last=false even=false odd=true]
@for : 3/3 レビュー [first=false last=true even=true odd=false]
@empty : タスクはありません ← 「空にする」をクリック
@switch: 下書き
@switch: 確認中 ← 「次へ」をクリック
@switch: 公開済み ← もう1回クリック(@default)
as : ログインしていません
as : 佐藤 さん(sato@example.com) ← 「ログイン」をクリック
@let : 小計 2400 円 / 税込 2640 円
@let : 小計 3600 円 / 税込 3960 円 / 送料無料 ← 「+1」をクリック
以降の章で、1つずつ分けて説明します。
@ifで条件分岐する(@else if・@else・as)
基本の形
@if (stock() > 10) {
<p>在庫あり</p>
} @else if (stock() > 0) {
<p>残りわずか</p>
} @else {
<p>入荷待ち</p>
}
| 従来 | 組み込み構文 |
|---|---|
要素に *ngIf を付ける |
要素を @if (...) { } で囲む |
else には ng-template と参照名が必要 |
@else { } を続けて書く |
3分岐以上は ng-template の入れ子 |
@else if (...) { } を何個でも並べられる |
複数の要素をまとめるには ng-container が必要 |
波かっこの中に何個でも書ける |
公式ドキュメント(Control flow)には、@else if は何個でも、@else は1つだけ書けると説明されています。
条件の結果に名前を付ける(as)
@if (式; as 名前) と書くと、式の結果をブロックの中で使えます。「値があるときだけ表示し、その値を使う」 という場面で、同じ式を何度も書かずに済みます。
@if (user(); as u) {
<p>{{ u.name }} さん({{ u.email }})</p>
} @else {
<p>ログインしていません</p>
}
user() の型は「オブジェクトまたは null」ですが、ブロックの中の u は null ではない型として扱われるので、u.name をそのまま書けます。22.2.1 では @else if (stock(); as s) のように、@else if に as を付けた形もビルドできました。
@elseだけを書くとどうなるか
@if の直後以外に @else を置くと、次のコンパイルエラーになりました。
✘ [ERROR] NG5002: @else block can only be used after an @if or @else if block. [plugin angular-compiler]
/home/taro/shop-app/src/app/cf/basic.ts:50:23:
50 │ <section id="let"> @else { <p>x</p> }
╵ ~~~~~~~~~~~~~~~~~~
@forで繰り返す(track・暗黙変数・@empty)
trackは必須。書かないとコンパイルエラー
@for の基本形は @for (変数 of 配列; track キー) { ... } です。track を省くと、ビルドの時点で次のエラーになりました。
@for (m of members()) {
<li>{{ m.name }}</li>
}
✘ [ERROR] NG5002: @for loop must have a "track" expression [plugin angular-compiler]
/home/taro/shop-app/src/app/cf/track.ts:13:6:
13 │ @for (m of members()) {
╵ ~~~~~~~~~~~~~~~~~~~~~~~
直し方は、; track m.id のようにキーを足すだけです。
@for (m of members(); track m.id) {
<li>{{ m.name }}</li>
}
*ngFor の trackBy は「関数を渡す・省略できる」ものでしたが、track は「式を書く・省略できない」ものです。
暗黙変数の一覧($index・$first・$last・$even・$odd・$count)
@for のブロックの中では、次の変数を宣言なしで使えます。意味は公式ドキュメントの表、値は上の実行結果(3件のリスト)で確かめたものです。
| 変数 | 意味 | 3件のリストでの値(1行目/2行目/3行目) | *ngFor での名前 |
|---|---|---|---|
$index |
何番目か(0始まり) | 0/1/2 | index |
$count |
全体の件数 | 3/3/3 | count |
$first |
最初の行か | true/false/false | first |
$last |
最後の行か | false/false/true | last |
$even |
$index が偶数か |
true/false/true | even |
$odd |
$index が奇数か |
false/true/false | odd |
$even は「0始まりの番号が偶数」なので、見た目の1行目・3行目が true になります。しま模様を付けるときは、この点だけ気を付けてください。
let で別名を付けることもできます(カンマ区切りで複数書けます)。@for を入れ子にして、外側の番号を内側で使いたいときに便利です。
@for (task of tasks(); track task.id; let i = $index, last = $last) {
<li [class.last]="last">{{ i + 1 }}. {{ task.title }}</li>
}
@emptyで0件のときの表示を書く
@for のブロックの直後に @empty { ... } を続けると、配列が空のときだけ表示されます。
@for (task of tasks(); track task.id) {
<li>{{ task.title }}</li>
} @empty {
<li>タスクはありません</li>
}
上の実行結果のとおり、tasks.set([]) で空にすると「タスクはありません」に切り替わりました。@for の直後以外に @empty を置くと、次のエラーになります。
✘ [ERROR] NG5002: @empty block can only be used after an @for block. [plugin angular-compiler]
trackの選び方:$indexとitem.idでDOMの再利用はどう変わるか
track は、配列が変わったときに 「どの行の DOM をそのまま使い回すか」 を決めるキーです。3通りの track を同じ配列に対して並べ、違いを調べました。
比べるコード
各行に入力欄を置いています。入力欄に打った文字は DOM ノードが持っているので、ノードが使い回されたかどうかが、文字の位置でわかります。
// src/app/cf/track.ts
import { Component, signal } from '@angular/core';
interface Member { id: number; name: string; }
@Component({
selector: 'app-cf-track',
template: `
<button id="prepend" (click)="prepend()">先頭に追加</button>
<button id="reverse" (click)="reverse()">逆順にする</button>
<button id="reload" (click)="reload()">同じ内容で作り直す</button>
<h3>track $index</h3>
<ul id="by-index">
@for (m of members(); track $index) {
<li>{{ m.name }} <input /></li>
}
</ul>
<h3>track m.id</h3>
<ul id="by-id">
@for (m of members(); track m.id) {
<li>{{ m.name }} <input /></li>
}
</ul>
<h3>track m</h3>
<ul id="by-ref">
@for (m of members(); track m) {
<li>{{ m.name }} <input /></li>
}
</ul>
`,
})
export class CfTrack {
protected readonly members = signal<Member[]>([
{ id: 1, name: '佐藤' },
{ id: 2, name: '鈴木' },
{ id: 3, name: '高橋' },
]);
private nextId = 4;
protected prepend(): void {
this.members.update(list => [{ id: this.nextId++, name: '新規' + (this.nextId - 1) }, ...list]);
}
protected reverse(): void {
this.members.update(list => [...list].reverse());
}
protected reload(): void {
this.members.update(list => list.map(m => ({ ...m })));
}
}
実行結果
Playwright で、最初の3つの li に N1〜N3 の目印を付け、各リストの1行目(佐藤)の入力欄に「メモ」と入力してから、ボタンを順に押しました。かっこの中は「DOM ノードの目印:入力欄の値」で、(新)は目印を付けたあとに作られたノードです。「変更数」は MutationObserver で数えた、li の追加・取り外しとテキストの書き換えの回数です。
--- 初期状態(各リストの1行目の入力欄に「メモ」と入力)
by-index 佐藤(N1:メモ) 鈴木(N2) 高橋(N3)
by-id 佐藤(N1:メモ) 鈴木(N2) 高橋(N3)
by-ref 佐藤(N1:メモ) 鈴木(N2) 高橋(N3)
--- 先頭に追加した後
by-index 新規4(N1:メモ) 佐藤(N2) 鈴木(N3) 高橋(新)
by-id 新規4(新) 佐藤(N1:メモ) 鈴木(N2) 高橋(N3)
by-ref 新規4(新) 佐藤(N1:メモ) 鈴木(N2) 高橋(N3)
変更数 {"by-index":{"added":1,"removed":0,"text":4},"by-id":{"added":1,"removed":0,"text":1},"by-ref":{"added":1,"removed":0,"text":1}}
--- 逆順にした後
by-index 高橋(N1:メモ) 鈴木(N2) 佐藤(N3) 新規4(新)
by-id 高橋(N3) 鈴木(N2) 佐藤(N1:メモ) 新規4(新)
by-ref 高橋(N3) 鈴木(N2) 佐藤(N1:メモ) 新規4(新)
変更数 {"by-index":{"added":0,"removed":0,"text":4},"by-id":{"added":3,"removed":3,"text":0},"by-ref":{"added":3,"removed":3,"text":0}}
--- 同じ内容で配列を作り直した後
by-index 高橋(N1:メモ) 鈴木(N2) 佐藤(N3) 新規4(新)
by-id 高橋(N3) 鈴木(N2) 佐藤(N1:メモ) 新規4(新)
by-ref 高橋(新) 鈴木(新) 佐藤(新) 新規4(新)
変更数 {"by-index":{"added":0,"removed":0,"text":0},"by-id":{"added":0,"removed":0,"text":0},"by-ref":{"added":4,"removed":4,"text":4}}
最後の操作のとき、ブラウザーのコンソールに次の警告が出ました(track m のリストに対するものです)。
NG0956: The configured tracking expression (track by identity) caused re-creation of the entire collection of size 4. This is an expensive operation requiring destruction and subsequent creation of DOM nodes, directives, components etc. Please review the "track expression" and make sure that it uniquely identifies items in a collection. Find more at https://v22.angular.dev/errors/NG0956
結果の読み取り
| 操作 | track $index |
track m.id |
track m(オブジェクトそのもの) |
|---|---|---|---|
| 先頭に追加 | ノードは位置に固定。「メモ」が新規4の行に付いた。テキストを4か所書き換え、末尾にノードを1つ追加 | 佐藤のノードがそのまま下へ。「メモ」は佐藤に付いたまま。追加は新しい1行だけ | m.id と同じ |
| 逆順 | ノードは動かず、テキストだけ4か所書き換え。「メモ」は1行目(高橋)に残る | 既存ノードを並べ替え(3つを付け外し)。「メモ」は佐藤と一緒に移動 | m.id と同じ |
| 同じ内容で配列を作り直す(API の再取得に相当) | 変更なし | 変更なし | 全ノードを作り直し。「メモ」が消え、NG0956 の警告 |
track $indexは「何番目か」がキーなので、ノードは位置に固定されます。並びが変わると、入力中の値やフォーカスなど DOM 側の状態が別のデータの行に付きます。track m.idは「データの ID」がキーなので、ノードがデータと一緒に動きます。配列を作り直しても、ID が同じなら何も作り直しません。track mはオブジェクトの同一性がキーです。同じオブジェクトを並べ替えるだけならm.idと同じ動きですが、サーバーから取り直すなどでオブジェクトが新しくなると、全行を作り直します。
どれを選ぶかの判断表
| データの性質 | 選ぶ track |
理由 |
|---|---|---|
| ID などの一意な値がある | track item.id |
追加・並べ替え・再取得のどれでもノードを使い回せる |
| 文字列や数値の配列で、重複がない | track item |
値そのものが一意なキーになる |
| 追加・削除・並べ替えをしない固定のリスト | track $index |
キーを用意しなくてよい |
| 一意な値がなく、行に入力欄などの状態がある | ID を振ってから track item.id |
$index だと状態が別の行に付く |
| オブジェクトの配列を取り直すことがある | track item.id(track item は避ける) |
取り直すたびに全行を作り直すことになる |
公式ドキュメントも、一意な識別子(id や uuid)があればそれを使い、変化しない静的なコレクションには $index を使う、と案内しています。
@switchで多分岐する(@case・@default)
@switch (status()) {
@case ('draft') { <span>下書き</span> }
@case ('review') { <span>確認中</span> }
@default { <span>公開済み</span> }
}
| ポイント | 内容 |
|---|---|
| 比較の方法 | ===(厳密等価)で比べる |
| フォールスルー | しない。break に当たるものは書かなくてよい |
@default |
どの @case にも当たらないときに表示。省略すると何も表示しない |
| 複数の値で同じ表示 | @case を続けて並べる |
(上の3行は公式ドキュメントの説明、4行目は次のコードで確かめた内容です。)
複数の@caseをまとめる・書きもれを型で見つける
@case を続けて書くと、複数の値に同じブロックを割り当てられます。さらに @default never; と書くと、すべての値を @case で網羅しているかをコンパイル時に検査できます。
// src/app/cf/sw.ts
import { Component, signal } from '@angular/core';
type Status = 'draft' | 'review' | 'published';
@Component({
selector: 'app-cf-sw',
template: `
@let s = status();
@switch (s) {
@case ('draft') { <span>下書き</span> }
@case ('review')
@case ('published') { <span>確認中または公開済み</span> }
@default never;
}
`,
})
export class CfSw {
protected readonly status = signal<Status>('draft');
}
このコードはビルドが通り、画面には「下書き」と表示されました。ここから @case ('published') の行を消すと、次のエラーになります。
✘ [ERROR] TS2322: Type '"published"' is not assignable to type 'never'. [plugin angular-compiler]
/home/taro/shop-app/src/app/cf/sw.ts:9:13:
9 │ @switch (s) {
╵ ^
Status に値を足したのに @case を足し忘れた、という書きもれをビルドで見つけられます。 なお、@switch (status()) のようにシグナルの呼び出しを直接書いた場合は、すべての @case を書いていても Type 'Status' is not assignable to type 'never' のエラーになりました。上のコードのように @let でいったん変数に受けてから @switch に渡すと、期待どおりに検査されます。
@letと組み合わせる
@let 名前 = 式; は、テンプレートの中で変数を宣言する構文です。同じ計算を何度も書かずに済み、@if や @for の条件にもそのまま使えます。
@let total = price() * qty();
@let taxed = Math.round(total * 1.1);
<p>小計 {{ total }} 円 / 税込 {{ taxed }} 円</p>
@if (total >= 3000) {
<p>送料無料</p>
}
実行結果では、qty を 2 から 3 に増やすと「小計 3600 円 / 税込 3960 円」に変わり、「送料無料」が表示されました。元の値が変われば、@let の値も自動で計算し直されます。
| 使いどころ | 例 |
|---|---|
| 計算結果を複数の場所で使う | @let total = price() * qty(); |
| シグナルの値を1回だけ読んで使い回す | @let s = status(); |
@switch の網羅チェックと組み合わせる |
@let s = status(); @switch (s) { ... @default never; } |
@for の中で行ごとの計算に名前を付ける |
@let subtotal = item.price * item.qty; |
@let は宣言より後ろでしか使えません。先に使うと、次のエラーになりました。
✘ [ERROR] NG8016: Cannot read @let declaration 'taxed' before it has been defined. [plugin angular-compiler]
ngIf・ngForから移行する(移行コマンドの実行ログと差分)
変換前のコード
CommonModule を使った従来の書き方のコンポーネントを用意しました。
// src/app/legacy/order-list.ts(変換前)
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
interface Order { id: number; item: string; status: 'new' | 'shipped' | 'done'; }
@Component({
selector: 'app-order-list',
imports: [CommonModule],
templateUrl: './order-list.html',
})
export class OrderList {
loading = false;
orders: Order[] = [
{ id: 101, item: 'ノート', status: 'new' },
{ id: 102, item: 'ペン', status: 'shipped' },
{ id: 103, item: '付箋', status: 'done' },
];
trackById(index: number, order: Order): number {
return order.id;
}
}
<!-- src/app/legacy/order-list.html(変換前) -->
<div *ngIf="loading; else loaded">読み込み中...</div>
<ng-template #loaded>
<p *ngIf="orders.length === 0">注文はありません</p>
<ul>
<li *ngFor="let order of orders; trackBy: trackById; let i = index; let last = last"
[class.last]="last">
{{ i + 1 }}. {{ order.item }}
<span [ngSwitch]="order.status">
<em *ngSwitchCase="'new'">受付</em>
<em *ngSwitchCase="'shipped'">発送済み</em>
<em *ngSwitchDefault>完了</em>
</span>
</li>
</ul>
</ng-template>
この状態で 22.2.1 でもビルドでき、画面には「1. ノート 受付」「2. ペン 発送済み」「3. 付箋 完了」と表示されました。
移行コマンドを実行する
ng generate @angular/core:control-flow --path=src/app/legacy
UPDATE src/app/legacy/order-list.html (629 bytes)
UPDATE src/app/legacy/order-list.ts (527 bytes)
| オプション | 既定値 | 働き |
|---|---|---|
--path |
./ |
変換する範囲(プロジェクトルートからの相対パス) |
--format |
有効 | テンプレートを整形し直す。元の整形を残したいときは無効にする |
--dry-run(-d) |
false | 書き換えずに結果だけ表示する |
(--path と --dry-run は --help の出力、--format の既定は公式ドキュメント「Migration to Control Flow syntax」の説明です。)
変換後のテンプレート
@if (loading) {
<div>読み込み中...</div>
} @else {
@if (orders.length === 0) {
<p>注文はありません</p>
}
<ul>
@for (order of orders; track trackById(i, order); let i = $index; let last = $last) {
<li [class.last]="last">
{{ i + 1 }}. {{ order.item }}
<span>
@switch (order.status) {
@case ('new') {
<em>受付</em>
}
@case ('shipped') {
<em>発送済み</em>
}
@default {
<em>完了</em>
}
}
</span>
</li>
}
</ul>
}
TypeScript 側の差分は次のとおりでした。
-import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
-interface Order { id: number; item: string; status: 'new' | 'shipped' | 'done'; }
+interface Order {
+ id: number;
+ item: string;
+ status: 'new' | 'shipped' | 'done';
+}
@Component({
selector: 'app-order-list',
- imports: [CommonModule],
+ imports: [],
templateUrl: './order-list.html',
})
| 変換前 | 変換後 | コメント |
|---|---|---|
*ngIf="loading; else loaded" + ng-template #loaded |
@if (loading) { } @else { } |
ng-template と参照名がなくなった |
trackBy: trackById |
track trackById(i, order) |
関数を「渡す」から「呼び出す」に変わった |
let i = index; let last = last |
let i = $index; let last = $last |
$ 付きの暗黙変数に置き換え |
[ngSwitch]・*ngSwitchCase・*ngSwitchDefault |
@switch・@case・@default |
span は残り、中身がブロックになった |
imports: [CommonModule] |
imports: [] |
import 文も削除された |
1行の interface |
複数行に整形 | TypeScript ファイルも整形された |
変換後もビルドは通り、表示は変換前と同じ「1. ノート 受付」「2. ペン 発送済み」「3. 付箋 完了」でした。
変換後に手で整えるところ
移行コマンドは動きを変えないことを優先するので、track trackById(i, order) のように関数呼び出しが残ります。関数の中身が return order.id; だけなら、次のように直すと読みやすくなります(trackById メソッドも不要になります)。
<!-- 直す前(移行コマンドの出力) -->
@for (order of orders; track trackById(i, order); let i = $index; let last = $last) {
<!-- 直した後 -->
@for (order of orders; track order.id; let i = $index; let last = $last) {
@if (orders.length === 0) と @for が分かれている部分は、@empty にまとめられます。
ビルドサイズの変化
同じプロジェクトで、移行の前後に ng build(production 設定)を実行した結果です。
| Raw size | Estimated transfer size | |
|---|---|---|
変換前(CommonModule を import) |
225.57 kB | 61.39 kB |
| 変換後(import なし) | 211.19 kB | 58.12 kB |
このプロジェクトでは CommonModule を使っていたのがこの1コンポーネントだけだったので、変換によって Raw size が 14.38 kB 小さくなりました。
公式ドキュメントは、@for と *ngFor の違いとして「オブジェクトの参照が同じまま、track に使っているプロパティの値が変わったとき、@for は要素を作り直さずにバインディングを更新する」点を挙げています。移行後は、リストの画面を一度操作して動きを見ておくと安心です。
コンパイルエラーと警告の早見表
すべて 22.2.1 で実際に出したメッセージです。
| メッセージ | 出したときの書き方 | 直し方 |
|---|---|---|
NG5002: @for loop must have a "track" expression |
@for (m of members()) { } |
; track m.id を足す |
NG5002: @else block can only be used after an @if or @else if block. |
@if の直後以外に @else |
@if のブロックの直後に続けて書く |
NG5002: @empty block can only be used after an @for block. |
@for の直後以外に @empty |
@for のブロックの直後に続けて書く |
NG5002: Unexpected closing block. The block may have been closed earlier. If you meant to write the `}` character, you should use the "}" HTML entity instead. |
テキストに閉じ波かっこをそのまま書いた | 文字として表示したい閉じ波かっこは } と書く |
NG8016: Cannot read @let declaration 'taxed' before it has been defined. |
@let より前で変数を使った |
宣言を使う場所より前に移す |
NG8015: Cannot assign to @let declaration 'n'. |
(click)="n = 2" のように @let の変数へ代入した |
@let は読み取り専用。値を変えたいときは元のシグナルを更新する |
TS2322: Type '"published"' is not assignable to type 'never'. |
@default never; で @case が足りない |
足りない値の @case を足す |
TS2339: Property 'trackById' does not exist on type 'CfTrack'. |
track trackById と書いたがメソッドがない |
track m.id のように式で書く |
NG0956: The configured tracking expression (track by identity) caused re-creation of the entire collection ...(実行時の警告) |
track m で配列を作り直した |
track m.id にする |
メールアドレスのように文中に @ が入るテキスト(info@example.com、@sato まで という文で試しました)は、そのまま書いてもビルドが通りました。
よくある質問
Q. *ngIf や *ngFor は今も使えますか?
A. 22.2.1 では、CommonModule を import した従来の書き方もビルドでき、同じ表示になりました。一方、公式 API リファレンスでは NgIf と NgForOf が v20.0 から非推奨とされ、@if・@for を使うよう案内されています。新しく書くテンプレートは組み込み構文にし、既存のものは移行コマンドで順に置き換えるのがおすすめです。
Q. track には何を書けばよいですか?
A. ID のような一意な値があれば track item.id です。実行結果のとおり、追加・並べ替え・配列の作り直しのどの操作でも、DOM ノードがデータと一緒に保たれました。固定のリストなら track $index でも構いません。
Q. @for で $index と let i = $index はどちらを使いますか?
A. どちらでも同じ値です。@for が1つなら $index をそのまま使えば十分です。入れ子にして外側の番号を内側で使うときは、let で別名を付けます。
Q. @if と @let はどう使い分けますか?
A. 「値があるときだけ表示したい」なら @if (x; as y)、「表示はいつもするが、計算結果に名前を付けたい」なら @let です。@if の as は条件が真のブロックの中だけで使え、@let は宣言したあとの同じ階層とその内側で使えます。
まとめ
@if・@for・@switchは import なしで使える組み込み構文。*ngIf・*ngFor・ngSwitchと1対1で置き換えられます。@forはtrackが必須。書かないとNG5002: @for loop must have a "track" expressionでビルドが止まります。track item.idを基本にする。track $indexは並びが変わると入力中の値が別の行に付き、track itemは配列を作り直すと全行を作り直すことを確認しました。- 暗黙変数は
$index・$count・$first・$last・$even・$odd。0件の表示は@emptyで書けます。 @switchはフォールスルーなし。@caseを続けて並べれば複数の値をまとめられ、@default never;で書きもれをビルドで見つけられます。@if (x; as y)と@letで、同じ式を繰り返さずに書けます。- 移行は
ng generate @angular/core:control-flow。--pathで範囲を絞って少しずつ進められます。
参考・出典
- Angular 公式ドキュメント「Control flow」(angular.dev/guide/templates/control-flow)
- Angular 公式ドキュメント「Migration to Control Flow syntax」(angular.dev/reference/migrations/control-flow)
- Angular 公式 API リファレンス「NgIf」「NgForOf」(angular.dev/api/common/NgIf、angular.dev/api/common/NgForOf)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular @if・@for・@switchの使い方|ngIfとの対応表」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。