跳到主要内容

Angularコンポーネントの基礎

Angularのコンポーネントは、Reactコンポーネントと同様にUIの構成単位ですが、構造と仕組みが大きく異なります。

1. コンポーネントの基本構造

Angularのコンポーネントは TypeScriptクラス@Component デコレータを付けたものです。

@Component デコレータ

src/app/hello/hello.component.ts
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>
templateUrlHTMLテンプレートファイルへのパス
templateインラインでHTMLを記述する場合(templateUrl と排他)
styleUrlスタイルファイルへのパス(Angular 17+、単数形)
styleUrls複数スタイルファイルを配列で指定
stylesインラインスタイルを配列で記述
standalonetrue にすると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 との比較
// 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';
}

シンプルな小コンポーネントではインラインが便利です。

React JSX との記法比較
機能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 との比較
// 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 との比較
// 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 との比較
// React: useState + onChange で制御コンポーネントを実現
const [message, setMessage] = useState('');
<input value={message} onChange={e => setMessage(e.target.value)} />

4. 入力と出力(@Input / @Output)

@Input:親から子へデータを渡す

子コンポーネント: user-card.component.ts
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:子から親へイベントを送る

子コンポーネント: confirm-dialog.component.ts
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()"
/>
React との比較
AngularReact
@Input() valueprops.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に相当する機能です。

基本的なコンテンツ投影

card.component.ts
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セレクタを指定し、複数のスロットに振り分けることができます。

panel.component.ts
@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 との対比
// 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;
}
React の CSS Modules / styled-components との比較
AngularReact
ViewEncapsulation.Emulated(デフォルト)CSS Modules(import styles from './Component.module.css'
ViewEncapsulation.NoneグローバルCSS / createGlobalStyle (styled-components)
ViewEncapsulation.ShadowDomWeb Components + Shadow DOM
:hostコンポーネントのルート要素への直接スタイリング

Reactでは明示的にCSS Modulesやstyled-componentsなどのツールを選択しますが、Angularはスコープ化がデフォルトで組み込まれています。