とほほのAngular入門

目次

Angular とは

Angularのバージョン

Angular 2.0 2016年9月14日
Angular 4.0 2016年12月13日
Angular 5.0 2017年11月1日
Angular 6.0 2018年5月4日
Angular 7.0 2018年10月18日
Angular 8.0 2019年5月28日
Angular 9.0 2020年2月6日
Angular 10.0 2020年6月24日
Angular 11.0 2020年11月11日
Angular 12.0 2021年5月13日
Angular 13.0 2021年11月3日

チュートリアルのイメージ

下記の環境で検証しています。

OS: Ubuntu 20.04
Node.js: 16.1.0
npm: 7.24.0
Angular CLI: 13.2.0

このチュートリアルでは、最終的に下記の様なイメージの画面を作成します。メニューやユーザ名をクリックしてください。値までは変化しませんが、これから作成する画面のおおよそのイメージをつかむことができます。

Angular Sample Console
Dashboard

This is a sample of Angular application.

Users
IdNameEmail
1Yamadayamada@example.com
2Suzukisuzuki@example.com
3Tanakatanaka@example.com
User edit
Id1
Name
Email
Confirm

Are you sure you want to update this user?

チュートリアルで作成するソースコードは下記からダウンロードできます。

インストール

Node.js 系のパッケージ管理ツール npm を使用して、Angular の CLI (Command Line Interface) をインストールします。

Shell
$ npm install -g @angular/cli
    :
how to change this setting, see http://angular.io/analytics. (y/N) N

アプリケーションを作成する

ng コマンドで新しいアプリケーションを作成します。my-app は作成するアプリケーション名となります。Angular routing は後程利用するので y と答えてください。スタイルシートを CSS, SCSS などから選べます。オススメは SCSS ですが、とりあえずデフォルトの CSS で。

Shell
$ ng new my-app
? Would you like to add Angular routing? (y/N) y
? Which stylesheet format would you like to use? CSS	# カーソル移動キーで選択してEnter

サーバを起動する

アプリケーションをビルドして開発用簡易サーバを起動します。起動にはしばらく時間がかかります。ブラウザでアクセスすると、Angular のサンプル画面が表示されます。LISTEN する IPアドレスやポート番号を変更するには、--host, --port オプションを指定します。他のマシンからアクセスする際は、firewall-cmd などでポートの穴あけが必要な場合があります。

Shell
$ cd my-app
$ ng serve --host 0.0.0.0 --port 8080

ブラウザから http://{サーバアドレス}:8080/ にアクセスして Angular の画面が表示されれば成功です。

HTMLやCSSを編集する

ソースを下記の様に書き換え、Hello world! を表示します。サーバが起動していれば、ソースファイルを修正すると自動的にリビルドと、ブラウザの再表示が行われます。

src/app/app.component.html
<h1>Hello world!</h1>
src/styles.css
* {
  margin: 0;
  padding: 0;
}
h1 {
  font-size: 1.4em;
}

ブラウザが自動的にリロードされ、Hello world! が表示されます。

コンポーネントを追加する

ヘッダ、メニュー、ダッシュボードなどの画面要素をコンポーネントとして追加します。

Shell
$ ng generate component header
$ ng generate component menu
$ ng generate component dashboard

app コンポーネントの HTML を下記の様に書き換えます。

src/app/app.component.html
<app-header></app-header>
<app-menu></app-menu>
<app-dashboard></app-dashboard>

ヘッダは黒地に白文字とし、Angular のアイコンを表示します。

src/app/header/header.component.html
<div class="header">
  <img src="../favicon.ico" alt="Angular"> Angular Sample Console
</div>
src/app/header/header.component.css
.header {
  padding: 4px;
  background-color: #000;
  color: #fff;
}
.header img {
  width: 18px;
  vertical-align: middle;
}

メニューには Dashboar メニューを記述します。

src/app/menu/menu.component.html
<div class="menu">
  <ul>
  <li><a href="/">Dashboard</a></li>
  </ul>
