Angular をゼロから 100 の例で学ぶ: コンポーネント、テンプレート、シグナル、フォーム、ルーティング、HTTP、あらゆる種類のプロパティ

Angular をゼロから 100 の例で学ぶ: コンポーネント、テンプレート、シグナル、フォーム、ルーティング、HTTP、あらゆる種類のプロパティ

Angular は Google が提供する、TypeScript で大規模で構造化された Web アプリケーションを作るためのフレームワークです。この記事では、プロジェクトの作成から始めて 100 個の動く例まで進みます。コンポーネントとテンプレート、制御フローの構文、パイプ、入力と出力、コンポーネントが持てるあらゆる種類のプロパティ、ライフサイクルフック、依存性の注入、ルーティング、リアクティブフォームとテンプレート駆動フォーム、RxJS による HTTP、ディレクティブ、シグナル、テスト、デプロイを学びます。各例は短いコードです。TypeScript のコードは TypeScript 5.6 のパーサーで構文を確認し、テンプレートとビルドコマンドは Angular 19 のプロジェクトで使う形で示しています。

必要なもの

Node.js 20 以降と npm、Visual Studio Code などのコードエディター、ブラウザが必要です。npm install -g @angular/cli で Angular CLI をインストールしてください。HTML、CSS、JavaScript の基本を知っていると役立ちます。クラスと関数が何かを知っていると、TypeScript の例が理解しやすくなります。

例の使い方

TypeScript の例はスタンドアロンの Angular プロジェクトで、該当するコンポーネントまたはサービスのファイルに入れます。コマンドの例はプロジェクトのターミナルで実行し、HTML の例はコンポーネントの HTML ファイルに置くテンプレートです。

トピック 1: プロジェクトの準備と最初のコンポーネント

Angular アプリはコンポーネントの木です。CLI がプロジェクトを作り、各コンポーネントは、セレクターとテンプレートを与えるデコレーターを付けたクラスです。

プロジェクトの作成

ng new はスタンドアロンアプリ用のファイルでプロジェクトを作ります。つまりコンポーネントに NgModule は要りません。ng serve はライブリロード付きの開発サーバーを起動し、ng generate は新しいコンポーネント、サービス、パイプのファイルを作ります。

ブートストラップと設定

main.ts は、ルートコンポーネントとアプリケーション設定を渡して bootstrapApplication を呼び出します。設定では、ルーターや HTTP クライアントなどの provider を一か所にまとめます。

コンポーネント、セレクター、スタイル

@Component デコレーターは、HTML タグであるセレクター、テンプレート、スタイルを設定します。Angular は各コンポーネントの要素に一意の属性を付けるので、スタイルはそのコンポーネントだけに効きます。そのため、二つのコンポーネントが同じ card クラスを使っても安全です。

図解

Angular コンポーネントが画面に表示される流れ: テンプレートとクラスが一緒にコンパイルされ、Angular がコンポーネントの状態を保持し、シグナルと変更検知が変わった部分だけをビューに反映します
Angular コンポーネントが画面に表示される流れ: テンプレートとクラスが一緒にコンパイルされ、Angular がコンポーネントの状態を保持し、シグナルと変更検知が変わった部分だけをビューに反映します

例 1: CLI でプロジェクトを作る

Angular CLI をインストールし、スタンドアロンのアプリを生成します。

npm install -g @angular/cli
ng new blog --standalone --routing --style=css
cd blog
ng serve

例 2: コンポーネントを生成する

CLI が新しいコンポーネントのファイルを作ります。

ng generate component hello
# short form
ng g c hello --standalone

例 3: アプリケーションを起動する

アプリの設定とともにルートコンポーネントを起動します。

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

例 4: スタンドアロンコンポーネント

NgModule を必要としないコンポーネントです。

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: '<h1>Hello, Angular!</h1>',
})
export class AppComponent {}

例 5: 複数行のインラインテンプレート

バッククォートで複数行のテンプレートを書きます。

import { Component } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <section>
      <h2>Welcome</h2>
      <p>This template spans several lines.</p>
    </section>
  `,
})
export class CardComponent {}

例 6: コンポーネントのスタイル

このコンポーネントだけに適用されるスタイルです。

import { Component } from '@angular/core';

@Component({
  selector: 'app-badge',
  standalone: true,
  template: '<span class="badge">New</span>',
  styles: [`.badge { background: #3366ff; color: white; padding: 4px 8px; border-radius: 6px; }`],
})
export class BadgeComponent {}

例 7: providers を含むアプリ設定

ルーターなどのサービスを一か所に登録します。

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)],
};

例 8: index.html のルート要素

app-root は Angular がアプリを描画する場所です。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Blog</title>
  <base href="/">
</head>
<body>
  <app-root></app-root>
</body>
</html>

トピック 2: テンプレートとデータバインディング

テンプレートは、追加の構文を持つ HTML です。コンポーネントの値を表示し、ユーザーの操作を戻し、入力と値を同期させます。

補間と式

二重の波括弧は値を文字として表示します。式では計算ができ、三項演算子を使えます。安全なナビゲーション演算子 ?. を使えば、存在しないオブジェクトがあってもエラーになりません。

プロパティ、属性、クラスのバインディング

角括弧は [disabled] のように DOM のプロパティをバインドします。対応するプロパティがない HTML 属性、例えば aria のラベルには attr. を使った属性バインディングが必要です。クラスとスタイルのバインディングは、値に応じてクラスやインラインスタイルを追加します。

イベントバインディングと双方向バインディング

丸括弧は (click) などのイベントを受け取ります。[(ngModel)] による双方向バインディングは、フォームの項目とコンポーネントの値を一致させ、FormsModule が必要です。#box のようなテンプレート参照変数は、要素にテンプレートの他の部分から使える名前を付けます。

例 9: 補間

二重の波括弧で値を文字の中に表示します。

