はじめに
以前の記事でAngularの概要とAngular用語集を紹介しましたが、実際にAngularでアプリを作るには、それぞれの技術要素をもう少し踏み込んで理解しておく必要があります。
本記事はそのシリーズ第1弾として、Angularアプリの最も基本的な構成要素であるコンポーネント・ディレクティブ・パイプ・モジュールを、サンプルコードとともに詳しく解説します。
この記事で扱う技術要素
- Component(コンポーネント)
- Directive(ディレクティブ)
- Pipe(パイプ)
- NgModule / スタンドアロンコンポーネント
- ライフサイクルフック
Component(コンポーネント)とは
Angularアプリは、コンポーネントと呼ばれるUI部品を組み合わせて作られています。
1つのコンポーネントは、以下の3つの要素で構成されます。
- テンプレート: 画面の見た目(HTML)
- スタイル: 見た目の装飾(CSS)
- クラス: ロジック(TypeScript)
最もシンプルなコンポーネントを見てみましょう。
// hello.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-hello', // このコンポーネントをHTML上でどう呼び出すか
standalone: true, // スタンドアロンコンポーネントであることを示す(後述)
template: `
<h1>こんにちは、{{ name }}さん!</h1>
`,
styles: `
h1 {
color: #dd0031; /* Angularのブランドカラー */
}
`,
})
export class HelloComponent {
name = 'たろう';
}
このコンポーネントを、別のテンプレートの中で以下のように呼び出せます。
<!-- app.component.html -->
<app-hello></app-hello>
実際の表示: こんにちは、たろうさん!(赤色の見出しで表示される)
@Componentという部分に注目してください。
これはデコレータと呼ばれるTypeScriptの構文で、「このクラスはAngularのコンポーネントである」というメタデータ(付加情報)をクラスに付与しています。
デコレータがなければ、HelloComponentはただのTypeScriptクラスにすぎず、Angularはそれをコンポーネントとして認識できません。
テンプレートを別ファイルに分離する
上記の例ではtemplateプロパティにHTMLを直接文字列で書きましたが、実務では見やすさのために別ファイルに分離するのが一般的です。
@Component({
selector: 'app-hello',
standalone: true,
templateUrl: './hello.component.html', // 別ファイルを参照
styleUrl: './hello.component.css',
})
export class HelloComponent {
name = 'たろう';
}
<!-- hello.component.html -->
<h1>こんにちは、{{ name }}さん!</h1>
ng generate component(または短縮形のng g c)コマンドを使うと、この3ファイル構成(.ts・.html・.css)が自動生成されます。
ng generate component hello
Directive(ディレクティブ)とは
ディレクティブは、DOM要素の見た目や振る舞いを変更するための仕組みです。
実は、これまで見てきた「コンポーネント」も、テンプレートを持つ特別なディレクティブの一種にすぎません。
ディレクティブには3つの種類があります。
| 種類 | 役割 |
|---|---|
| Component Directive | テンプレートを持つディレクティブ(=普通のコンポーネント) |
| Structural Directive(構造ディレクティブ) | DOM要素の追加・削除など、構造そのものを変更する(*ngIf・*ngForなど) |
| Attribute Directive(属性ディレクティブ) | 既存の要素の見た目や振る舞いだけを変更する |
構造ディレクティブの詳細(および現在主流の@if・@for構文)は、次回のテンプレート構文編で扱うため、本記事では属性ディレクティブを自作する例を見てみます。
属性ディレクティブの自作例
「マウスホバーで背景色が変わる」ディレクティブを作ってみます。
// highlight.directive.ts
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]', // 属性として使えるようにする ([]で囲むのが慣習)
standalone: true,
})
export class HighlightDirective {
constructor(private el: ElementRef) {}
@HostListener('mouseenter') // マウスが要素に入った時のイベント
onMouseEnter() {
this.setBackgroundColor('yellow');
}
@HostListener('mouseleave') // マウスが要素から出た時のイベント
onMouseLeave() {
this.setBackgroundColor('');
}
private setBackgroundColor(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
<!-- 利用側 -->
<p appHighlight>この文章にマウスを乗せると、背景色が黄色に変わります。</p>
@HostListenerデコレータで、ディレクティブが付与された要素に対するDOMイベント(mouseenter・mouseleave)を購読しています。
このように、共通の見た目・振る舞いの変更処理を、コンポーネントのクラスに直接書かず、再利用可能な属性として切り出せるのがディレクティブの利点です。
Pipe(パイプ)とは
パイプは、テンプレート内で値を「変換して表示する」ための仕組みです。
|(パイプ)記号を使って、値をパイプに通すように記述します。
組み込みパイプの例
<p>{{ price | currency:'JPY' }}</p> <!-- ¥3,000 のように通貨表示 -->
<p>{{ today | date:'yyyy年MM月dd日' }}</p> <!-- 2026年09月04日 のように日付整形 -->
<p>{{ userName | uppercase }}</p> <!-- TAROU のように大文字化 -->
<p>{{ description | slice:0:10 }}</p> <!-- 先頭10文字だけ切り出し -->
Angularにはcurrency・date・uppercase・lowercase・slice・jsonなど、実務でよく使う変換処理があらかじめ組み込みパイプとして用意されています。
パイプを自作する例
「文字数が多い場合に省略記号を付ける」パイプを作ってみます。
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate', // テンプレートで使う名前
standalone: true,
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength: number = 20): string {
if (value.length <= maxLength) {
return value;
}
return value.slice(0, maxLength) + '...';
}
}
<!-- 利用側: 第1引数はvalue、コロン以降が第2引数(maxLength)に渡る -->
<p>{{ longText | truncate:15 }}</p>
入力: "Angularのパイプは値を変換して表示するための仕組みです"
出力: "Angularのパイプは値を変換..."
パイプはテンプレート内で完結する軽量な変換処理に向いています。
逆に、複雑な計算やAPI通信のような重い処理は、パイプではなくコンポーネントのクラス側やサービスに書くべきとされています(パイプは画面の再描画のたびに呼ばれる可能性があるため)。
NgModuleとスタンドアロンコンポーネント
ここまでの例では、すべてのコンポーネント・ディレクティブ・パイプにstandalone: trueを指定してきました。
これはスタンドアロンコンポーネントという仕組みで、Angular 19以降のデフォルトの構成方式です。
しかし、Angularを学んでいると、書籍やネット記事で以下のようなNgModuleベースの古い書き方を目にすることもまだ多いため、両方の違いを理解しておきましょう。
NgModuleベースの書き方(従来方式)
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
@NgModule({
declarations: [AppComponent, HelloComponent], // このモジュールに属するコンポーネント群を宣言
imports: [BrowserModule], // 他のモジュールを取り込む
bootstrap: [AppComponent], // アプリの起点となるコンポーネント
})
export class AppModule {}
この方式では、新しいコンポーネントを作るたびに、どこかのNgModuleのdeclarationsに登録する必要がありました。
プロジェクトが大きくなるにつれて、「このコンポーネントはどのモジュールに属しているのか」を把握するコストが上がっていくという課題がありました。
スタンドアロンコンポーネント(現在の主流)
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent); // NgModuleを介さず、直接コンポーネントを起動する
スタンドアロンコンポーネントでは、NgModuleへの登録が一切不要になります。
コンポーネント自身が「自分が必要とする他のコンポーネント・ディレクティブ・パイプ」を、importsプロパティで直接指定します。
import { Component } from '@angular/core';
import { HelloComponent } from './hello.component';
import { TruncatePipe } from './truncate.pipe';
@Component({
selector: 'app-root',
standalone: true,
imports: [HelloComponent, TruncatePipe], // このコンポーネントが使う部品を直接列挙する
template: `
<app-hello></app-hello>
<p>{{ description | truncate:15 }}</p>
`,
})
export class AppComponent {
description = 'スタンドアロンコンポーネントの解説記事です';
}
| 観点 | NgModule方式 | スタンドアロンコンポーネント |
|---|---|---|
| 登録場所 | モジュールのdeclarationsに一括登録 |
コンポーネント自身のimportsに個別指定 |
| ボイラープレート | 多い(モジュールファイルの管理が必要) | 少ない |
| ツリーシェイキング | モジュール単位になりがち | コンポーネント単位で効きやすい |
| 現在の位置づけ | 大規模な既存プロジェクトで現役 | Angular 19以降のデフォルト、新規プロジェクト推奨 |
新規にAngularを学ぶのであれば、まずはスタンドアロンコンポーネントの書き方を覚えれば十分です。
ただし、既存の大規模プロジェクトの保守に入る場合は、NgModuleベースのコードに出会う可能性も高いため、両方の存在を知っておくことが重要です。
コンポーネントのライフサイクル
コンポーネントは、生成されてから破棄されるまでの間に、いくつかの決まったタイミングで特定のメソッド(ライフサイクルフック)が呼び出されます。
代表的なものを紹介します。
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>経過秒数: {{ seconds }}</p>`,
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = 0;
private intervalId?: ReturnType<typeof setInterval>;
ngOnInit(): void {
// コンポーネントの初期化が完了した直後に1度だけ呼ばれる
console.log('TimerComponentが初期化されました');
this.intervalId = setInterval(() => this.seconds++, 1000);
}
ngOnDestroy(): void {
// コンポーネントが画面から破棄される直前に呼ばれる
console.log('TimerComponentが破棄されます');
clearInterval(this.intervalId); // タイマーの後始末を忘れずに行う
}
}
ngOnInit()は「初期データの取得」、ngOnDestroy()は「タイマーの停止やイベント購読の解除」など、リソースの後始末を行う場所として頻繁に使われます。
特にngOnDestroy()での後始末を忘れると、コンポーネントが画面から消えた後も裏でタイマーが動き続ける、といったメモリリークの原因になるため注意が必要です。
まとめ
| 用語 | 役割 |
|---|---|
| Component | テンプレート・スタイル・ロジックをセットにしたUI部品の最小単位 |
| Directive | DOM要素の構造・見た目・振る舞いを変更する仕組み(コンポーネントもその一種) |
| Pipe | テンプレート内で値を変換して表示する仕組み |
| NgModule | コンポーネント群をグループ化する従来の仕組み(現在は非推奨気味) |
| スタンドアロンコンポーネント | NgModule登録が不要な、現在主流のコンポーネント構成方式 |
| ライフサイクルフック | コンポーネントの生成〜破棄の各タイミングで呼ばれる特殊メソッド群 |
次回は、Angularのもう1つの中核機能であるSignals(シグナル)によるリアクティブな状態管理を解説します。
学習に使った書籍
本記事の内容整理にあたっても、以下の書籍を参考にしました。
- Angular 超入門 第2版 [ 掌田津耶乃 ]
- Angularアプリケーションプログラミング 開発の勘所と手順をしっかりマスター [ 山田祥寛 ]
- Angular Webアプリ開発スタートブック [ 大澤文孝 ]
※上記リンクはアフィリエイトリンクを含みます。