【Angular】Vueユーザー向けAngularチュートリアル

フレームワークを学習する際、「Vueで言うところのあの機能は、このフレームワークではどうやって実装するのだろう...」という風な学習の進め方をします。

最近Angularを学習しているのですが、その際に整理した代表的な「Vueで言うところの...」を、記事にしてみました。Vueとの比較をしながら、Angularの基礎も紹介しています。Angularの学習を始められた同志のお力になれると幸いです！(^｡^)

[こちらの記事](https://qiita.com/whopper1962/items/31d82ef527b1162ab925)ではReactの「Vueで言うところの...」を紹介しています。是非併せてご覧ください！✨

## Angularとは
![angular-logo-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/7b0baa8a-0ef5-17f6-1299-364ca1d16870.png)
Angularは、Googleによって開発されたオープンソースのJavaScriptフレームワークです。2012年6月に初版がリリースされ、2022年6月に、最新版となるバージョン14がリリースされました（2023年5月現在）。TypeScripをベースに作成されている為、TypeScriptを利用する為に特別設定が必要ないのも特徴です。React、Vue、Svelteに並ぶ人気者です。
[**公式ドキュメントはこちら**](https://angular.jp/docs)

## Angularアプリケーションの起動方法
以下コマンドを実行して、Angularアプリケーションの作成を実行してください。
~~~bash
npm init @angular {アプリケーション名}
~~~
実行すると対話形式のCLIが起動するので、ルーティングの導入と、スタイリングに使用する言語の選択を要求されます。それぞれ作成したいアプリケーションに合わせて選択すると、アプリケーションの作成が開始します。
~~~bash
> ? Would you like to add Angular routing? (y/N)
> ? Which stylesheet format would you like to use? (Use arrow keys)
❯ CSS 
  SCSS   [ https://sass-lang.com/documentation/syntax#scss                ] 
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] 
  Less   [ http://lesscss.org
~~~
以下のように出力されれば、アプリケーションの作成が正常終了しています。`command not found: npm`というエラーが出た場合は、実行環境にNode.jsとnpmがインストールされていません。[こちらの記事]()を参考にインストールした後に再度以下コマンドを実行してください。
~~~bash
...
CREATE my-first-app/src/app/app.module.ts (393 bytes)
CREATE my-first-app/src/app/app.component.css (0 bytes)
CREATE my-first-app/src/app/app.component.html (23115 bytes)
CREATE my-first-app/src/app/app.component.spec.ts (1091 bytes)
CREATE my-first-app/src/app/app.component.ts (216 bytes)
✔ Packages installed successfully.
    Successfully initialized git.
~~~
インストールが完了したら、作成されたアプリケーションのディレクトリに移動してください。
~~~bash
cd {アプリケーション名}
~~~
以下コマンドを実行すると、Angularの開発サーバーが[localhost:4200](http://localhost:4200)起動します（4200が使用されている場合は別ポートで起動します）。
~~~bash
npm start
~~~
~~~bash
> my-first-app@0.0.0 start
> ng serve

✔ Browser application bundle generation complete.

Initial Chunk Files   | Names         |  Raw Size
vendor.js             | vendor        |   2.04 MB | 
polyfills.js          | polyfills     | 314.28 kB | 
styles.css, styles.js | styles        | 209.41 kB | 
main.js               | main          |  48.10 kB | 
runtime.js            | runtime       |   6.52 kB | 

                      | Initial Total |   2.60 MB

Build at: 2023-05-03T05:01:09.540Z - Hash: d570b7afea270a79 - Time: 4150ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **


✔ Compiled successfully.
~~~
以下のような画面が表示されれば、Angularアプリケーションの作成が完了しました！👏
![Screen Shot 2023-05-03 at 14.07.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/75cfaaea-4a49-b2c5-0ad2-368bf74cbcf2.png)

また、AngularCLIを使用することでAngularアプリケーション開発がより快適になります。インストールをおすすめします。AngularCLIの細かい説明は[こちらの公式ドキュメント](https://angular.jp/cli)をご覧ください。
~~~bash
npm install -g @angular/cli
~~~

## コンポーネント基礎
Angularでは**TypeScriptファイル**, **HTMLファイル**, **CSSファイル**を1セットとしてコンポーネントを構成します。Vueの`template`タグ,`script`タグ,`style`タグがそれぞれ別のファイルになっているイメージです。そう聞くと少し面倒な感じがしますが、AngularCLIを使用することで簡単にコンポーネントを作成することができます。
アプリケーションのディレクトリで、以下コマンドを実行してください。
~~~bash
ng generate component {コンポーネント名}
~~~
以下が出力されれば、コンポーネントの自動生成が完了しています。コンポーネント作成時は`app.module.ts`でコンポーネントの登録を行う必要がありますが、これも自動で修正をしてくれます。便利です。
~~~bash
CREATE src/app/hello/hello.component.css (0 bytes)
CREATE src/app/hello/hello.component.html (30 bytes)
CREATE src/app/hello/hello.component.spec.ts (656 bytes)
CREATE src/app/hello/hello.component.ts (237 bytes)
UPDATE src/app/app.module.ts (509 bytes)
~~~
また、上記のコマンドではspecファイルも生成されます。こちらはテスト実装用のファイルなので必要に応じて生成してください。`--skip-tests`オプションを付与することでspecファイルを生成しないようにすることができます。
~~~bash
ng generate component {コンポーネント名} --skip-tests
~~~
`AngularではTypeScriptファイル, HTMLファイル, CSSファイルを1セットとしてコンポーネントを構成します`と書きましたが、TypeScriptファイル内でテンプレートを表現することも可能です。本記事は基本的にファイルを分ける方法で記載していますので、ご了承ください。
~~~typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `
    <h1>Hello Component</h1>
  `,
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
}
~~~
## Vueで言うところの`data`
TypeScriptファイルのコンポーネントクラスで、メンバー変数として定義することで、dataと同様の動きをします。
~~~diff_typescript:TypeScriptファイル
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  message = 'Hello World!';
  flag = false;
}
~~~
コンポーネントから使用する際は、以下のように使用します。
~~~html:HTMLファイル
<p *ngIf="flag">{{ message }}</p>
~~~

## Vueで言うところの`methods`
TypeScriptファイル内コンポーネントクラスで、メソッドを定義することで、methodsと同様の動きをします。
メソッド内でメンバー変数（`data`）にアクセスする際は、Vue同様に`this.{アクセスしたいデータ}`という風に書きます。
~~~diff_typescript:TypeScriptファイル
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  message = 'Hello World!';

  helloWorld(): void {
    alert(this.message);
  }
  saySomething(something: string): void {
    alert(something);
  }
}
~~~
HTMLファイルからメソッドを使用する際は、以下のように記述します。
~~~html:HTMLファイル
<button (click)="helloWorld()">HelloWorld!</button>
<button (click)="saySomething('Something!!')">SaySomething!</button>
~~~
## Vueで言うところの`computed`
VueのcomputedプロパティをAngularで再現する方法として、`get`キーワードを付与してゲッターメソッドを定義する方法がよく紹介されています。
ですが、以下の場合、コンソールには「numberToDisplay called.」が`numberToDisplay`が複数回出力されます。返却する値が同じものでも、何度も再計算を行なっています。
Vueの`computed`プロパティは、計算結果のキャッシュ機能が特徴となっています。Vueで以下のような実装をした場合は一度だけコンソールに出力され、依存するデータが変化するまで再計算が行われることはありません。（[こちらの記事](https://qiita.com/Yudai_35_/items/ca13a428ff66f54852ed)で`computed`プロパティのキャッシュ機能が分かりやすく解説されています！）
その為、computed`プロパティを再現する方法として`get`キーワードを使用したゲッターメソッドを定義するのは適切とは言えません。キャッシュする挙動まで再現する方法は調べた限り無さそうだったので、方法が分かる方は是非コメントお願いします。
~~~typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-user',
  template: `
    <p>{{ numberToDisplay }}</p>
    <p>{{ numberToDisplay }}</p>
    <p>{{ numberToDisplay }}</p>
    <p>{{ numberToDisplay }}</p>
  `,
  styleUrls: ['./user.component.css'],
})
export class UserComponent {
  number: number = 0;

  get numberToDisplay(): number {
    console.log('numberToDisplay called.');
    return this.number + 1;
  }
}
~~~

## Vueで言うところの`props`
`@Input`デコレーターを使用することで親から子にデータを渡すことができるようになります。以下では、子コンポーネントにプロパティ`name`を定義して、親から「Test props」という値を渡しています。
~~~diff_typescript:TypeScriptファイル（子）
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() name: string = '';
}
~~~
~~~html:HTMLファイル（子）
<p>{{ language }}</p> <!-- Test props -->
~~~
~~~html:HTMLファイル（親）
<app-child name="Test props"></app-child>
~~~
また、子コンポーネントに渡す値に変数をバインディングする場合は、以下のようにプロパティ名を`[]`で囲う必要があります。
~~~html:HTMLファイル（親）
<app-child [language]="propToChild"></app-child>
~~~
~~~typescript:TypeScriptファイル（親）
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  propToChild = 'Hello';
}
~~~

## Vueで言うところの`emit`
`@Output`デコレーターと、`EventEmitter`クラスを使用することでカスタムイベント（emit）を定義をすることができます。以下では、`close`というカスタムイベントを定義し、子コンポーネントのボタンが押下されたタイミングでイベントを発火しています。`this.{イベント名}.emit()`で定義したイベントを発火することができます。
~~~diff_html:HTMLファイル（子）
<button (click)="onClickClose()">Close</button>
~~~
~~~diff_typescript:TypeScriptファイル（子）
import {
  Component,
  EventEmitter,
  Output
} from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css'],
})
export class ChildComponent {
  @Output() close = new EventEmitter<any>();

  onClickClose(): void {
    this.close.emit();
  }
}
~~~
~~~diff_html:HTMLファイル（親）
<app-child language="propToChild" (close)="onEmitClose()"></app-child>
~~~
~~~diff_typescript:TypeScriptファイル（親）
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  onEmitClose(): void {
    alert('Emitted!!!');
  }
}
~~~
イベント発火時に引数として値を渡す場合は以下のように実装します。
~~~diff_html:HTMLファイル（子）
<button (click)="onClickClose(args)">Close</button>
~~~
~~~diff_typescript:TypeScriptファイル（子）
import {
  Component,
  EventEmitter,
  Output
} from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css'],
})
export class ChildComponent {
  @Output() close = new EventEmitter<string>();

  onClickClose(): void {
    this.close.emit('Hello');
  }
}
~~~
~~~diff_html:HTMLファイル（親）
<app-child language="propToChild" (close)="onEmitClose($event)"></app-child>
~~~
~~~diff_typescript:TypeScriptファイル（親）
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  onEmitClose(args): void {
    alert(args);
  }
}
~~~

## Vueで言うところの`watch`
Angularではメンバ変数として定義した値（Vueで言うところの`data`）の変更を検知する方法は無さそうです（何か方法がありましたら是非教えていただきたいです）。ですが、`ngOnChanges()`メソッドを使用して`@Input()`を使用して親から受け取った値（Vueで言うところの`props`）の変更を検知することは可能です。
`ngOnChanges()`メソッドを使用するには、`OnChange`インターフェースを`implements`する必要があります。
~~~diff_typescript:TypeScriptファイル（子）
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent implements OnChanges {
  @Input() name = '';

  ngOnChanges(changes: SimpleChanges): void {
    console.error('Changed:', changes);
  }
}
~~~
~~~diff_html:HTMLファイル（子）
<p>Name: {{ name }}</p>
~~~
~~~diff_typescript:TypeScriptファイル（親）
import { Component } from '@angular/core';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent {
  parentName: string = '';
}
~~~
~~~diff_html:HTMLファイル（親）
Name: <input type="text" [(ngModel)]="parentName"/>
<app-hello [name]="parentName"></app-hello>
~~~
`ngOnChanges()`メソッドは引数として以下のような`SimpleChanges`オブジェクトを受け取ります。このオブジェクトからプロパティ毎の変更前の値（`previousValue`）と変更後の値（`currentValue`）を取得することができます。`ngOnChanges()`メソッドは`@Input()`プロパティ初期化時にも実行される為、コンポーネント初期化時に必ず一度実行されます。その際は`firstChange`が`true`になります。
~~~javascript
{
  "name": {
    "previousValue": "a",
    "currentValue": "aa",
    "firstChange": false
  }
}
~~~
## Vueで言うところの`components`
Vueでは、コンポーネント毎に`components`に使用する子コンポーネントを追加するのば一般的ですが、Angularでは`app.module.ts`で使用するコンポーネントを指定して、全てのコンポーネントから全てのコンポーネントを使用できるようにするのが一般的なようです。
`@NgModule()`デコレーションの`declarations`にインポートしたクラスを追加することでコンポーネントの登録が完了します。
~~~typescript:src/app/app.component.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HelloComponent } from './hello/hello.component';
import { RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { UserComponent } from './user/user.component';
import { ChildComponent } from './child/child.component';
import { ParentComponent } from './parent/parent.component';
import { TestComponent } from './test/test.component';

@NgModule({
  declarations: [
    AppComponent,
    HelloComponent,
    UserComponent,
    ChildComponent,
    ParentComponent,
    TestComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    RouterModule.forRoot([]),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
~~~

## ディレクティブ
### Vueで言うところの`@click`
`click`を使用することで要素がクリックされた際のイベントを定義することができます。`(click)="クリック時に発火するイベント"`というような形で使用します。以下では、ボタンをクリックしたらメソッド`hello`を実行しています。
~~~html
<button (click)="hello()">
  Button
</button>
~~~
Vueでは`@click="hello"`という風に書くと、メソッドの引数にはイベントオブジェクトが渡されますが、Angularでは、以下のようにイベントオブジェクトの`$event`を明示的に引数として渡す必要があります。
~~~html
<button (click)="hello($event)">
  Button
</button>
~~~

### Vueで言うところの`v-model`
`ngModel`を使用することでフォームの入力に変数をバインディングすることができます。`[(ngModel)]="バインディングする変数"`というような形で使用します。以下では、変数`name`をテキストフォームにバインディングしています。
~~~html
<input type="text" [(ngModel)]="name" />
~~~
注意点として、`ngModel`を使用する際には`app.module.ts`で以下のように`FormsModule`の定義をする必要があります。定義しない場合、`Can't bind to 'ngModel' since it isn't a known property of 'input'`というエラーが出力されます。
~~~diff_typescript:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello/hello.component';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    HelloComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
~~~
### Vueで言うところの`v-for`
`ngFor`を使用することでループを表現します。`*ngFor="let 要素 of ループ対象"`というような形で使用します。以下では、`someArr`の要素数回分、`ngFor`が付与された要素以下を表示します。
~~~html
<div *ngFor="let item of someArr">
  {{ item }}
</div>
~~~

### Vueで言うところの`v-if`
`ngIf`を使用することで要素の表示に条件を付与することができます。`*ngIf="条件"`というような形で使用します。以下では、`isAngular`が`true`の場合に`ngIf`が付与された要素以下を表示します
~~~html
<div *ngIf="isAngular">
  <p>Hello Angular!</p>
</div>
~~~

## Vueで言うところの`created`/`mounted`
`ngOnInit()`を使用することで、コンポーネント初期化時に一度だけ処理をフックすることができます。
`ngOnInit()`を使用するには、コンポーネントクラスで`OnInit`インターフェースをimplementsする必要があります。implementsすることにより、`ngOnInit()`メソッドを使用することができるようになります。
~~~typescript
import { Component, OnInit } from '@angular/core';
import axios from 'axios';

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

  ngOnInit(): void {
    this.getUsers();
  }

  async getUsers(): Promise<void> {
    try {
      const { data } = await axios.get('/api/users');
      console.log(data);
    } catch (e) {
      console.error(e);
    }
  }
}
~~~
他、ライフサイクルフックに関しては[公式ドキュメント](https://angular.jp/guide/lifecycle-hooks)を参照してください。
## ルーティング（Vueで言うところのVueRouter）
### 初期設定
Vueでは、VueRouterをインストールしてルーティングを行ないますが、Angularでは追加パッケージなどインストールすることなくルーティングを行うことができます。ここではルーティング導入の代表例を紹介します。
`app.module.ts`の`@NgModule()`デコレーターの`imports`に、`RouterModule.forRoot()`を追加してください。
~~~diff_typescript:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
~~~
次に`src/app/app.component.html`に`router-outlet`（VueRotuerで言うところの`router-view`）タグを追加します。
~~~html:src/app/app.component.html
<router-outlet></router-outlet>
~~~
あとは、`RouterModule.forRoot()`の引数に配列形式でルート情報を追加すればルーティングの設定が完了です。ルート情報は以下のようにパス（`path`）と表示するコンポーネント（`component`）を持つオブジェクトを指定してください。
~~~diff_javascript:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { HelloComponent } from './hello/hello.component';

@NgModule({
  declarations: [
    AppComponent,
    HelloComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: 'hello',
        component: HelloComponent
      }
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
~~~
また、ルーターにパスパラメータを指定する場合には、`path`の中で`:paramName`のように指定します。
~~~typescript
{
  path: 'users/:userId',
  component: UserComponent
}
~~~
### 遷移
#### Vueで言うところの`<router-link/>`
AngularにはVueRouterの`<router-link>`のようなタグは用意されていません。その代わり、aタグにルーター用のディレクティブが追加されます。遷移先を指定するには、`routerLink`を使用します。この`routerLink`の値として、定義したパス（ルート定義時に`path`に指定した値）を指定することで、対象の画面に遷移することができます。
~~~html
<a routerLink="/users">Users</a>
~~~
パスパラメータを指定する場合には、リンクパラメータ配列を使用します。`routerLink`を`[]`で囲うことで、値に配列を指定することができるようになります。配列の1つ目の要素に遷移先の`path`、2つ目の要素にパスパラメータを指定します。以下の場合、遷移先のURLは`/users/1`となります。
~~~html
<a [routerLink]="['/users', 1]">Users</a>
~~~
#### Vueで言うところの`this.$router.push()`
TypeScriptで画面遷移する際には、`navigate()`メソッドを使用します。`naviagate()`の引数には、`routerLink`で指定したものと同様のものを渡します。
~~~diff_typescript
import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
  constructor(private _router: Router) {}

  jumpToUserPage(): void {
    this._router.navigate(['/users', 1]);
  }
}
~~~