import { Component } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: '<p>Hello, {{ name }}! You have {{ count + 1 }} message(s).</p>',
})
export class GreetingComponent {
  name = 'Ada';
  count = 2;
}

例 10: テンプレート式

テンプレート内で計算、三項演算子、安全なナビゲーションを使います。

<p>{{ price * quantity }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>
<p>{{ user?.name ?? 'Guest' }}</p>

例 11: プロパティバインディング

コンポーネントの値で要素のプロパティを設定します。

<img [src]="photoUrl" [alt]="photoAlt" width="120">
<button [disabled]="saving">Save</button>

例 12: 属性バインディング

対応するプロパティがない HTML 属性を設定します。

<button [attr.aria-label]="closeLabel">X</button>
<td [attr.colspan]="columns">Total</td>

例 13: イベントバインディング

ユーザーがクリックしたときにメソッドを実行します。

import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Clicked {{ clicks }} times</p>
    <button (click)="increment()">Click me</button>
  `,
})
export class CounterComponent {
  clicks = 0;
  increment() {
    this.clicks++;
  }
}

例 14: ngModel による双方向バインディング

入力欄とコンポーネントの値を常に同期させます。

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-name-field',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="name">
    <p>You typed: {{ name }}</p>
  `,
})
export class NameFieldComponent {
  name = '';
}

例 15: クラスバインディング

値に応じて CSS クラスを追加・削除します。

<div [class.active]="isActive" [class.error]="hasError">Panel</div>
<div [ngClass]="{ 'big': size === 'lg', 'hidden': !visible }">Box</div>

例 16: スタイルバインディング

コンポーネントの値からインラインスタイルを設定します。

<p [style.color]="textColor" [style.font-size.px]="fontSize">Styled text</p>

例 17: テンプレート参照変数

要素に名前を付け、テンプレートの別の場所から使えるようにします。

<input #nameBox type="text">
<button (click)="greet(nameBox.value)">Greet</button>

トピック 3: 制御フロー: @if、@for、@switch、@defer

Angular 17 以降、テンプレートには条件と繰り返しのための組み込みブロックがあります。これらは古い構造ディレクティブに代わるもので、読みやすくなっています。

@if と @else

@if は条件が真のときだけブロックを表示します。@else if と @else は複数のブロックから一つを選びます。ブロックは入れ子にでき、例えば for の中に if を置けます。

track と @empty を付けた @for

@for は要素ごとにブロックを繰り返します。track は、どの要素がどれかを Angular に伝え、変わった行だけを描き直せるようにします。@empty は、リストが空のときにメッセージを表示します。track は必須です。

@switch と @defer

@switch は式の値でブロックを選び、@default を代わりに使えます。@defer はページの一部を後から読み込みます。例えば表示されたときや、ユーザーが操作したときです。それまでは @placeholder や @loading を表示します。

例 18: @if

条件が真のときだけブロックを表示します。

@if (items.length > 0) {
  <p>You have {{ items.length }} items.</p>
}

例 19: @if と @else

複数のブロックから一つを選びます。

@if (isLoggedIn) {
  <p>Welcome back!</p>
} @else if (isGuest) {
  <p>Browsing as a guest.</p>
} @else {
  <p>Please sign in.</p>
}

例 20: track を付けた @for

各要素を繰り返し、track で要素を識別します。

<ul>
  @for (fruit of fruits; track fruit.id) {
    <li>{{ fruit.name }}</li>
  }
</ul>

例 21: @for と @empty

リストが空のときにメッセージを表示します。

@for (post of posts; track post.id) {
  <article>{{ post.title }}</article>
} @empty {
  <p>No posts yet.</p>
}

例 22: @switch

式の値でブロックを選びます。

@switch (status) {
  @case ('draft') { <span>Draft</span> }
  @case ('published') { <span>Live</span> }
  @default { <span>Unknown</span> }
}

例 23: placeholder 付きの @defer

ページの一部を後から読み込み、先にプレースホルダーを表示します。

@defer (on viewport) {
  <app-heavy-chart />
} @placeholder {
  <p>Chart will load when visible.</p>
} @loading {
  <p>Loading chart...</p>
}

例 24: @let でローカル値を定義する

値に名前を付け、テンプレート内で何度も使います。

@let total = price * quantity;
<p>Total: {{ total }}</p>

トピック 4: パイプ

パイプはテンプレートの中で値を変換します。例えば日付や金額を整形します。コンポーネントの値は変わりません。

組み込みの整形パイプ

date、currency、number、uppercase、lowercase、titlecase、slice はテンプレートの中で値を整形します。引数はコロンの後に渡します。例えば date: 'longDate' や currency: 'USD' です。

json、keyvalue、async

json はオブジェクトを文字列として表示し、デバッグに便利です。keyvalue はオブジェクトのキーと値を順に処理します。async は Observable を購読して最新の値を表示し、コンポーネントが破棄されると自動で購読を解除します。

カスタムパイプ

@Pipe デコレーターを付け、transform を実装したクラスが、新しいパイプになります。同じ種類の値を多くの場所で整形するときに作ります。Angular が結果を再利用できるように、副作用のない作りにしてください。

例 25: 日付パイプ

日付を表示用に整形します。

<p>{{ today | date: 'longDate' }}</p>
<p>{{ today | date: 'shortTime' }}</p>

例 26: 通貨と数値のパイプ

金額や小数を整形します。

<p>{{ 1234.5 | currency: 'USD' }}</p>
<p>{{ 3.14159 | number: '1.2-2' }}</p>

例 27: 文字列のパイプ

テンプレート内で大文字小文字を変えたり、文字列を切り出したりします。

<p>{{ 'angular' | uppercase }}</p>
<p>{{ 'HELLO' | lowercase }}</p>
<p>{{ 'hello world' | titlecase }}</p>
<p>{{ 'Angular' | slice: 0 : 3 }}</p>

例 28: json と keyvalue のパイプ

