Angular
TypeScriptベースのWebアプリケーションフレームワーク。Googleが開発し、大規模企業向けの包括的な機能セット、強力な型安全性、RxJSによるリアクティブプログラミングを提供。
GitHub概要
angular/angular
Deliver web apps with confidence 🚀
スター98,317
ウォッチ2,999
フォーク26,424
作成日:2014年9月18日
言語:TypeScript
ライセンス:MIT License
トピックス
angularjavascriptpwatypescriptwebweb-frameworkweb-performance
スター履歴
データ取得日時: 2025/7/19 08:06
フレームワーク
Angular
概要
Angularは、Googleが開発・保守している、モダンなWebアプリケーション構築のためのフルスタックフレームワークです。
詳細
Angular(アンギュラー)は、TypeScriptをベースとしたオープンソースのWebアプリケーションフレームワークです。2016年にAngularJS(1.x)の後継として生まれ変わり、コンポーネントベースアーキテクチャを採用しています。フルスタックフレームワークとして、ルーティング、フォーム、HTTP通信、テスト、国際化など、Webアプリケーション開発に必要なすべての機能を標準で提供します。シングルページアプリケーション(SPA)の構築に特化しており、エンタープライズレベルの大規模アプリケーション開発に適しています。依存性注入(DI)システムによりコードの保守性と拡張性を確保し、Angular CLIによる開発環境の自動化、Server-Side Rendering(SSR)やPWA対応など、現代のWeb開発に必要な機能を包括的にサポートしています。Googleの内部プロダクトでも使用されており、高い品質と安定性を誇ります。
メリット・デメリット
メリット
- フルスタックフレームワーク: 開発に必要なすべての機能を標準で提供
- TypeScript標準: 型安全性による大規模開発での品質確保
- 企業サポート: Googleによる継続的な開発・保守とLTSサポート
- 強力なCLI: プロジェクト生成、ビルド、テスト、デプロイを自動化
- 優秀なツール: Angular DevToolsやLanguage Serviceによる開発体験
- エンタープライズ向け: 大規模チーム開発に適した設計思想
- PWA対応: プログレッシブWebアプリの構築をサポート
- SSR/SSG対応: サーバーサイドレンダリングと静的サイト生成
デメリット
- 学習コスト: フレームワークが大きく、習得すべき概念が多い
- 複雑性: 小規模プロジェクトには過度に複雑な場合がある
- バンドルサイズ: 最小構成でも他のライブラリより大きい
- 頻繁な更新: メジャーアップデートが年2回あり、追従が必要
- 設定の複雑さ: 初期設定や高度なカスタマイズが複雑
主要リンク
書き方の例
Hello World
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<h1>Hello, Angular!</h1>`,
styles: [`
h1 {
color: #1976d2;
font-family: Arial, sans-serif;
}
`]
})
export class AppComponent {
title = 'Hello World';
}
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent);
コンポーネントとプロパティバインディング
// welcome.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome-container">
<h2>こんにちは、{{ name }}さん!</h2>
<p>あなたは{{ age }}歳で、{{ department }}部門に所属しています。</p>
<p *ngIf="isAdmin">管理者権限があります。</p>
</div>
`,
styles: [`
.welcome-container {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
margin: 10px 0;
}
`]
})
export class WelcomeComponent {
@Input() name: string = '';
@Input() age: number = 0;
@Input() department: string = '';
@Input() isAdmin: boolean = false;
}
// app.component.ts
import { Component } from '@angular/core';
import { WelcomeComponent } from './welcome.component';
@Component({
selector: 'app-root',
imports: [WelcomeComponent],
template: `
<app-welcome
name="太郎"
[age]="28"
department="開発"
[isAdmin]="true">
</app-welcome>
<app-welcome
name="花子"
[age]="32"
department="営業"
[isAdmin]="false">
</app-welcome>
`
})
export class AppComponent {}
サービスと依存性注入
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
}
// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
imports: [CommonModule],
template: `
<div class="user-list">
<h2>ユーザー一覧</h2>
<div *ngIf="loading">読み込み中...</div>
<div *ngIf="error" class="error">エラー: {{ error }}</div>
<ul *ngIf="!loading && !error">
<li *ngFor="let user of users" class="user-item">
<strong>{{ user.name }}</strong> - {{ user.email }}
</li>
</ul>
</div>
`,
styles: [`
.user-list { padding: 20px; }
.error { color: red; }
.user-item { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
`]
})
export class UserListComponent implements OnInit {
users: User[] = [];
loading = false;
error: string | null = null;
constructor(private userService: UserService) {}
ngOnInit() {
this.loadUsers();
}
private loadUsers() {
this.loading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = err.message;
this.loading = false;
}
});
}
}
ディレクティブ(*ngFor, *ngIf)
// todo-list.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
interface Todo {
id: number;
text: string;
completed: boolean;
}
@Component({
selector: 'app-todo-list',
imports: [CommonModule, FormsModule],
template: `
<div class="todo-app">
<h2>TODOリスト</h2>
<!-- 新しいTODO追加フォーム -->
<div class="add-todo">
<input
[(ngModel)]="newTodoText"
placeholder="新しいTODOを入力"
(keyup.enter)="addTodo()">
<button (click)="addTodo()" [disabled]="!newTodoText.trim()">
追加
</button>
</div>
<!-- フィルター -->
<div class="filters">
<button
*ngFor="let filter of filters"
[class.active]="currentFilter === filter.value"
(click)="currentFilter = filter.value">
{{ filter.label }}
</button>
</div>
<!-- TODOリスト -->
<ul class="todo-list" *ngIf="filteredTodos.length > 0; else noTodos">
<li
*ngFor="let todo of filteredTodos; trackBy: trackByTodo"
[class.completed]="todo.completed"
class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)">
<span class="todo-text">{{ todo.text }}</span>
<button (click)="deleteTodo(todo.id)" class="delete-btn">削除</button>
</li>
</ul>
<ng-template #noTodos>
<p *ngIf="todos.length === 0">TODOがありません</p>
<p *ngIf="todos.length > 0 && filteredTodos.length === 0">
該当するTODOがありません
</p>
</ng-template>
<!-- 統計情報 -->
<div class="stats" *ngIf="todos.length > 0">
<p>
完了: {{ completedCount }} / {{ todos.length }}
({{ completionPercentage }}%)
</p>
</div>
</div>
`,
styles: [`
.todo-app { max-width: 600px; margin: 0 auto; padding: 20px; }
.add-todo { margin-bottom: 20px; }
.add-todo input { padding: 8px; margin-right: 10px; width: 300px; }
.filters button { margin-right: 10px; padding: 5px 10px; }
.filters button.active { background-color: #007bff; color: white; }
.todo-list { list-style: none; padding: 0; }
.todo-item {
display: flex; align-items: center; padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .todo-text {
text-decoration: line-through; color: #888;
}
.todo-text { flex: 1; margin: 0 10px; }
.delete-btn { background: #dc3545; color: white; border: none; padding: 5px 10px; }
.stats { margin-top: 20px; font-weight: bold; }
`]
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, text: 'Angular学習', completed: false },
{ id: 2, text: '買い物', completed: true },
{ id: 3, text: '運動', completed: false }
];
newTodoText = '';
currentFilter: 'all' | 'active' | 'completed' = 'all';
filters = [
{ value: 'all' as const, label: 'すべて' },
{ value: 'active' as const, label: '未完了' },
{ value: 'completed' as const, label: '完了済み' }
];
get filteredTodos(): Todo[] {
switch (this.currentFilter) {
case 'active':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}
get completedCount(): number {
return this.todos.filter(todo => todo.completed).length;
}
get completionPercentage(): number {
return Math.round((this.completedCount / this.todos.length) * 100);
}
addTodo() {
if (this.newTodoText.trim()) {
const newTodo: Todo = {
id: Date.now(),
text: this.newTodoText.trim(),
completed: false
};
this.todos.push(newTodo);
this.newTodoText = '';
}
}
toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
deleteTodo(id: number) {
this.todos = this.todos.filter(t => t.id !== id);
}
trackByTodo(index: number, todo: Todo): number {
return todo.id;
}
}
フォーム処理(リアクティブフォーム)
// contact-form.component.ts
import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-contact-form',
imports: [ReactiveFormsModule, CommonModule],
template: `
<div class="form-container">
<h2>お問い合わせフォーム</h2>
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
<!-- 名前 -->
<div class="form-group">
<label for="name">お名前 *</label>
<input
id="name"
type="text"
formControlName="name"
[class.error]="isFieldInvalid('name')">
<div *ngIf="isFieldInvalid('name')" class="error-message">
<span *ngIf="contactForm.get('name')?.errors?.['required']">
名前は必須です
</span>
<span *ngIf="contactForm.get('name')?.errors?.['minlength']">
名前は2文字以上で入力してください
</span>
</div>
</div>
<!-- メールアドレス -->
<div class="form-group">
<label for="email">メールアドレス *</label>
<input
id="email"
type="email"
formControlName="email"
[class.error]="isFieldInvalid('email')">
<div *ngIf="isFieldInvalid('email')" class="error-message">
<span *ngIf="contactForm.get('email')?.errors?.['required']">
メールアドレスは必須です
</span>
<span *ngIf="contactForm.get('email')?.errors?.['email']">
正しいメールアドレスを入力してください
</span>
</div>
</div>
<!-- 電話番号 -->
<div class="form-group">
<label for="phone">電話番号</label>
<input
id="phone"
type="tel"
formControlName="phone"
[class.error]="isFieldInvalid('phone')">
<div *ngIf="isFieldInvalid('phone')" class="error-message">
<span *ngIf="contactForm.get('phone')?.errors?.['pattern']">
正しい電話番号を入力してください(例:090-1234-5678)
</span>
</div>
</div>
<!-- カテゴリ -->
<div class="form-group">
<label for="category">お問い合わせ種別 *</label>
<select
id="category"
formControlName="category"
[class.error]="isFieldInvalid('category')">
<option value="">選択してください</option>
<option value="general">一般的なお問い合わせ</option>
<option value="support">サポート</option>
<option value="business">商談・提案</option>
<option value="other">その他</option>
</select>
<div *ngIf="isFieldInvalid('category')" class="error-message">
お問い合わせ種別を選択してください
</div>
</div>
<!-- メッセージ -->
<div class="form-group">
<label for="message">メッセージ *</label>
<textarea
id="message"
formControlName="message"
rows="5"
[class.error]="isFieldInvalid('message')">
</textarea>
<div *ngIf="isFieldInvalid('message')" class="error-message">
<span *ngIf="contactForm.get('message')?.errors?.['required']">
メッセージは必須です
</span>
<span *ngIf="contactForm.get('message')?.errors?.['minlength']">
メッセージは10文字以上で入力してください
</span>
</div>
</div>
<!-- 同意チェック -->
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
formControlName="agree"
[class.error]="isFieldInvalid('agree')">
プライバシーポリシーに同意します *
</label>
<div *ngIf="isFieldInvalid('agree')" class="error-message">
プライバシーポリシーへの同意が必要です
</div>
</div>
<button
type="submit"
[disabled]="contactForm.invalid || isSubmitting"
class="submit-btn">
{{ isSubmitting ? '送信中...' : '送信' }}
</button>
</form>
<!-- 送信完了メッセージ -->
<div *ngIf="isSubmitted" class="success-message">
お問い合わせを受け付けました。ありがとうございます。
</div>
</div>
`,
styles: [`
.form-container { max-width: 600px; margin: 0 auto; padding: 20px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select, textarea {
width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;
}
input.error, select.error, textarea.error { border-color: #dc3545; }
.error-message { color: #dc3545; font-size: 0.875em; margin-top: 5px; }
.checkbox-label { display: flex; align-items: center; }
.checkbox-label input { width: auto; margin-right: 8px; }
.submit-btn {
background: #007bff; color: white; padding: 12px 24px;
border: none; border-radius: 4px; cursor: pointer;
}
.submit-btn:disabled { background: #6c757d; cursor: not-allowed; }
.success-message {
background: #d4edda; color: #155724; padding: 12px;
border-radius: 4px; margin-top: 20px;
}
`]
})
export class ContactFormComponent implements OnInit {
contactForm!: FormGroup;
isSubmitting = false;
isSubmitted = false;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.contactForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
phone: ['', [Validators.pattern(/^[0-9-]+$/)]],
category: ['', Validators.required],
message: ['', [Validators.required, Validators.minLength(10)]],
agree: [false, Validators.requiredTrue]
});
}
isFieldInvalid(fieldName: string): boolean {
const field = this.contactForm.get(fieldName);
return !!(field && field.invalid && (field.dirty || field.touched));
}
async onSubmit() {
if (this.contactForm.valid) {
this.isSubmitting = true;
try {
// APIへの送信をシミュレート
await this.simulateApiCall(this.contactForm.value);
this.isSubmitted = true;
this.contactForm.reset();
} catch (error) {
console.error('送信エラー:', error);
alert('送信に失敗しました。もう一度お試しください。');
} finally {
this.isSubmitting = false;
}
} else {
// すべてのフィールドをtouchedにしてエラーを表示
Object.keys(this.contactForm.controls).forEach(key => {
this.contactForm.get(key)?.markAsTouched();
});
}
}
private simulateApiCall(data: any): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
console.log('送信データ:', data);
resolve();
}, 2000);
});
}
}
HTTPクライアント
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
// 他のプロバイダー
]
};
// api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
export interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
export interface Post {
id: number;
title: string;
content: string;
authorId: number;
createdAt: string;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly baseUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
// GET リクエスト
getPosts(): Observable<ApiResponse<Post[]>> {
return this.http.get<ApiResponse<Post[]>>(`${this.baseUrl}/posts`)
.pipe(
retry(3),
catchError(this.handleError)
);
}
// パラメータ付きGET
getPostsByAuthor(authorId: number, page: number = 1): Observable<ApiResponse<Post[]>> {
const params = new HttpParams()
.set('authorId', authorId.toString())
.set('page', page.toString())
.set('limit', '10');
return this.http.get<ApiResponse<Post[]>>(`${this.baseUrl}/posts`, { params })
.pipe(
catchError(this.handleError)
);
}
// POST リクエスト
createPost(post: Omit<Post, 'id' | 'createdAt'>): Observable<ApiResponse<Post>> {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
});
return this.http.post<ApiResponse<Post>>(`${this.baseUrl}/posts`, post, { headers })
.pipe(
catchError(this.handleError)
);
}
// PUT リクエスト
updatePost(id: number, post: Partial<Post>): Observable<ApiResponse<Post>> {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
});
return this.http.put<ApiResponse<Post>>(`${this.baseUrl}/posts/${id}`, post, { headers })
.pipe(
catchError(this.handleError)
);
}
// DELETE リクエスト
deletePost(id: number): Observable<ApiResponse<void>> {
const headers = new HttpHeaders({
'Authorization': `Bearer ${this.getAuthToken()}`
});
return this.http.delete<ApiResponse<void>>(`${this.baseUrl}/posts/${id}`, { headers })
.pipe(
catchError(this.handleError)
);
}
// ファイルアップロード
uploadFile(file: File): Observable<ApiResponse<{ url: string }>> {
const formData = new FormData();
formData.append('file', file);
const headers = new HttpHeaders({
'Authorization': `Bearer ${this.getAuthToken()}`
});
return this.http.post<ApiResponse<{ url: string }>>(
`${this.baseUrl}/upload`,
formData,
{ headers }
).pipe(
catchError(this.handleError)
);
}
private getAuthToken(): string {
return localStorage.getItem('authToken') || '';
}
private handleError(error: any): Observable<never> {
let errorMessage = 'サーバーエラーが発生しました';
if (error.error instanceof ErrorEvent) {
// クライアントサイドエラー
errorMessage = `エラー: ${error.error.message}`;
} else {
// サーバーサイドエラー
switch (error.status) {
case 400:
errorMessage = 'リクエストが正しくありません';
break;
case 401:
errorMessage = '認証が必要です';
break;
case 403:
errorMessage = 'アクセス権限がありません';
break;
case 404:
errorMessage = 'リソースが見つかりません';
break;
case 500:
errorMessage = 'サーバー内部エラーです';
break;
default:
errorMessage = `エラーコード: ${error.status}`;
}
}
console.error('HTTP Error:', error);
return throwError(() => new Error(errorMessage));
}
}
// post-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ApiService, Post } from './api.service';
import { Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-post-list',
imports: [CommonModule],
template: `
<div class="post-list-container">
<h2>投稿一覧</h2>
<button (click)="loadPosts()" [disabled]="loading">
{{ loading ? '読み込み中...' : '再読み込み' }}
</button>
<div *ngIf="error" class="error">
{{ error }}
<button (click)="loadPosts()">再試行</button>
</div>
<div *ngIf="loading" class="loading">
データを読み込んでいます...
</div>
<div *ngIf="!loading && posts.length > 0" class="posts">
<article *ngFor="let post of posts" class="post-item">
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
<div class="post-meta">
作成者ID: {{ post.authorId }} |
作成日: {{ post.createdAt | date:'yyyy/MM/dd HH:mm' }}
</div>
<div class="post-actions">
<button (click)="editPost(post)">編集</button>
<button (click)="deletePost(post.id)" class="delete">削除</button>
</div>
</article>
</div>
<div *ngIf="!loading && posts.length === 0 && !error" class="no-posts">
投稿がありません
</div>
</div>
`,
styles: [`
.post-list-container { padding: 20px; max-width: 800px; margin: 0 auto; }
.error { color: #dc3545; background: #f8d7da; padding: 10px; margin: 10px 0; }
.loading { color: #007bff; text-align: center; margin: 20px 0; }
.post-item {
border: 1px solid #ddd; margin: 10px 0; padding: 15px;
border-radius: 5px;
}
.post-meta { color: #666; font-size: 0.9em; margin: 10px 0; }
.post-actions button { margin-right: 10px; padding: 5px 10px; }
.delete { background: #dc3545; color: white; }
.no-posts { text-align: center; color: #666; margin: 40px 0; }
`]
})
export class PostListComponent implements OnInit, OnDestroy {
posts: Post[] = [];
loading = false;
error: string | null = null;
private destroy$ = new Subject<void>();
constructor(private apiService: ApiService) {}
ngOnInit() {
this.loadPosts();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
loadPosts() {
this.loading = true;
this.error = null;
this.apiService.getPosts()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response) => {
this.posts = response.data;
this.loading = false;
},
error: (error) => {
this.error = error.message;
this.loading = false;
this.posts = [];
}
});
}
editPost(post: Post) {
console.log('編集:', post);
// 編集処理の実装
}
deletePost(id: number) {
if (confirm('この投稿を削除しますか?')) {
this.apiService.deletePost(id)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => {
this.posts = this.posts.filter(p => p.id !== id);
},
error: (error) => {
alert(`削除に失敗しました: ${error.message}`);
}
});
}
}
}