フレームワークを学習する際、「Vueで言うところのあの機能は、このフレームワークではどうやって実装するのだろう...」という風な学習の進め方をします。
最近Angularを学習しているのですが、その際に整理した代表的な「Vueで言うところの...」を、記事にしてみました。Vueとの比較をしながら、Angularの基礎も紹介しています。Angularの学習を始められた同志のお力になれると幸いです!(^。^)
こちらの記事ではReactの「Vueで言うところの...」を紹介しています。是非併せてご覧ください!✨
Angularとは
Angularは、Googleによって開発されたオープンソースのJavaScriptフレームワークです。2012年6月に初版がリリースされ、2022年6月に、最新版となるバージョン14がリリースされました(2023年5月現在)。TypeScripをベースに作成されている為、TypeScriptを利用する為に特別設定が必要ないのも特徴です。React、Vue、Svelteに並ぶ人気者です。
公式ドキュメントはこちら
Angularアプリケーションの起動方法
以下コマンドを実行して、Angularアプリケーションの作成を実行してください。
npm init @angular {アプリケーション名}
実行すると対話形式のCLIが起動するので、ルーティングの導入と、スタイリングに使用する言語の選択を要求されます。それぞれ作成したいアプリケーションに合わせて選択すると、アプリケーションの作成が開始します。
> ? 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がインストールされていません。こちらの記事を参考にインストールした後に再度以下コマンドを実行してください。
...
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.
インストールが完了したら、作成されたアプリケーションのディレクトリに移動してください。
cd {アプリケーション名}
以下コマンドを実行すると、Angularの開発サーバーがlocalhost:4200起動します(4200が使用されている場合は別ポートで起動します)。
npm start
> 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アプリケーションの作成が完了しました!👏
また、AngularCLIを使用することでAngularアプリケーション開発がより快適になります。インストールをおすすめします。AngularCLIの細かい説明はこちらの公式ドキュメントをご覧ください。
npm install -g @angular/cli
コンポーネント基礎
AngularではTypeScriptファイル, HTMLファイル, CSSファイルを1セットとしてコンポーネントを構成します。Vueのtemplate
タグ,script
タグ,style
タグがそれぞれ別のファイルになっているイメージです。そう聞くと少し面倒な感じがしますが、AngularCLIを使用することで簡単にコンポーネントを作成することができます。
アプリケーションのディレクトリで、以下コマンドを実行してください。
ng generate component {コンポーネント名}
以下が出力されれば、コンポーネントの自動生成が完了しています。コンポーネント作成時はapp.module.ts
でコンポーネントの登録を行う必要がありますが、これも自動で修正をしてくれます。便利です。
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ファイルを生成しないようにすることができます。
ng generate component {コンポーネント名} --skip-tests
AngularではTypeScriptファイル, HTMLファイル, CSSファイルを1セットとしてコンポーネントを構成します
と書きましたが、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と同様の動きをします。
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;
}
コンポーネントから使用する際は、以下のように使用します。
<p *ngIf="flag">{{ message }}</p>
Vueで言うところのmethods
TypeScriptファイル内コンポーネントクラスで、メソッドを定義することで、methodsと同様の動きをします。
メソッド内でメンバー変数(data
)にアクセスする際は、Vue同様にthis.{アクセスしたいデータ}
という風に書きます。
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ファイルからメソッドを使用する際は、以下のように記述します。
<button (click)="helloWorld()">HelloWorld!</button>
<button (click)="saySomething('Something!!')">SaySomething!</button>
Vueで言うところのcomputed
VueのcomputedプロパティをAngularで再現する方法として、get
キーワードを付与してゲッターメソッドを定義する方法がよく紹介されています。
ですが、以下の場合、コンソールには「numberToDisplay called.」がnumberToDisplay
が複数回出力されます。返却する値が同じものでも、何度も再計算を行なっています。
Vueのcomputed
プロパティは、計算結果のキャッシュ機能が特徴となっています。Vueで以下のような実装をした場合は一度だけコンソールに出力され、依存するデータが変化するまで再計算が行われることはありません。(こちらの記事でcomputed
プロパティのキャッシュ機能が分かりやすく解説されています!)
その為、computedプロパティを再現する方法として
get`キーワードを使用したゲッターメソッドを定義するのは適切とは言えません。キャッシュする挙動まで再現する方法は調べた限り無さそうだったので、方法が分かる方は是非コメントお願いします。
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」という値を渡しています。
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
@Input() name: string = '';
}
<p>{{ language }}</p> <!-- Test props -->
<app-child name="Test props"></app-child>
また、子コンポーネントに渡す値に変数をバインディングする場合は、以下のようにプロパティ名を[]
で囲う必要があります。
<app-child [language]="propToChild"></app-child>
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()
で定義したイベントを発火することができます。
<button (click)="onClickClose()">Close</button>
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();
}
}
<app-child language="propToChild" (close)="onEmitClose()"></app-child>
import { Component } from '@angular/core';
@Component({
selector: 'app-hello',
templateUrl: './hello.component.html',
styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
onEmitClose(): void {
alert('Emitted!!!');
}
}
イベント発火時に引数として値を渡す場合は以下のように実装します。
<button (click)="onClickClose(args)">Close</button>
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');
}
}
<app-child language="propToChild" (close)="onEmitClose($event)"></app-child>
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
する必要があります。
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);
}
}
<p>Name: {{ name }}</p>
import { Component } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent {
parentName: string = '';
}
Name: <input type="text" [(ngModel)]="parentName"/>
<app-hello [name]="parentName"></app-hello>
ngOnChanges()
メソッドは引数として以下のようなSimpleChanges
オブジェクトを受け取ります。このオブジェクトからプロパティ毎の変更前の値(previousValue
)と変更後の値(currentValue
)を取得することができます。ngOnChanges()
メソッドは@Input()
プロパティ初期化時にも実行される為、コンポーネント初期化時に必ず一度実行されます。その際はfirstChange
がtrue
になります。
{
"name": {
"previousValue": "a",
"currentValue": "aa",
"firstChange": false
}
}
Vueで言うところのcomponents
Vueでは、コンポーネント毎にcomponents
に使用する子コンポーネントを追加するのば一般的ですが、Angularではapp.module.ts
で使用するコンポーネントを指定して、全てのコンポーネントから全てのコンポーネントを使用できるようにするのが一般的なようです。
@NgModule()
デコレーションのdeclarations
にインポートしたクラスを追加することでコンポーネントの登録が完了します。
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
を実行しています。
<button (click)="hello()">
Button
</button>
Vueでは@click="hello"
という風に書くと、メソッドの引数にはイベントオブジェクトが渡されますが、Angularでは、以下のようにイベントオブジェクトの$event
を明示的に引数として渡す必要があります。
<button (click)="hello($event)">
Button
</button>
Vueで言うところのv-model
ngModel
を使用することでフォームの入力に変数をバインディングすることができます。[(ngModel)]="バインディングする変数"
というような形で使用します。以下では、変数name
をテキストフォームにバインディングしています。
<input type="text" [(ngModel)]="name" />
注意点として、ngModel
を使用する際にはapp.module.ts
で以下のようにFormsModule
の定義をする必要があります。定義しない場合、Can't bind to 'ngModel' since it isn't a known property of 'input'
というエラーが出力されます。
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
が付与された要素以下を表示します。
<div *ngFor="let item of someArr">
{{ item }}
</div>
Vueで言うところのv-if
ngIf
を使用することで要素の表示に条件を付与することができます。*ngIf="条件"
というような形で使用します。以下では、isAngular
がtrue
の場合にngIf
が付与された要素以下を表示します
<div *ngIf="isAngular">
<p>Hello Angular!</p>
</div>
Vueで言うところのcreated
/mounted
ngOnInit()
を使用することで、コンポーネント初期化時に一度だけ処理をフックすることができます。
ngOnInit()
を使用するには、コンポーネントクラスでOnInit
インターフェースをimplementsする必要があります。implementsすることにより、ngOnInit()
メソッドを使用することができるようになります。
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);
}
}
}
他、ライフサイクルフックに関しては公式ドキュメントを参照してください。
ルーティング(Vueで言うところのVueRouter)
初期設定
Vueでは、VueRouterをインストールしてルーティングを行ないますが、Angularでは追加パッケージなどインストールすることなくルーティングを行うことができます。ここではルーティング導入の代表例を紹介します。
app.module.ts
の@NgModule()
デコレーターのimports
に、RouterModule.forRoot()
を追加してください。
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
)タグを追加します。
<router-outlet></router-outlet>
あとは、RouterModule.forRoot()
の引数に配列形式でルート情報を追加すればルーティングの設定が完了です。ルート情報は以下のようにパス(path
)と表示するコンポーネント(component
)を持つオブジェクトを指定してください。
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
のように指定します。
{
path: 'users/:userId',
component: UserComponent
}
遷移
Vueで言うところの<router-link/>
AngularにはVueRouterの<router-link>
のようなタグは用意されていません。その代わり、aタグにルーター用のディレクティブが追加されます。遷移先を指定するには、routerLink
を使用します。このrouterLink
の値として、定義したパス(ルート定義時にpath
に指定した値)を指定することで、対象の画面に遷移することができます。
<a routerLink="/users">Users</a>
パスパラメータを指定する場合には、リンクパラメータ配列を使用します。routerLink
を[]
で囲うことで、値に配列を指定することができるようになります。配列の1つ目の要素に遷移先のpath
、2つ目の要素にパスパラメータを指定します。以下の場合、遷移先のURLは/users/1
となります。
<a [routerLink]="['/users', 1]">Users</a>
Vueで言うところのthis.$router.push()
TypeScriptで画面遷移する際には、navigate()
メソッドを使用します。naviagate()
の引数には、routerLink
で指定したものと同様のものを渡します。
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]);
}
}