デバッグ用にオブジェクトを表示し、キーと値を順に処理します。

<pre>{{ user | json }}</pre>
<ul>
  @for (item of settings | keyvalue; track item.key) {
    <li>{{ item.key }}: {{ item.value }}</li>
  }
</ul>

例 29: async パイプ

Observable を購読し、最新の値を表示します。

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable, of } from 'rxjs';

@Component({
  selector: 'app-clock',
  standalone: true,
  imports: [AsyncPipe],
  template: '<p>Value: {{ value$ | async }}</p>',
})
export class ClockComponent {
  value$: Observable<number> = of(42);
}

例 30: カスタムパイプ

文字列を短くする独自のパイプを作ります。

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'shorten', standalone: true })
export class ShortenPipe implements PipeTransform {
  transform(value: string, max = 20): string {
    return value.length > max ? value.slice(0, max) + '...' : value;
  }
}

トピック 5: コンポーネント同士の通信

データは入力を通して親から子へ流れ、イベントは出力を通して子から親へ流れます。親子ではないコンポーネントは、代わりにサービスを共有できます。

入力

input() は親が値を設定するプロパティを宣言します。既定値を与えられ、親に必ず値を設定させたいときは input.required を使い、属性名を変えたいときは alias を付けます。従来の @Input デコレーターも使えます。

出力と @Output

output() は、子が .emit() で発行するイベントを宣言します。親は (liked)='save()' のようなイベントバインディングで受け取ります。EventEmitter を使う従来の @Output も同じように動きます。

投影、クエリ、共有サービス

ng-content は親が子の中に自分の内容を置けるようにします。viewChild と contentChild は名前で要素やテンプレートを探します。Subject やシグナルを持つ共有サービスは、離れたコンポーネント同士の通信を可能にします。

例 31: input() のシグナル

親から値を受け取り、既定値を設定します。

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: '<p>Hello, {{ name() }}</p>',
})
export class UserCardComponent {
  name = input<string>('Guest');
}

例 32: 必須の input

親に値の指定を強制します。

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-price',
  standalone: true,
  template: '<p>{{ amount() }} THB</p>',
})
export class PriceComponent {
  amount = input.required<number>();
}

例 33: output() イベント

子から親へイベントを送ります。

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-like-button',
  standalone: true,
  template: '<button (click)="liked.emit()">Like</button>',
})
export class LikeButtonComponent {
  liked = output<void>();
}

例 34: @Output と EventEmitter

子コンポーネントからイベントを出す従来の方法です。

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: '<input (keyup.enter)="submit($event)">',
})
export class SearchBoxComponent {
  @Output() searched = new EventEmitter<string>();

  submit(event: Event) {
    this.searched.emit((event.target as HTMLInputElement).value);
  }
}

例 35: viewChild のシグナルクエリ

コンポーネントのテンプレート内の要素を参照します。

import { Component, ElementRef, viewChild } from '@angular/core';

@Component({
  selector: 'app-focus',
  standalone: true,
  template: '<input #box type="text">',
})
export class FocusComponent {
  box = viewChild<ElementRef<HTMLInputElement>>('box');

  focusBox() {
    this.box()?.nativeElement.focus();
  }
}

例 36: ng-content によるコンテンツ投影

親が子の中に自分の内容を配置できるようにします。

import { Component } from '@angular/core';

@Component({
  selector: 'app-panel',
  standalone: true,
  template: `
    <section class="panel">
      <ng-content></ng-content>
    </section>
  `,
})
export class PanelComponent {}

例 37: contentChild と ng-template

親からテンプレートを受け取り、各要素ごとに描画します。

import { Component, contentChild, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-list',
  standalone: true,
  imports: [],
  template: `
    @for (item of items; track item) {
      <ng-container [ngTemplateOutlet]="itemTpl()" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
    }
  `,
})
export class ListComponent {
  items = ['a', 'b'];
  itemTpl = contentChild<TemplateRef<unknown>>('itemTpl');
}

例 38: host binding と host listener

ホスト要素にバインドし、そのイベントを受け取ります。

import { Component, HostBinding, HostListener } from '@angular/core';

@Component({
  selector: 'app-hover-card',
  standalone: true,
  template: '<p>Hover me</p>',
})
export class HoverCardComponent {
  @HostBinding('class.highlighted') highlighted = false;

  @HostListener('mouseenter') onEnter() {
    this.highlighted = true;
  }

  @HostListener('mouseleave') onLeave() {
    this.highlighted = false;
  }
}

例 39: host メタデータ

デコレーターでホストのクラスと属性を設定します。

import { Component } from '@angular/core';

@Component({
  selector: 'app-tag',
  standalone: true,
  template: '<ng-content></ng-content>',
  host: {
    'role': 'note',
    '[class.small]': 'small',
  },
})
export class TagComponent {
  small = true;
}

例 40: メッセージを送る共有サービス

親子ではないコンポーネント同士でメッセージを送ります。

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MessageBus {
  private messages = new Subject<string>();
  messages$ = this.messages.asObservable();

  send(text: string) {
    this.messages.next(text);
  }
}

トピック 6: あらゆる種類のプロパティ

コンポーネントはいくつかの種類のプロパティを持ちます。各種類は、通常の値、読み取り専用の値、計算された値、リアクティブなシグナル、双方向の値など、異なる用途に合います。

通常、readonly、省略可能なプロパティ

通常のプロパティは、変更できる値を持つフィールドです。readonly は、設定した後に再代入されることを防ぎます。email? のようにプロパティに疑問符を付けると、値がないことがあるという意味になり、コードは undefined を扱う必要があります。

getter、setter、static プロパティ

getter はプロパティを読むときにコードを実行し、setter は書き込むときにコードを実行します。static プロパティは各インスタンスではなくクラス自体に属します。テーマ名のような共有の既定値に向いています。

シグナル: signal、computed、effect