</div>
src/app/menu/menu.component.css
.menu {
  background-color: #ccc;
}
.menu li {
  padding: 4px 8px;
  display: inline-block;
}

ダッシュボードにはタイトルを表示します。

src/app/dashboard/dashboard.component.html
<h1>Dashboard</h1>

ヘッダ、メニューバー、ダッシュボードタイトルが表示されれば成功です。

変数の値を表示する

{{変数名}} で、コンポーネントが持つ変数を表示することができます。各ソースを下記の様に修正してください。

src/app/dashboard/dashboard.component.ts
  :
export class DashboardComponent implements OnInit {
  message: string;

  constructor() {
    this.message = 'This is a sample of Angular application.';
  }
  :
src/app/dashboard/dashboard.component.html
<h1>Dashboard</h1>
<p>{{message}}</p>

Dashboard 画面にコンポーネントで定義した message 変数の値が表示されれば成功です。

ルーティングモジュールを追加する

URL によって下記の様に表示を切り替えます。表示の切り替えにはルーティングモジュールを使用します。

アプリケーション作成時にルーティングモジュールの追加を行っていない場合は、ルーティングモジュールを追加します。

Shell
$ ng generate module app-routing --flat --module=app

ユーザ一覧、ユーザ編集コンポーネントを追加します。

Shell
$ ng generate component user/user-list
$ ng generate component user/user-edit

各ファイルを下記の様に修正します。

src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { DashboardComponent } from './dashboard/dashboard.component';
import { UserListComponent } from './user/user-list/user-list.component';
import { UserEditComponent } from './user/user-edit/user-edit.component';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'users', component: UserListComponent },
  { path: 'users/:id/edit', component: UserEditComponent },
];
    :
src/app/app.component.html
<app-header></app-header>
<app-menu></app-menu>
<div class="main">
  <router-outlet></router-outlet>
</div>
src/app/menu/menu.component.html
<div class="menu">
  <ul>
  <li><a routerLink="/">Dashboard</a></li>
  <li><a routerLink="/users">Users</a></li>
  </ul>
</div>
src/app/user/user-list/user-list.component.html
<h1>Users</h1>

Dashboard メニューをクリックすると Dashboard 画面が、Users メニューをクリックすると Users 画面(タイトルのみ)が表示されれば成功です。

リストを表示する

Users 画面にユーザ一覧を表示します。まず、User オブジェクトのクラスを定義します。

src/app/user/user.ts
export class User {
  id: number;
  name: string;
  email: string;

  constructor() {
    this.id = 0;
    this.name = "";
    this.email = "";
  }
}

UserList コンポーネントで users リストを初期化します。

src/app/user/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../user';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
  users: User[] = [];

  constructor() { }

  ngOnInit(): void {
    this.users = [
      { id: 1, name: 'Yamada', email: 'yamada@example.com' },
      { id: 2, name: 'Suzuki', email: 'suzuki@example.com' },
      { id: 3, name: 'Tanaka', email: 'tanaka@example.com' },
    ];
  }

}

UserList 画面で、users リストを表示します。*ngFor は配列を繰り返し表示する際に使用されるディレクティブです。

src/app/user/user-list/user-list.component.html
<h1>Users</h1>
<table>
  <tr><th>Id</th><th>Name</th><th>Email</th></tr>
  <tr *ngFor="let user of users">
    <td>{{user.id}}</td>
    <td>{{user.name}}</td>
    <td>{{user.email}}</td>
  </tr>
</table>

すこし、見栄えを整えてやります。

src/styles.css
* {
  margin: 0;
  padding: 0;
}
h1 {
  font-size: 1.4em;
}
a:link,
a:visited {
  color: #000;
}
.main {
  padding: 8px;
}
table {
  border-collapse: collapse;
  width: 100%;
}
table th,
table td {
  border: 1px solid #ccc;
  padding: 4px;
}
table th {
  background-color: #ddd;
  text-align: left;
}

