Angular

TypeScriptベースのWebアプリケーションフレームワーク。Googleが開発し、大規模企業向けの包括的な機能セット、強力な型安全性、RxJSによるリアクティブプログラミングを提供。

TypeScriptフレームワークFrontendUIコンポーネントSPAPWA

GitHub概要

angular/angular

Deliver web apps with confidence 🚀

スター98,317
ウォッチ2,999
フォーク26,424
作成日:2014年9月18日
言語:TypeScript
ライセンス:MIT License

トピックス

angularjavascriptpwatypescriptwebweb-frameworkweb-performance

スター履歴

angular/angular Star History
データ取得日時: 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}`);
          }
        });
    }
  }
}