signal() は値を保持し、値が変わると Angular に知らせます。count() のように関数として読みます。computed() はシグナルから値を導き、シグナルが変わると更新されます。effect() は、読み取っているシグナルが変わるたびにコードを実行します。

model()、alias、input.required

model() は双方向の入力です。子が値を設定でき、親の値もそれに従います。独自のコンポーネントにおける ngModel のようなものです。alias は入力に別の属性名を与えます。input.required は親に値の指定を強制します。

アロー関数のプロパティ

アロー関数を値として持つプロパティは、その関数をコールバックとして渡したときもコンポーネントの this を保ちます。アロー関数は作られた時点の this を捕まえるからです。

例 41: 既定値を持つクラスのプロパティ

開始値を持つ通常のプロパティです。

import { Component } from '@angular/core';

@Component({
  selector: 'app-profile',
  standalone: true,
  template: '<p>{{ username }} ({{ role }})</p>',
})
export class ProfileComponent {
  username = 'mali';
  role: string = 'editor';
}

例 42: readonly プロパティ

宣言時に一度だけ設定できるプロパティです。

import { Component } from '@angular/core';

@Component({
  selector: 'app-version',
  standalone: true,
  template: '<p>Version {{ version }}</p>',
})
export class VersionComponent {
  readonly version = '1.4.0';
}

例 43: ゲッターとセッター

読み書きのときにコードを実行するプロパティです。

import { Component } from '@angular/core';

@Component({
  selector: 'app-temperature',
  standalone: true,
  template: '<p>{{ fahrenheit }} F</p>',
})
export class TemperatureComponent {
  private celsius = 0;

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

例 44: set と update を持つシグナルのプロパティ

set や update で変更できるリアクティブな値です。

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter-signal',
  standalone: true,
  template: `
    <p>{{ count() }}</p>
    <button (click)="count.update(n => n + 1)">+1</button>
    <button (click)="count.set(0)">Reset</button>
  `,
})
export class CounterSignalComponent {
  count = signal(0);
}

例 45: computed プロパティ

シグナルから導かれ、自動で更新される値です。

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-cart',
  standalone: true,
  template: '<p>Total: {{ total() }}</p>',
})
export class CartComponent {
  prices = signal([120, 80, 50]);
  total = computed(() => this.prices().reduce((sum, p) => sum + p, 0));
}

例 46: effect

読み取っているシグナルが変わるたびにコードを実行します。

import { Component, effect, signal } from '@angular/core';

@Component({
  selector: 'app-theme',
  standalone: true,
  template: '<p>Theme: {{ theme() }}</p>',
})
export class ThemeComponent {
  theme = signal('light');

  constructor() {
    effect(() => {
      console.log('Theme changed to', this.theme());
    });
  }
}

例 47: model() による双方向入力

子コンポーネントも更新できる入力です。ngModel に似ています。

import { Component, model } from '@angular/core';

@Component({
  selector: 'app-toggle',
  standalone: true,
  template: '<button (click)="on.set(!on())">{{ on() ? "On" : "Off" }}</button>',
})
export class ToggleComponent {
  on = model(false);
}

例 48: alias を持つ input

テンプレートで別の属性名を使います。

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-avatar',
  standalone: true,
  template: '<img [alt]="label()">',
})
export class AvatarComponent {
  label = input('Profile picture', { alias: 'caption' });
}

例 49: 省略可能なプロパティ

値がないことがあり、疑問符で示すプロパティです。

interface Customer {
  id: number;
  name: string;
  email?: string;
}

const shopper: Customer = { id: 1, name: 'Ploy' };
console.log(shopper.email ?? 'no email');

例 50: static プロパティとアロー関数のプロパティ

クラス全体で共有される値と、this を保つ関数です。

import { Component } from '@angular/core';

@Component({
  selector: 'app-config',
  standalone: true,
  template: '<p>{{ appName }}</p>',
})
export class ConfigComponent {
  static readonly defaultTheme = 'light';
  appName = 'Blog';
  logName = () => console.log(this.appName);
}

トピック 7: ライフサイクルフック

Angular は、コンポーネントの生涯の決まった時点で特別なメソッドを呼び出します。作られたとき、入力が変わったとき、ビューが描画された後、削除される前などです。

ngOnInit と ngOnChanges

ngOnInit は、Angular が最初の入力を設定した後に一度だけ実行されるので、データを読み込む場所として適しています。ngOnChanges は入力が変わるたびに実行され、SimpleChanges で古い値と新しい値を受け取ります。

ngOnDestroy

ngOnDestroy は、コンポーネントが取り除かれる直前に実行されます。長時間動くストリームの購読を解除し、タイマーを止めるのに使います。メモリリークを防ぎます。

ngAfterViewInit と ngDoCheck

ngAfterViewInit は、コンポーネントのビューと子ビューが描画された後に実行されるので、ビューの要素が使えます。ngDoCheck は変更検知のたびに実行されるので、軽く保ちましょう。

例 51: ngOnInit

Angular が入力を設定した後、準備のコードを一度だけ実行します。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  standalone: true,
  template: '<p>{{ items.length }} tasks loaded</p>',
})
export class TodoListComponent implements OnInit {
  items: string[] = [];

  ngOnInit() {
    this.items = ['Write', 'Test', 'Ship'];
  }
}

例 52: ngOnChanges

親からの入力が変わったときに反応します。

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-badge-count',
  standalone: true,
  template: '<span>{{ count }}</span>',
})
export class BadgeCountComponent implements OnChanges {
  @Input() count = 0;

  ngOnChanges(changes: SimpleChanges) {
    if (changes['count']) {
      console.log('Count changed to', changes['count'].currentValue);
    }
  }
}

例 53: ngOnDestroy

コンポーネントが取り除かれるときに購読やタイマーを片付けます。

import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: '<p>Ticks: {{ ticks }}</p>',
})
export class TimerComponent implements OnInit, OnDestroy {
  ticks = 0;
  private sub?: Subscription;