Users 画面に、ユーザの一覧が表示されれば成功です。

サービスを作成する

コンポーネント間でデータを共有したりするために、サービスを作成します。下記の例では、ユーザ一覧画面、ユーザ編集画面で共有するユーザ情報を管理する user サービスを作成します。

Shell
$ ng generate service user/user

app.module.ts に user サービスを追加します。

src/app/app.module.ts
  :
import { DashboardComponent } from './dashboard/dashboard.component';
import { UserService } from './user/user.service';
import { UserListComponent } from './user/user-list/user-list.component';
  :

@NgModule({
  :
  providers: [
    UserService
  ],
  :

サービスに、初期化、一覧取得、詳細取得、設定用のメソッドを追加します。

src/app/user/user.service.ts
import { Injectable } from '@angular/core';
import { User } from './user';

@Injectable()
export class UserService {
  users: User[] = [];

  constructor() {
    this.users = [
      { id: 1, name: 'Yamada', email: 'yamada@example.com' },
      { id: 2, name: 'Suzuki', email: 'suzuki@example.com' },
      { id: 3, name: 'Tanaka', email: 'tanaka@example.com' },
    ];
  }

  getUsers(): User[] {
    return this.users;
  }

  getUser(id: number): User | undefined {
    return this.users.find(user => user.id === id);
  }

  setUser(user: User): void {
    for (let i = 0; i < this.users.length; i++) {
      if (this.users[i].id === user.id) {
        this.users[i] = user;
      }
    }
  }
}
src/app/user/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../user';
import { UserService } from '../user.service';

  :
export class UserListComponent implements OnInit {
  users: User[];

  constructor(
    private service: UserService
  ) { }

  ngOnInit(): void {
    this.users = this.service.getUsers();
  }
}

画面には変動はありませんが、サービスからデータを取得するようになりました。

フォームを作成する

ユーザ編集画面でフォームを取り扱います。まず、名前をクリックすると、/users/:id/edit に割り当てられたユーザ編集画面に遷移するようにします。

src/app/user/user-list/user-list.component.html
<h1>Users</h1>
<table>
  <tr><th>Id</th><th>Name</th><th>Email</th></tr>
  <tr *ngFor="let user of users">
    <td>{{user.id}}</td>
    <td><a routerLink="/users/{{user.id}}/edit">{{user.name}}</a></td>
    <td>{{user.email}}</td>
  </tr>
</table>

app.modules.ts に FormsModule を組み込みます。

src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
  :
@NgModule({
  :
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule
  ],
  :

ユーザ編集画面のコンポーネントを定義します。

src/app/user/user-edit/user-edit.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { User } from '../user';
import { UserService } from '../user.service';

@Component({
  selector: 'app-user-edit',
  templateUrl: './user-edit.component.html',
  styleUrls: ['./user-edit.component.css']
})

export class UserEditComponent implements OnInit {
  user: User = { id: 0, name: '', email: '' };

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: UserService
  ) { }

  ngOnInit(): void {
    const id = Number(this.route.snapshot.paramMap.get('id'));
    var user = this.service.getUser(id);
    if (user) {
      this.user = user;
    }
  }

  onSubmit(form: any): void {
    let user = {
      id: form.id,
      name: form.name,
      email: form.email
    };
    this.service.setUser(user);
    this.router.navigate(["/users"]);
  }
}

ユーザ編集画面の HTML を下記の様に書き換えます。

src/app/user/user-edit/user-edit.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
  <input type="hidden" name="id" [ngModel]="user.id">
  <table>
    <tr><th>Id</th><td>{{user.id}}</td></tr>
    <tr><th>Name</th><td><input type="text" name="name" [ngModel]="user.name"></td></tr>
    <tr><th>Email</th><td><input type="text" name="email" [ngModel]="user.email"></td></tr>
  </table>
  <button type="submit">OK</button>
  <button routerLink="/users">Cancel</button>
</form>

見栄えを少し追記。

src/styles.css
  :
button {
  margin-top: 4px;
  margin-right: 4px;
  padding: 4px;
  min-width: 120px;
}
input[type="text"] {
  padding: 4px;
  width: 300px;
}

ユーザ名をクリックするとユーザ編集画面が表示され、名前やメールアドレスを修正すると、一覧に反映されるようになれば成功です。

モーダルダイアログを表示する

Angular Material という部品群の中から MatDialog というモーダルダイアログを利用してみます。まず、Angular/material と Angular/CDK (Component Dev. Kit) をインストールします。

Shell
$ npm install --save @angular/material @angular/cdk

Angular Material 用の CSS ファイルを読み込みます。

src/styles.css
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
* {
  margin: 0;
  padding: 0;
}

モーダルダイアログのサンプルとして、コンファームダイアログコンポーネントを作成してみます。

Shell
$ ng generate component tools/confirm-dialog

各ファイルを次のように修正してください。

src/app/app.module.ts
  :
import { FormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
  :

@NgModule({
  :
  imports: [
    :
    FormsModule,
    MatDialogModule,
    BrowserAnimationsModule
  ],
  providers: [
    UserService
  ],
  entryComponents: [
    ConfirmDialogComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
src/app/tools/confirm-dialog/confirm-dialog.component.ts
import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
  selector: 'app-confirm-dialog',
  templateUrl: './confirm-dialog.component.html',
  styleUrls: ['./confirm-dialog.component.css']
})
export class ConfirmDialogComponent implements OnInit {

  constructor(
    public dialogRef: MatDialogRef<ConfirmDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: any
  ) { }

  ngOnInit(): void {
  }

  closeDialog(result: boolean) {
    this.dialogRef.close(result);
  }
}
src/app/tools/confirm-dialog/confirm-dialog.component.html
<h1>{{data.title}}</h1>
<p>{{data.message}}</p>
<button (click)="closeDialog(false)">Cancel</button>
<button (click)="closeDialog(true)">OK</button>
src/app/user/user-edit/user-edit.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../tools/confirm-dialog/confirm-dialog.component';
  :

export class UserEditComponent implements OnInit {
  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: UserService,
    private dialog: MatDialog
  ) { }

  :
  
  onSubmit(form: any): void {
    let user = {
      id: form.id,
      name: form.name,
      email: form.email
    };

    let dialogRef = this.dialog.open(ConfirmDialogComponent, {
      width: '300px',
      data: {
        title: 'Confirm',
        message: 'Are you sure you want to update this user?'
      }
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === true) {
        this.service.setUser(user);
        this.router.navigate(["/users"]);
      }
    });
  }
}

ユーザ編集画面で [OK] ボタンを押した際、本当に変更するか確認ダイアログが表示されれば成功です。ここまでのソースを、angular-sample-1.zip としてまとめてあります。

簡単な REST-API サーバを作る

上記までの例では、ブラウザ側の JavaScript 内にダミーデータを保持し、それを書き換えるのみでしたが、ユーザ情報をサーバから REST-API で読み出し・格納できるように拡張していきます。まず、Express を用いて簡単な REST-API サーバを作成します。

Shell
$ mkdir ~/rest-test
$ cd ~/rest-test
$ npm init                        // 質問にはすべて Enter
$ npm install express
$ vi index.js
下記の例では、Access-Control-Allow-Origin を用いて他オリジンからのアクセスを許可していますが、セキュリティ上のリスクがあります。実際に使用する際には適切なオリジンを指定するなどのセキュリティ対策を十分に行ってください。
index.js
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.listen(8888);
app.use(bodyParser.json());
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");     // セキュリティリスク有り
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

let users = [
  { id: 1, name: "Yamada", email: "yamada@example.com" },
  { id: 2, name: "Tanaka", email: "tanaka@example.com" },
  { id: 3, name: "Suzuki", email: "suzuki@example.com" }
];

app.get('/users', function(req, res) {
  res.send(JSON.stringify(users));
});

app.post('/users', function(req, res) {
  users.push(req.body);
  res.end();
});

app.get('/users/:id', function(req, res) {
  for (let i = 0; i < users.length; i++) {
    if (users[i].id == req.params.id) {
      res.send(JSON.stringify(users[i]));
    }
  }
});

app.post('/users/:id', function(req, res) {
  for (let i = 0; i < users.length; i++) {
    if (users[i].id == req.params.id) {
      users[i] = req.body;
    }
  }
  res.end();
});

app.delete('/users/:id', function(req, res) {
  for (let i = 0; i < users.length; i++) {
    if (users[i].id == req.params.id) {
      users.splice(i, 1);
    }
  }
  res.end();
});
Shell
$ node index.js

別のコンソールから動作を確認します。

Shell
# ユーザ一覧
$ curl -s -X GET http://localhost:8888/users | python -mjson.tool

# ユーザ追加
$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"id":4,"name":"Sasaki","email":"sasaki@example.com"}' \
  http://127.0.0.1:8888/users

# ユーザ更新
$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"id":4,"name":"Sasaki2","email":"sasaki2@example.com"}' \
  http://127.0.0.1:8888/users/4

