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 クラスを使っても安全です。
図解
例 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 の例を覚える最もよい方法は、タスクリストのプロジェクトを作り、一度に一つずつ変えてみることです。