  ngOnInit() {
    this.sub = interval(1000).subscribe(() => this.ticks++);
  }

  ngOnDestroy() {
    this.sub?.unsubscribe();
  }
}

例 54: ngAfterViewInit

テンプレートが描画された後で、ビューの要素を使います。

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'app-autofocus',
  standalone: true,
  template: '<input #field>',
})
export class AutofocusComponent implements AfterViewInit {
  @ViewChild('field') field!: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    this.field.nativeElement.focus();
  }
}

例 55: ngDoCheck

変更検知のたびに確認を行います。

import { Component, DoCheck } from '@angular/core';

@Component({
  selector: 'app-watcher',
  standalone: true,
  template: '<p>Checks run: {{ checks }}</p>',
})
export class WatcherComponent implements DoCheck {
  checks = 0;

  ngDoCheck() {
    this.checks++;
  }
}

トピック 8: 依存性の注入とサービス

サービスは、複数のコンポーネントが必要とするロジックやデータを保持します。Angular がサービスを作り、依存性の注入で渡すので、コンポーネントが自分用のコピーを作る必要はありません。

@Injectable と providedIn root

@Injectable({ providedIn: 'root' }) を付けたクラスは、アプリ全体で一度だけ作られます。コンポーネントは inject() またはコンストラクターの引数で受け取ります。サービスの中でシグナルを使うと、テンプレートが読める状態を保持できます。

InjectionToken と値の provider

トークンは、設定のオブジェクトのようなクラスではない値を識別します。provider で登録し、固定値なら useValue、クラスなら useClass、値を組み立てるなら useFactory を使います。

コンポーネント単位の provider

コンポーネントの providers 配列は、そのコンポーネントとその子に、サービスの専用インスタンスを与えます。コンポーネントを作るたびに状態をリセットしたいときに役立ちます。

例 56: inject() 関数

コンストラクターの引数を使わずにサービスを取得します。

import { Component, inject } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-items',
  standalone: true,
  template: '<p>{{ data.count() }} items</p>',
})
export class ItemsComponent {
  data = inject(DataService);
}

例 57: 注入可能なサービス

どのコンポーネントも受け取れる共有のクラスです。

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  private itemsSignal = signal<string[]>([]);
  readonly items = this.itemsSignal.asReadonly();

  add(item: string) {
    this.itemsSignal.update((list) => [...list, item]);
  }
}

例 58: InjectionToken

設定のように、クラスではない値を提供します。

import { InjectionToken } from '@angular/core';

export interface AppSettings {
  apiUrl: string;
  pageSize: number;
}

export const APP_SETTINGS = new InjectionToken<AppSettings>('app.settings');

例 59: useValue による provider

固定のオブジェクトをインジェクターに渡します。

import { ApplicationConfig } from '@angular/core';
import { APP_SETTINGS } from './app-settings.token';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_SETTINGS,
      useValue: { apiUrl: 'https://api.example.com', pageSize: 20 },
    },
  ],
};

例 60: useClass と useFactory

必要になったときにクラスを選ぶか、値を作ります。

import { ApplicationConfig, inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { ConsoleLogger } from './console-logger';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: LoggerService, useClass: ConsoleLogger },
    {
      provide: 'clock',
      useFactory: () => () => new Date().getFullYear(),
    },
  ],
};

例 61: コンポーネントごとの provider

一つのコンポーネントの木の中でだけ使う、専用のインスタンスを与えます。

import { Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';

@Component({
  selector: 'app-scoped-counter',
  standalone: true,
  template: '<p>{{ store.value() }}</p>',
  providers: [CounterStore],
})
export class ScopedCounterComponent {
  store = inject(CounterStore);
}

トピック 9: ルーティング

ルーターは URL のパスをコンポーネントに対応づけ、一致したページを router-outlet に表示します。ページは必要になったときだけ読み込むこともできます。

ルートとナビゲーションのリンク

Routes の配列は、'' や 'about' などのパスをコンポーネントに対応づけます。routerLink はページを再読み込みせずにリンクを作り、routerLinkActive は現在のページのリンクにクラスを付けます。** のワイルドカードルートは未知の URL を扱います。

パラメーターと遅延読み込み

'post/:id' のようなルートは id を渡し、ActivatedRoute から読み取れます。動的 import を使う loadComponent は、ユーザーがそのページを開いたときだけコードを読み込むので、最初のダウンロードが小さくなります。

ガード、リゾルバー、コードでのナビゲーション

CanActivate ガードは true か、リダイレクト先の UrlTree を返します。リゾルバーはページを開く前にデータを読み込みます。Router.navigate はコードからユーザーを移動させ、クエリパラメーターを渡せます。

例 62: ルート

URL のパスをコンポーネントに対応づけます。

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '**', redirectTo: '' },
];

例 63: routerLink と router-outlet

ページ間のリンクを作り、対応するページを表示します。

<nav>
  <a routerLink="/" routerLinkActive="active">Home</a>
  <a routerLink="/about" routerLinkActive="active">About</a>
</nav>
<router-outlet></router-outlet>

例 64: ルートパラメーター

URL から ID などの値を読み取ります。

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-post',
  standalone: true,
  template: '<p>Post id: {{ id }}</p>',
})
export class PostComponent {
  private route = inject(ActivatedRoute);
  id = this.route.snapshot.paramMap.get('id');
}

例 65: 遅延読み込みのルート

ユーザーがページを開いたときだけ、そのページのコードを読み込みます。

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/admin.component').then((m) => m.AdminComponent),
  },
];

例 66: CanActivate ガード

チェックの結果でルートを許可または拒否します。

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

例 67: リゾルバー

ページが開く前にデータを読み込みます。

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { PostService } from './post.service';

export const postResolver: ResolveFn<string> = (route) => {
  return inject(PostService).titleFor(route.paramMap.get('id') ?? '');
};