# ユーザ削除
$ curl -s -X DELETE http://127.0.0.1:8888/users/4

RxJS で非同期通信を行う

REST-API サーバと非同期通信を行うために、RxJS というライブラリを利用します。

src/app/app.module.ts
  :
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { MatDialogModule } from '@angular/material/dialog';
  :
@NgModule({
  :
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    :
  ],
  :

url に指定するアドレスは適切に変更してください。サーバからではなくブラウザから見た RESTサーバのアドレスを指定します。

src/app/user/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { User } from './user';jj

@Injectable()
export class UserService {
  users: User[] = [];
  private url = 'http://127.0.0.1:8888';     // 適切に変更してください

  constructor(
    private http: HttpClient
  ) { }

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.url}/users`)
      .pipe(
        catchError(this.handleError('getUsers', []))
      );
  }

  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.url}/users/${id}`)
      .pipe(
        catchError(this.handleError<User>(`getUser id=${id}`))
      );
  }

  setUser(user: User): Observable<User> {
    const id = user.id;
    return this.http.post<User>(`${this.url}/users/${id}`, user)
      .pipe(
        catchError(this.handleError<User>(`setUser id=${id}`))
      );
  }

  private handleError<T> (operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error);
      console.log(`${operation} failed: ${error.message}`);
      return of(result as T);
    };
  }
}
src/app/user/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../user';
import { UserService } from '../user.service';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.css']
})

export class UserListComponent implements OnInit {
  users: User[];

  constructor(
    private service: UserService
  ) { }

  ngOnInit(): voi {
    this.service.getUsers().subscribe(res => {
      this.users = res;
    });
  }
}
src/app/user/user-edit/user-edit.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { User } from '../user';
import { UserService } from '../user.service';

@Component({
  selector: 'app-user-edit',
  templateUrl: './user-edit.component.html',
  styleUrls: ['./user-edit.component.css']
})
export class UserEditComponent implements OnInit {
  user: User = { id: 0, name: '', email: '' };

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: UserService,
    private dialog: MatDialog
  ) { }

  ngOnInit(): void {
    const id = Number(this.route.snapshot.paramMap.get('id'));
    this.service.getUser(id).subscribe(res => {
      this.user = res;
    });
  }

  onSubmit(form: any): void {
    :
    dialogRef.afterClosed().subscribe(result => {
      if (result === true) {
        this.service.setUser(user).subscribe(() => {
          this.router.navigate(["/users"]);
        });
      }
    });
  }
}

サーバが保持するユーザデータを、一覧表示、編集することが可能となりました。ここまでのプログラム(src)配下を angular-sample-2.zip としてダウンロードできます。