Angular基礎 #3 データバインディングとイベント
前回は Angular コンポーネントの構造とテンプレート構文の基本を見てきました。クラスに宣言した値を {{ }} で画面に出力したりもしましたね。しかし実際のアプリは単に値を表示するだけでは終わりません。ユーザーがボタンを押し、入力欄に文字を打ち、それに応じて画面が更新されなければなりません。
今回は Angular がコンポーネントクラスとテンプレートの間でデータをやり取りする方法である データバインディング の 4 つの方式と、モダン Angular のリアクティブ状態管理ツールである Signals を一緒に見ていきます。
4 つのバインディング方式を一目で #
Angular のデータバインディングは「どの方向に流れるか」を基準に 4 つに分かれます。
| 種類 | 構文 | 流れ |
|---|---|---|
| Interpolation | {{ value }} | クラス → テンプレート |
| Property binding | [prop]="value" | クラス → テンプレート (DOM プロパティ) |
| Event binding | (event)="handler()" | テンプレート → クラス |
| Two-way binding | [(ngModel)]="value" | 双方向 |
構文に使われる括弧の形がそのまま流れを意味します。[ ] は入ってくる矢印 (左 ←)、( ) は出ていく矢印 (右 →)、そして 2 つを合わせた [( )] は双方向 (↔) です。形を見ただけでどちら側にデータが流れるのかが直感的に分かるように作られています。
ではひとつずつ詳しく見ていきます。
補間 (Interpolation) 復習 #
もっとも基本的なバインディング方式です。コンポーネントクラスの値をテンプレートにそのまま差し込みます。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>こんにちは、{{ name }}さん!</h1>
<p>10 年後には {{ age + 10 }} 歳になります。</p>
`,
})
export class AppComponent {
name = '太郎';
age = 30;
}{{ }} の中には式 (expression) であれば何でも入れることができます。算術演算もメソッド呼び出しも三項演算子も可能です。ただし代入文 (=) や if のような制御フロー文は入れることができません。
補間は結局テキストに変換されて画面に出力されます。HTML 属性に値を入れるときにも使えますが、属性を扱うときは次に出てくる Property binding の方が適している場合が多いです。
Property binding — 角括弧で DOM プロパティに値を渡す #
[属性名]="式" の形で DOM プロパティ (property) に値をバインディングします。
@Component({
selector: 'app-root',
standalone: true,
template: `
<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">送信</button>
<input [value]="defaultName" />
`,
})
export class AppComponent {
imageUrl = '/images/logo.svg';
imageAlt = 'ロゴ画像';
isLoading = true;
defaultName = '太郎';
}[src]="imageUrl" は imageUrl 変数の値を評価して src 属性に渡します。もしクオートの中に角括弧なしで src="{{ imageUrl }}" と書いても似たように動きますが、2 つの方式には微妙な違いがあります。
<!-- Interpolation: 結果が常に文字列になる -->
<button disabled="{{ isLoading }}">送信</button>
<!-- Property binding: 式の結果がそのまま渡される -->
<button [disabled]="isLoading">送信</button>disabled のように 真偽値、数値、オブジェクトなど文字列でない値 を扱うときは必ず property binding ([disabled]) を使う必要があります。補間はすべての値を文字列に変えるため disabled="false" という文字列が入ってしまい、意図と異なってボタンが無効化されるようなバグが発生しやすいです。
Event binding — 丸括弧でユーザー入力を受け取る #
これで反対方向、つまりユーザーの動作をコンポーネントクラスに受け取る方法です。(イベント名)="ハンドラ()" の形で使います。
@Component({
selector: 'app-root',
standalone: true,
template: `
<p>現在のカウント: {{ count }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">リセット</button>
`,
})
export class AppComponent {
count = 0;
increment() {
this.count = this.count + 1;
}
reset() {
this.count = 0;
}
}(click)="increment()" はボタンがクリックされるたびにコンポーネントクラスの increment() メソッドを呼び出すという意味です。click、input、keyup、submit など標準 DOM イベントはすべて同じ方式で使えます。
イベントオブジェクトが必要なら $event という特殊変数でアクセスします。
@Component({
selector: 'app-root',
standalone: true,
template: `
<input (input)="onInput($event)" placeholder="名前を入力してください" />
<p>入力値: {{ name }}</p>
`,
})
export class AppComponent {
name = '';
onInput(event: Event) {
const input = event.target as HTMLInputElement;
this.name = input.value;
}
}event.target を HTMLInputElement にキャストして .value を取り出して使うパターンです。毎回こう書くのが煩わしく感じられたら鋭いです — 次に出てくる双方向バインディングがまさにこの煩わしさを軽減してくれます。
Two-way binding — 双方向で一度に #
入力欄にユーザーが打った値が変数に入り、変数の値が画面にもそのまま見える — この 2 つの流れを一度に束ねるのが双方向バインディング ([(ngModel)]) です。角括弧と丸括弧を合わせた形がまさに「双方向」という意味を視覚的に見せています。この形のため一般に「バナナボックス (banana in a box)」と呼ぶこともあります。
ngModel は FormsModule に含まれているため、standalone コンポーネントでは imports 配列に追加する必要があります。
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name" placeholder="名前を入力してください" />
<p>こんにちは、{{ name }}さん!</p>
`,
})
export class AppComponent {
name = '';
}前の例と比較してみると (input) ハンドラも、event.target キャストもなくなりました。入力欄に文字を打つたびに name の値が自動的に更新され、更新された値がまた <p> タグに反映されます。
[(ngModel)]="name" は [ngModel]="name" (クラス → 入力欄) と (ngModelChange)="name = $event" (入力欄 → クラス) を同時に書いた省略形です。シグナルで単純なリアクティブ状態管理 #
ここまで見た例では一般のクラスプロパティ (count = 0) をそのまま使っていました。これでも問題なく動きます。しかしモダン Angular (v17+) では単純なリアクティブ状態に対して Signal というより明確なツールを提供します。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<p>現在のカウント: {{ count() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">リセット</button>
`,
})
export class AppComponent {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
reset() {
this.count.set(0);
}
}3 つの変わった点があります。
count = 0→count = signal(0): シグナルで包んだ状態になります。- テンプレートで
{{ count }}→{{ count() }}: シグナルは関数のように呼び出して値を取り出します。 - 値を変えるとき
this.count = ...の代わりにset()やupdate()を使います。
set(value) は新しい値で丸ごと差し替えるもので、update(prev => ...) は前の値を受け取って新しい値を作る関数型の更新です。カウンタのように前の値を基にして次の値を計算するときは update() の方が安全です。
シグナルが良い理由 #
「ただのプロパティを使っても画面はちゃんと更新されるのに、なぜシグナルを使うの?」という疑問が出るのも当然です。シグナルの利点は次のとおりです。
- 変更を明示的にする:
set()/update()でしか値を変えられないので、「どこで誰がこの値を変えたのか」を追跡しやすくなります。 - 変更検知が精密になる: Angular はどのシグナルがどこで使われているかを知っているので、シグナルが変わるとそのシグナルを使う部分だけを正確に再描画できます。大きなアプリでパフォーマンスに役立ちます。
- 計算された値 (
computed) と副作用 (effect) に拡張される: 単純な状態から始めて、同じモデルの上で派生値と効果を自然に表現できます。
新しいプロジェクトでは可能な限り最初からシグナルで始めることをお勧めします。この講座の以降の記事でも基本的にシグナルを使っていきます。
クラスバインディングとスタイルバインディング #
property binding の応用としてよく使われる 2 つをさらに見ていきます。CSS クラスを条件付きで付けたり、インラインスタイルを動的に変えたりするパターンです。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<p
[class.active]="isActive()"
[class.disabled]="isDisabled()"
[style.color]="textColor()"
[style.font-size.px]="fontSize()"
>
サンプルテキスト
</p>
<button (click)="toggle()">アクティブ切り替え</button>
`,
styles: [`
.active { font-weight: bold; }
.disabled { opacity: 0.4; }
`],
})
export class AppComponent {
isActive = signal(false);
isDisabled = signal(false);
textColor = signal('tomato');
fontSize = signal(16);
toggle() {
this.isActive.update(v => !v);
}
}[class.active]="isActive()": 式が truthy ならactiveクラスを追加、falsy なら削除します。[style.color]="textColor()":colorスタイルを動的に変えます。[style.font-size.px]="fontSize()": 単位 (px) をドットで繋げて書くと数値だけ渡しても自動的に単位が付きます。
複数のクラスやスタイルを一度に扱うにはオブジェクト形式も可能です。
<p [ngClass]="{ active: isActive(), disabled: isDisabled() }">テキスト</p>
<p [ngStyle]="{ color: textColor(), 'font-size': fontSize() + 'px' }">テキスト</p>[ngClass]/[ngStyle] は CommonModule に入っているディレクティブなので、standalone コンポーネントでは imports に追加する必要があります。単純なケースでは [class.x]/[style.x] の方が軽くて読みやすいのでよく使われます。
シグナル vs 一般プロパティ、いつ何を使うか? #
基準を単純化すれば次のようになります。
- シグナルを使う: 時間の経過とともに変わり、その変化を画面が追従しなければならない値。カウント、ロード状態、ユーザー名、トグル状態など。
- 一般プロパティでも十分: コンポーネント生成後に変わらない設定値、メソッド、定数。
最初は「変わりうるすべての状態はシグナル」と単純に覚えておいても良いです。慣れた後により精緻に区別すれば良いです。
整理: 4 つの形と流れ #
復習の意味でもう一度押さえておきましょう。
{{ value }} <!-- クラス → テンプレート (テキスト) -->
<img [src]="value" /> <!-- クラス → テンプレート (プロパティ) -->
<button (click)="fn()" /> <!-- テンプレート → クラス (イベント) -->
<input [(ngModel)]="value" /> <!-- 双方向 -->括弧の形がそのまま矢印の方向だということだけ覚えておけば、初めて見るコードもどちら側にデータが流れているのかが一目で読めるはずです。
まとめ #
今回の記事では Angular の 4 つのデータバインディング方式と、モダン Angular のリアクティブ状態管理ツールであるシグナルを見てきました。整理すると:
{{ }}は値をテキストとして出力[ ]はクラス → テンプレート (プロパティ、クラス、スタイル)( )はテンプレート → クラス (イベント)[( )]は双方向 (ngModelはFormsModuleが必要)signal()/set()/update()で明示的なリアクティブ状態管理
次回の「Angular基礎 #4 Directive と Pipe」では、テンプレートの表現力を一段階引き上げるツールである Directives (*ngIf、*ngFor、そして新構文の @if/@for) と、画面に表示される直前に値を変換してくれる Pipes を扱っていきます。