例 68: プログラムによるナビゲーション

コードから、クエリパラメーター付きで別のページへ移動します。

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: '<button (click)="finish()">Finish</button>',
})
export class CheckoutComponent {
  private router = inject(Router);

  finish() {
    this.router.navigate(['/thanks'], { queryParams: { ref: 'cart' } });
  }
}

トピック 10: フォーム

Angular には二つのフォームの書き方があります。リアクティブフォームはフォームをコードで作り、大きなフォームで普通に使われます。テンプレート駆動フォームは HTML の中で ngModel を使い、小さなフォームに向いています。

FormControl、FormGroup、FormBuilder

FormControl は一つの値を保持し、FormGroup は複数のコントロールをまとめます。FormBuilder はコードを減らし、nonNullable はリセット後も値を null ではなく文字列のままにします。

バリデーターとエラーメッセージ

Validators.required、minLength、min、max、email は組み込みです。コントロールが無効で、かつユーザーが触れた後にだけエラーを表示します。そうすると、入力前にエラーが見えてしまうことがありません。カスタムバリデーターは、エラーのオブジェクトまたは null を返す関数です。

FormArray と valueChanges

FormArray は、追加したり削除したりできるコントロールのリストです。valueChanges は値が変わるたびに新しい値を流す Observable で、ライブ検索や下書きの保存に便利です。

例 69: リアクティブな FormControl

値がコード側に保持されるフォームの項目です。

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-email-input',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<input [formControl]="email">',
})
export class EmailInputComponent {
  email = new FormControl('', { nonNullable: true });
}

例 70: FormBuilder による FormGroup

複数の項目を一つのリアクティブフォームにまとめます。

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<form [formGroup]="form"></form>',
})
export class SignupComponent {
  private fb = inject(FormBuilder);
  form = this.fb.group({
    name: [''],
    email: [''],
  });
}

例 71: 組み込みのバリデーター

required、minLength、min、max、email による検査です。

import { FormBuilder, Validators } from '@angular/forms';
import { inject } from '@angular/core';

const fb = inject(FormBuilder);
const form = fb.nonNullable.group({
  name: ['', [Validators.required, Validators.minLength(2)]],
  age: [18, [Validators.min(13), Validators.max(120)]],
  email: ['', Validators.email],
});

例 72: リアクティブフォームのテンプレート

FormGroup を HTML に結び付け、エラーを表示します。

<form [formGroup]="form" (ngSubmit)="save()">
  <input formControlName="name" placeholder="Name">
  @if (form.controls.name.invalid && form.controls.name.touched) {
    <p class="error">Name is required.</p>
  }
  <button [disabled]="form.invalid">Save</button>
</form>

例 73: FormArray

増減できるフォームコントロールのリストです。

import { FormArray, FormControl } from '@angular/forms';

const tags = new FormArray([
  new FormControl('angular'),
  new FormControl('typescript'),
]);

tags.push(new FormControl('rxjs'));
console.log(tags.length);

例 74: カスタムバリデーター

空白を禁止するなど、独自の規則を作ります。

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function noSpaces(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = String(control.value ?? '');
    return value.includes(' ') ? { noSpaces: true } : null;
  };
}

例 75: テンプレート駆動フォーム

テンプレートの中で ngModel を使ってフォームを作ります。

<form #form="ngForm" (ngSubmit)="submit(form.value)">
  <input name="title" ngModel required>
  <button [disabled]="form.invalid">Post</button>
</form>

例 76: valueChanges

フォームの値が変わるたびに反応します。

import { FormControl } from '@angular/forms';

const search = new FormControl('');

search.valueChanges.subscribe((text) => {
  console.log('Searching for', text);
});

トピック 11: HTTP、RxJS、非同期データ

HttpClient はリクエストを送り、Observable を返します。RxJS のオペレーターはそのストリームを変換し、async パイプは手動の購読をせずに結果を表示します。

HttpClient の設定

アプリケーション設定で provideHttpClient() を呼び出します。HttpClient の get と post は Observable を返しますが、購読されるまで何もしないので、テンプレートでは async パイプか toSignal を使います。

オペレーター: map、filter、switchMap

map は各値を変換し、filter は必要な値だけを残します。switchMap は新しい値が来ると、前のリクエストを取り消します。入力しながらの検索に適しています。入力が止まるのを待つには debounceTime を追加します。

エラーと再試行

catchError は失敗したリクエストを処理して、安全な値を返せます。retry は失敗する前にリクエストをもう一度試します。エラーのときにユーザーが何を見るかは必ず決めてください。空の画面は混乱を招きます。

例 77: provideHttpClient

アプリの設定で HttpClient を有効にします。

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
};

例 78: async パイプでのデータ取得

手動で購読せずに、HTTP から取得した一覧を表示します。

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @for (user of users$ | async; track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UsersComponent {
  private http = inject(HttpClient);
  users$: Observable<{ id: number; name: string }[]> =
    this.http.get<{ id: number; name: string }[]>('/api/users');
}

例 79: RxJS の map と filter

ストリームから必要な値だけを残し、変換します。

import { map, filter } from 'rxjs';

const prices$ = of(120, 35, 80);
prices$
  .pipe(
    filter((p) => p > 50),
    map((p) => p * 1.07),
  )
  .subscribe((total) => console.log(total.toFixed(2)));

例 80: ライブ検索のための switchMap

入力が止まるのを待ってから、古いリクエストを取り消します。

import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';

@Component({
  selector: 'app-live-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<input [formControl]="query">',
})
export class LiveSearchComponent {
  private http = inject(HttpClient);
  query = new FormControl('', { nonNullable: true });

  results$ = this.query.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((q) => this.http.get<string[]>('/api/search', { params: { q } })),
  );
}

例 81: catchError と retry

失敗したリクエストを再試行し、安全な値に切り替えます。

import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of, retry } from 'rxjs';

