Angularコンポーネントの基礎
Angularのコンポーネントは、Reactコンポーネントと同様にUIの構成単位ですが、構造と仕組みが大きく異なります。
1. コンポーネントの基本構造
Angularのコンポーネントは TypeScriptクラス に @Component デコレータを付けたものです。
@Component デコレータ
import { Component } from '@angular/core';
@Component({
selector: 'app-hello', // HTMLタグとして使用する名前
templateUrl: './hello.component.html', // テンプレートファイル
styleUrl: './hello.component.scss', // スタイルファイル
})
export class HelloComponent {
name = 'Angular';
}
| プロパティ | 説明 |
|---|---|
selector | このコンポーネントをHTMLで使用するタグ名(例: <app-hello>) |
templateUrl | HTMLテンプレートファイルへのパス |
template | インラインでHTMLを記述する場合(templateUrl と排他) |
styleUrl | スタイルファイルへのパス(Angular 17+、単数形) |
styleUrls | 複数スタイルファイルを配列で指定 |
styles | インラインスタイルを配列で記述 |
standalone | true にするとNgModuleなしで使用可能(Angular 17以降はデフォルト) |
ファイル構成:3ファイル分割 vs JSX一体型
Angularはデフォルトで1コンポーネントを3ファイルに分割します。
hello/
├── hello.component.ts # ロジック(TypeScript)
├── hello.component.html # テンプレート(HTML)
└── hello.component.scss # スタイル(SCSS)
Reactでは1ファイル(JSX)に記述するのが一般的ですが、Angularではこの分離により関心の分離が明確になります。
// React: JSX で一体化
export function Hello() {
const name = 'React';
return <h1>Hello, {name}!</h1>;
}
<!-- Angular テンプレート (hello.component.html) -->
<h1>Hello, {{ name }}!</h1>
ng generate でコンポーネントを生成
# コンポーネントを生成(3ファイル + specファイルが作られる)
ng generate component hello
# または短縮形
ng g c hello
# サブディレクトリに生成
ng g c features/user-profile
# インラインテンプレートとインラインスタイルで生成
ng g c hello --inline-template --inline-style
# スペックファイルなしで生成
ng g c hello --skip-tests
2. テンプレートとインラインテンプレート
テンプレートの記述方法には「外部ファイル参照」と「インライン」の2種類があります。
templateUrl(外部ファイル)
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html', // 外部HTMLファイルを参照
styleUrl: './user-card.component.scss',
})
export class UserCardComponent { }
HTMLファイルが独立するため、エディタの補完やフォーマットが効きやすく、テンプレートが長い場合に適しています。
template(インライン)
@Component({
selector: 'app-badge',
template: `
<span class="badge">{{ label }}</span>
`,
styles: [`
.badge { padding: 4px 8px; border-radius: 4px; }
`],
})
export class BadgeComponent {
label = 'New';
}
シンプルな小コンポーネントではインラインが便利です。
| 機能 | Angular テンプレート | React JSX |
|---|---|---|
| 変数の埋め込み | {{ value }} | {value} |
| 条件分岐 | @if (cond) { } / *ngIf | {cond && <...>} |
| ループ | @for (item of items; track item.id) { } / *ngFor | {items.map(...)} |
| クラスバインディング | [class.active]="isActive" | className={isActive ? 'active' : ''} |
3. データバインディングの4つの形式
Angularには4種類のデータバインディングがあります。
3-1. 補間(Interpolation): {{ }}
テンプレートに値を表示します。
@Component({
selector: 'app-greeting',
template: `
<p>Hello, {{ username }}!</p>
<p>今日は{{ today | date:'yyyy/MM/dd' }}です</p>
<p>合計: {{ price * quantity }}円</p>
`,
})
export class GreetingComponent {
username = 'Taro';
today = new Date();
price = 1000;
quantity = 3;
}
3-2. プロパティバインディング: [property]="value"
DOM要素やコンポーネントのプロパティに値をバインドします(TypeScript → HTML)。
@Component({
selector: 'app-image',
template: `
<!-- DOM プロパティへのバインド -->
<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">送信</button>
<input [value]="inputValue" />
<!-- class と style の特殊バインディング -->
<div [class.highlight]="isSelected">選択中</div>
<p [style.color]="textColor">カラーテキスト</p>
`,
})
export class ImageComponent {
imageUrl = 'https://example.com/photo.jpg';
imageAlt = 'サンプル画像';
isLoading = false;
inputValue = '初期値';
isSelected = true;
textColor = 'blue';
}
// React: JSX属性に式を直接書く
<img src={imageUrl} alt={imageAlt} />
<button disabled={isLoading}>送信</button>
Angularの [src] はReactの src={...} に相当します。
3-3. イベントバインディング: (event)="handler()"
ユーザーのアクションに応じてメソッドを呼び出します(HTML → TypeScript)。
@Component({
selector: 'app-counter',
template: `
<p>カウント: {{ count }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<input (input)="onInput($event)" (keydown.enter)="submit()" />
`,
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log(value);
}
submit() {
console.log('送信');
}
}
// React: onXxx 属性でハンドラを渡す
<button onClick={increment}>+1</button>
<input onChange={e => console.log(e.target.value)} />
Angularの (click) はReactの onClick に相当します。
3-4. 双方向バインディング: [(ngModel)]="value"
フォーム入力など、値の読み書きを同時に行います。
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-text-input',
imports: [FormsModule], // スタンドアロンコンポーネントの場合
template: `
<input [(ngModel)]="message" />
<p>入力値: {{ message }}</p>
`,
})
export class TextInputComponent {
message = '';
}
[(ngModel)] は「バナナインボックス」記法と呼ばれます。[ngModel] はプロパティバインディング(表示)、(ngModelChange) はイベントバインディング(更新)を合わせたシンタックスシュガーです。
// React: useState + onChange で制御コンポーネントを実現
const [message, setMessage] = useState('');
<input value={message} onChange={e => setMessage(e.target.value)} />
4. 入力と出力(@Input / @Output)
@Input:親から子へデータを渡す
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
`,
})
export class UserCardComponent {
@Input() name!: string;
@Input() email!: string;
@Input({ required: true }) userId!: number; // 必須入力(Angular 16+)
@Input() role = 'user'; // デフォルト値あり
}
<app-user-card
[name]="user.name"
[email]="user.email"
[userId]="user.id"
role="admin"
/>
@Output:子から親へイベントを送る
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-confirm-dialog',
template: `
<div class="dialog">
<p>削除しますか?</p>
<button (click)="onConfirm()">はい</button>
<button (click)="onCancel()">キャンセル</button>
</div>
`,
})
export class ConfirmDialogComponent {
@Output() confirmed = new EventEmitter<void>();
@Output() cancelled = new EventEmitter<void>();
onConfirm() {
this.confirmed.emit();
}
onCancel() {
this.cancelled.emit();
}
}
<app-confirm-dialog
(confirmed)="deleteItem()"
(cancelled)="closeDialog()"
/>
| Angular | React |
|---|---|
@Input() value | props.value |
@Input({ required: true }) | TypeScript型で必須化 |
@Output() clicked = new EventEmitter() | onClicked?: () => void のprops |
this.clicked.emit(data) | props.onClicked(data) |
Signal Input / Output(Angular 17.1+)
Angular 17.1以降では、Signalベースの新しい入出力APIが利用可能です。
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div>
<h3>{{ name() }}</h3>
<p>{{ price() }}円</p>
<button (click)="buy()">購入</button>
</div>
`,
})
export class ProductCardComponent {
// signal input(読み取りはSignal関数として呼び出す)
name = input.required<string>();
price = input<number>(0);
// output関数(EventEmitter の代替)
purchased = output<{ name: string; price: number }>();
buy() {
this.purchased.emit({ name: this.name(), price: this.price() });
}
}
Signal Inputのメリット:
!の非nullアサーションが不要- テンプレート内で Signal として扱えるため、変更検知が最適化される
computed()などReactiveな操作が可能
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-price-display',
template: `<p>税込: {{ priceWithTax() }}円</p>`,
})
export class PriceDisplayComponent {
price = input<number>(0);
taxRate = input<number>(0.1);
// computed でSignalから派生値を計算
priceWithTax = computed(() => Math.floor(this.price() * (1 + this.taxRate())));
}
5. コンテンツ投影(Content Projection)
ng-content を使うと、コンポーネントの呼び出し側が「コンテンツ」を差し込めます。ReactのchildrenPropsに相当する機能です。
基本的なコンテンツ投影
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
styles: [`.card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }`],
})
export class CardComponent {}
<app-card>
<h2>タイトル</h2>
<p>ここが ng-content に投影されます。</p>
</app-card>
複数スロット(Named Content Projection)
select 属性でCSSセレクタを指定し、複数のスロットに振り分けることができます。
@Component({
selector: 'app-panel',
template: `
<div class="panel">
<div class="panel-header">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
<div class="panel-footer">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
`,
})
export class PanelComponent {}
<app-panel>
<h2 slot="header">パネルタイトル</h2>
<p>本文のコンテンツ</p>
<button slot="footer">閉じる</button>
</app-panel>
// React: children props
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
// React: 複数スロット(props として渡す)
function Panel({ header, children, footer }: PanelProps) {
return (
<div>
<div>{header}</div>
<div>{children}</div>
<div>{footer}</div>
</div>
);
}
6. スタイルのスコープ
Angularのコンポーネントスタイルはデフォルトでスコープが限定されており、他のコンポーネントに影響を与えません。この動作は ViewEncapsulation で制御します。
ViewEncapsulation の3モード
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-example',
template: `<p class="text">サンプル</p>`,
styles: [`.text { color: red; }`],
encapsulation: ViewEncapsulation.Emulated, // デフォルト
})
export class ExampleComponent {}
| モード | 説明 | 用途 |
|---|---|---|
ViewEncapsulation.Emulated(デフォルト) | Angular独自の属性(_ngcontent-xxx)をDOMに付与してCSSをスコープ化 | 通常の開発 |
ViewEncapsulation.ShadowDom | ブラウザネイティブのShadow DOMを使ってスコープ化 | 完全な隔離が必要な場合 |
ViewEncapsulation.None | スコープなし(グローバルCSSとして扱われる) | 意図的にグローバルスタイルを定義したい場合 |
Emulated モードの仕組み
<!-- Angular がレンダリングすると以下のようになる -->
<p _ngcontent-abc-c123 class="text">サンプル</p>
/* 生成されるCSS(スコープ付き) */
.text[_ngcontent-abc-c123] { color: red; }
:host セレクタ
コンポーネント自身のホスト要素(<app-example> タグ自体)にスタイルを適用するには :host セレクタを使います。
/* コンポーネントのホスト要素にスタイル適用 */
:host {
display: block;
padding: 16px;
}
:host(.active) {
background-color: #e0f7fa;
}
| Angular | React |
|---|---|
ViewEncapsulation.Emulated(デフォルト) | CSS Modules(import styles from './Component.module.css') |
ViewEncapsulation.None | グローバルCSS / createGlobalStyle (styled-components) |
ViewEncapsulation.ShadowDom | Web Components + Shadow DOM |
:host | コンポーネントのルート要素への直接スタイリング |
Reactでは明示的にCSS Modulesやstyled-componentsなどのツールを選択しますが、Angularはスコープ化がデフォルトで組み込まれています。