const http = inject(HttpClient);
const safeList$ = http.get<string[]>('/api/tags').pipe(
  retry(2),
  catchError(() => of([] as string[])),
);

例 82: HTTP POST

リクエスト本体を付けてデータをサーバーに送ります。

import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export class PostService {
  private http = inject(HttpClient);

  createPost(title: string) {
    return this.http.post<{ id: number }>('/api/posts', { title });
  }
}

トピック 12: ディレクティブ

ディレクティブは、要素の振る舞いや、要素が表示されるかどうかを変えます。属性ディレクティブは要素を変え、構造ディレクティブは要素を追加または削除します。

属性ディレクティブ

[appHighlight] のように角括弧内のセレクターを持つディレクティブは、HostListener でマウスのイベントに反応し、要素のスタイルを変えられます。ロジックは小さく保ち、複雑な振る舞いはコンポーネントに任せましょう。

構造ディレクティブと TemplateRef

構造ディレクティブは TemplateRef と ViewContainerRef を注入します。条件が真のとき createEmbeddedView でビューを作り、偽のときはビューを消します。Angular の *ngIf や @if も同じように動きます。

ディレクティブの入力の setter

入力の setter は、親が新しい値を渡すたびに実行されます。ngOnChanges を書かずに、値に直接反応したいとき、例えばビューを作り直すときに使います。

例 83: 属性ディレクティブ

要素の見た目を変えるディレクティブです。

import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  color = input('yellow', { alias: 'appHighlight' });
  private el = inject<ElementRef<HTMLElement>>(ElementRef);

  @HostListener('mouseenter') onEnter() {
    this.el.nativeElement.style.backgroundColor = this.color();
  }

  @HostListener('mouseleave') onLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

例 84: 構造ディレクティブ

ページから要素を追加・削除するディレクティブです。

import { Directive, inject, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true,
})
export class UnlessDirective {
  private template = inject<TemplateRef<unknown>>(TemplateRef);
  private view = inject(ViewContainerRef);

  @Input() set appUnless(condition: boolean) {
    this.view.clear();
    if (!condition) {
      this.view.createEmbeddedView(this.template);
    }
  }
}

例 85: input のセッター

親が新しい値を渡すたびにコードを実行します。

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-tag-list',
  standalone: true,
  template: '<p>{{ tags.join(", ") }}</p>',
})
export class TagListComponent {
  private _tags: string[] = [];

  @Input()
  set tags(value: string[]) {
    this._tags = value.map((t) => t.trim());
  }
  get tags(): string[] {
    return this._tags;
  }
}

トピック 13: 描画、シグナル、変更検知

Angular は変更検知によって、どこを描き直すかを決めます。シグナルと OnPush はこれを制御する手段で、大きなページを速く保ちます。

インデックス付きのリストと計算された値

@for の中では $index、$first、$last を使って各行の見た目を変えられます。computed のシグナルはリストを絞り込み、検索文字列やリストが変わるたびに結果を更新します。

OnPush の変更検知

ChangeDetectionStrategy.OnPush を使うと、Angular はコンポーネントを、入力が変わったとき、そのコンポーネントでイベントが起きたとき、または読んでいるシグナルが変わったときにだけ確認します。コンポーネントが多いページでは大きく速くなります。

zoneless と toSignal

zoneless のアプリは zone.js なしで動きます。そのためビューは、ブラウザのすべてのイベントではなく、シグナルとイベントによって更新されます。toSignal は Observable をシグナルに変えるので、購読せずにテンプレートで読めます。

例 86: @for の $index と $first

ループ内での各要素の位置を使います。

@for (step of steps; track step; let i = $index, first = $first, last = $last) {
  <li [class.first]="first" [class.last]="last">{{ i + 1 }}. {{ step }}</li>
}

例 87: computed による絞り込み

入力に合わせて更新される絞り込み済みの一覧を表示します。

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-fruit-filter',
  standalone: true,
  template: `
    <input (input)="query.set($any($event.target).value)">
    @for (fruit of shown(); track fruit) {
      <p>{{ fruit }}</p>
    }
  `,
})
export class FruitFilterComponent {
  fruits = ['apple', 'banana', 'cherry'];
  query = signal('');
  shown = computed(() =>
    this.fruits.filter((f) => f.includes(this.query().toLowerCase())),
  );
}

例 88: OnPush の変更検知

入力が変わったときだけコンポーネントを確認します。

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '<p>{{ price }} THB</p>',
})
export class PriceTagComponent {
  @Input() price = 0;
}

例 89: ゾーンレスの変更検知

zone.js を使わずに Angular を動かし、シグナルでビューを更新します。

import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [provideZonelessChangeDetection()],
};

例 90: toSignal

Observable をテンプレートで読めるシグナルに変えます。

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';

@Component({
  selector: 'app-seconds',
  standalone: true,
  template: '<p>Seconds: {{ seconds() }}</p>',
})
export class SecondsComponent {
  seconds = toSignal(interval(1000).pipe(map((n) => n + 1)), { initialValue: 0 });
}

トピック 14: テストとアニメーション

テストは、コンポーネントやサービスを単独で確認します。アニメーションは、状態が変わるときに要素を滑らかに変化させます。

TestBed とコンポーネントのテスト

TestBed はテストのモジュールでコンポーネントを作ります。detectChanges は変更検知を実行し、テストは nativeElement の文字を確かめます。一つのテストには一つの振る舞いだけを確かめさせると、失敗したときに原因が特定しやすくなります。

HttpTestingController と fakeAsync

provideHttpClientTesting は実際のネットワークを偽物に置き換えます。expectOne と flush でテストに応答を返せます。fakeAsync と tick は時間を進めるので、遅延する処理を待たずにテストできます。

アニメーションのトリガー

アニメーションのトリガーは、open や closed などの状態と、その間の遷移とタイミングを定義します。トリガーをテンプレートの値にバインドすると、値が変わったときに Angular が遷移を再生します。

例 91: TestBed によるコンポーネントテスト

テストでコンポーネントを作り、表示された文字を確かめます。

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({ imports: [CounterComponent] }).compileComponents();
    fixture = TestBed.createComponent(CounterComponent);
  });

  it('starts at zero', () => {
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('0');
  });
});

例 92: HttpTestingController

実サーバーを使わずに HTTP サービスをテストします。

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  it('loads users', () => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    const service = TestBed.inject(UserService);
    const http = TestBed.inject(HttpTestingController);

    service.users().subscribe((list) => expect(list.length).toBe(2));
    http.expectOne('/api/users').flush([{ id: 1 }, { id: 2 }]);
    http.verify();
  });
});

例 93: fakeAsync と tick

テストの中で時間を進め、遅延した処理を確かめます。

import { fakeAsync, tick } from '@angular/core/testing';

it('runs a delayed action', fakeAsync(() => {
  let done = false;
  setTimeout(() => (done = true), 500);
  tick(500);
  expect(done).toBeTrue();
}));

例 94: アニメーションのトリガー

状態が変わったときに要素をアニメーションさせます。

import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-drawer',
  standalone: true,
  animations: [
    trigger('slide', [
      state('closed', style({ height: '0px' })),
      state('open', style({ height: '200px' })),
      transition('closed <=> open', animate('300ms ease-in')),
    ]),
  ],
  template: '<div [@slide]="isOpen ? \'open\' : \'closed\'">Content</div>',
})
export class DrawerComponent {
  isOpen = false;
}

トピック 15: 設定、エラー、デプロイ

実際のアプリには、環境ごとの設定、予期しないエラーへの対策、公開できる本番ビルドが必要です。

環境ファイルと initializer

API のアドレスなどの設定は環境ファイルに保存し、開発と本番で値を変えます。provideAppInitializer は、設定の読み込みなどのコードを、最初のページが描画される前に実行します。

エラーと ErrorHandler

Angular は捕まえられなかったエラーを ErrorHandler に渡します。独自のクラスを提供して、ログに残したり報告サービスに送ったりすれば、予期しないエラーの扱いを一か所で決められます。

ビルド、テスト、デプロイ

ng build --configuration production は最適化されたファイルを dist/ に作ります。ng test --watch=false はビルドのパイプラインで単体テストを一度だけ実行します。静的ファイルは、シングルページアプリに対応した Web サーバーや静的ホスティングに置きます。

例 95: 環境設定

API のアドレスなどの設定を一つのファイルにまとめます。

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
};

例 96: カスタム ErrorHandler

予期しないエラーを一か所で報告します。

import { ErrorHandler, Injectable } from '@angular/core';

@Injectable()
export class ReportingErrorHandler implements ErrorHandler {
  handleError(error: unknown): void {
    console.error('Unexpected error:', error);
  }
}

例 97: provideAppInitializer

アプリが起動する前に設定を読み込みます。

import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { ConfigService } from './config.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => inject(ConfigService).load()),
  ],
};

例 98: スタンドアロンコンポーネントの imports

コンポーネントが使うディレクティブとパイプを列挙します。

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
import { HighlightDirective } from './highlight.directive';
import { ShortenPipe } from './shorten.pipe';

@Component({
  selector: 'app-note',
  standalone: true,
  imports: [NgClass, HighlightDirective, ShortenPipe],
  template: '<p [ngClass]="{ bold: true }" appHighlight="pink">{{ text | shorten: 10 }}</p>',
})
export class NoteComponent {
  text = 'A long note about Angular standalone components';
}

例 99: 操作をきっかけにする @defer

ユーザーがクリックした後にだけ、ページの一部を読み込みます。

<button #openBtn>Show comments</button>

@defer (on interaction(openBtn)) {
  <app-comments />
} @placeholder {
  <p>Comments will load after you click.</p>
}

例 100: ビルドとテストのコマンド

日常の作業で使う主な Angular CLI のコマンドです。

# development server
ng serve
# production build (outputs to dist/)
ng build --configuration production
# unit tests
ng test --watch=false

練習プロジェクト: タスクリスト

各節の考え方を使う小さなアプリを作ります。まず自分で書き、その後で例と比べてください。

1. スタンドアロンのプロジェクトを作り、タスクの配列をシグナルに保持する TaskList コンポーネントを作ります。

2. 必須のテキスト欄を持つフォームを追加し、(ngSubmit) にバインドしたメソッドで新しいタスクをリストに加えます。

3. @for と track でタスクを表示し、リストが空のときは @empty のメッセージを出します。

4. 未完了のタスクを数える computed のシグナルを追加し、テンプレートに表示します。

5. URL から ActivatedRoute で id を読み取るタスク詳細ページのルートを追加します。

6. 空のメッセージを確かめるコンポーネントテストを一つ書き、ng test --watch=false で実行します。

次のステップ

次は、ルーターを深く学び、シグナルや NgRx による状態管理、Angular SSR によるサーバーサイドレンダリング、Angular Material のようなコンポーネントライブラリを学んでください。公式ドキュメントの angular.dev が最良の参照先です。この記事の例は、Angular 17 以降で推奨される構文に従っています。

まとめ

Angular はコンポーネントからアプリを組み立てます。テンプレートはデータとイベントをバインドし、制御フローのブロックは内容を表示したり繰り返したりし、パイプは値を整形します。入力と出力は親と子をつなぎます。コンポーネントは通常のプロパティ、readonly な値、getter、シグナルに状態を持ちます。サービスは依存性の注入で共有され、ルーターは URL をページに対応づけ、フォームは入力を検証し、HttpClient と RxJS はデータを読み込みます。テスト、設定、本番ビルドで作業の流れが完成します。100 の例を覚える最もよい方法は、タスクリストのプロジェクトを作り、一度に一つずつ変えてみることです。