Angularのコンポーネントを極める!ライフサイクルと複数コンポーネントの連携。

Angularのコンポーネントを極める!ライフサイクルと複数コンポーネントの連携。

この記事では、Angularの核である「コンポーネント」についてまとめてみた。

Angularアプリは数多くのコンポーネントが集まって構成される。

なかなかフォーカスして考える機会の少ないテーマだが、読みやすく保守性の高いコードを書くためには常に意識しておきたいテーマである。

ここではコンポーネントのライフサイクルや複数コンポーネントの連携といったテーマについて深く掘り下げてみた。

Angularにおけるコンポーネントとは?

コンポーネントはAngularアプリを構成するパーツのようなものであり、Angularアプリは数多くのコンポーネントが組み合わさって構成される。

また、その特徴からAngularはReactやVue.jsと共に「コンポーネント指向のフレームワーク」に分類される。

以下の図はAngularアプリのコンポーネントの構成の例を表している。

コンポーネントのイメージ図

スクリーンはHeader・Sidebar・MainView・Footerの各コンポーネントに分割されたうえ、Header内にはUserInfo・SearchBarという2つのコンポーネントが配置されている。

Routerの設定を行えば選択したメニューによってMainViewComponentの内容のみを切り替えれる。

レスポンシブデザインとして、モバイルやタブレットでのみコンポーネントの配置を変更することももちろん可能だ。

コンポーネントのファイル構成

次にプロジェクト内でコンポーネントがどのように構成されているのか確認したい。

Angularのプロジェクトでは、ひとつのコンポーネントに関連するファイルはひとつのフォルダにまとめて配置される。

通常ではひとつのコンポーネントは以下の4つのファイルから構成される。

Angular CLIの「ng generate」コマンドを使用してコンポーネントを作成した場合、デフォルトでは上記の4つのファイルが作成される。

  • name.component.html
    ⇒ コンポーネントのビューを決定するHTMLファイル。Angularでは「テンプレート」とも呼ばれる。
    バインディングやディレクティブを使用して動的なページを作成することが可能。
  • name.component.css
    ⇒ 一般的なCSSファイル。クラスのスタイルを決定する。
    CSSの代わりにSCSS・SASSを使用することも可能。
  • name.component.ts
    ⇒ コンポーネントのメインのTypeScriptファイルで、おもにコンポーネントのロジックにあたる部分を記述する。
  • name.component.spec.ts
    ⇒ ユニットテストのためのTypeScriptファイル。

component.ts内のComponentアノテーションで使用するhtmlとcssは定義されている。

@Component({
  selector: 'app-footer',
  templateUrl: './footer.component.html',
  styleUrls: ['./footer.component.css']
})
export class FooterComponent {
  constructor() { }
}

コンポーネント間の連携

次にコンポーネント同士がどのように結びつき、双方に連携していくのか見ていきたい。

コンポーネントの呼び出し

Angularのコンポーネント内で他のコンポーネントを読み込むにはいくつかの方法があるが、ここではもっとも簡単なコンポーネントセレクタを使ったパターンを紹介する。

HTMLファイル内では以下のようにセレクタを記述することによって他のコンポ-ネントを読み込むことができる。

<app-header></app-header>

セレクタ名はコンポーネントアノテーション内のselectorプロパティにて定義されている。

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

HeaderComponentをAppComponent内に読み込んだ例

@Inputデコレーター

親コンポーネントから子コンポーネントに向けて変数を受け渡したい場合は@Inputデコレーターを使用する。

ここでは親であるAppComponentから子であるHeaderComponentに向けてユーザ名の変数stringを受け渡す例を紹介する。

まずは子コンポーネント内に@Inputとともに変数をフィールドとして宣言する。

@Input() userName: string = ''; 

親コンポーネント内に渡す変数を定義する。

public appUserName = 'Kimura Natsuko'

親コンポ-ネントのテンプレートのセレクタタグ内に属性として子コンポーネントの変数名を鍵カッコで包み記述する。

<app-header [userName]="appUserName"></app-header>

なお、属性名はプロパティ名とは別にすることもできる。

@Input('name') userName: string = '';
<app-header [name]="appUserName"></app-header>

AppComponentから受け取ったuserNameを表示した例

なお、@Inputデコレーターはgetterやsetterに対して使うこともできる。

userName: string = '';

@Input()
set name(name: string) {
  this.userName = name;
}
  
get name(){
  return this.userName;
}

@Outputデコレーター

子コンポーネントから親コンポーネントに値を引き渡す場合は@Outputデコレーターを使用する。

@Outputデコレーターはイベントの通知に対して使われる。

ここでは、EventEmitterを使ってボタンクリックを親コンポーネントに通知する例を紹介する。

まずは子コンポーネントに新しいEventEmitterを追加し、ボタンのクリックと同時にイベントを通知する。

@Output() buttonClickEvent = new EventEmitter<any>();
<button (click)="buttonClickEvent.emit()">ボタン</button>

親コンポーネントのテンプレートでイベントを受け取る。@Outputデコレーターを受けとる際は丸カッコで記述する。

<app-header (buttonClickEvent)="onClickEvent()"></app-header>
onClickEvent() {
  console.log('button clicked!!');
}

@ViewChild

@ViewChildアノテーションはテンプレート内に配置されたコンポーネント取得するのに使用する。

実際に@ViewChildを使用してみよう。

まずはHeaderComponent.tsに適当な関数を作成する。関数は外部から参照できるようにpublicにしておく。

public callAlert() {
  alert('これはテストです!');
}

呼び出し側のテンプレートでHeaderComponentを読み込む。

<app-header></app-header>

@ViewChildを使いHeaderComponentの変数を宣言する。

@ViewChild(HeaderComponent) headerComponent;

呼び出し側のコンポーネント内でHeaderComponentに作成した関数を呼び出す。この際、関数はngAfterViewInit内で呼び出す。constructorやngOnInitではまだHeaderComponentが生成されていないためエラーが発生する。

*ngOnInitやngAfterViewInitについては次の章で説明する。

ngAfterViewInit() {
  this.headerComponent.callAlert();
}

以下のようにアラートが表示されれば関数を無事呼び出せたことになる。

@ViewChildren

次に@ViewChildrenアノテーションを見てみよう。

@ViewChildは単一のコンポーネントを取得する場合に使用するのに対し、@ViewChildrenはコンポーネントの集まりを取得する際に使用する。

先ほど作成した関数を@ViewChildrenを使い呼び出してみよう。

コンポーネントを参照するために「#」と共に名称を与える。

<app-header #header></app-header>

コンポーネント内でViewChildrenを宣言する。先ほど与えた名称は丸カッコ内に#無でstringとして記述する。

ViewChildrenはコンポーネントの集まりをQueryListに格納するので、QueryListをデータ型として指定する。

@ViewChildren('header') headerComponent: QueryList;

ngAfterViewInit内で関数を呼び出すが、まず最初にQueryListから目的のコンポーネントを見つける必要があるのでfind関数を使用する。

ngAfterViewInit() {
  (this.headerComponent.find((component: any) => component instanceof HeaderComponent) as HeaderComponent).callAlert();
}

コンポーネントのライフサイクル

次にコンポーネントのライフサイクルについて勉強したい。

「コンポーネントのライフサイクル」とはそのコンポーネントが生成されてから消失するまでの一連のサイクルを指す。

Angularではライフサイクルのフェーズ毎にライフサイクルメソッド(ライフサイクルフック)が用意されており、コンポーネントにそれらのメソッドをインターフェイスとして実装することにより、目的のフェーズ内で処理をおこなうことを可能としている。

以下の図はAngularコンポーネントのライフサイクルを表している。

次からはこのライフサイクルの中でも特に重要な「constructor」「ngOnInit」「ngAfterViewInit」「ngOnDestroy」について順に説明していく。

constructor

Angularのライフサイクルの最初に呼び出される。

constructorではコンポーネントに必要となるサービスなどのディペンデンシーを宣言する。

コンポーネントを初期化するための処理はngOnInitでおこなったほうがいいだろう。

ngOnInit

ngOnInit関数はライフサイクルの中でも中心的な役割を果たす。

Angular CLIを使用してコンポーネントを新規に作成した場合はngOnInitはデフォルトで備わっている。

画面を初期化するために必要となる処理の多くはngOnInitで処理するべきだろう。

@Inputアノテーションを通じて渡されたプロパティはconstructorの段階では読み込まれていないが、ngOnInitの段階では読み込まれている。

ngAfterViewInit

AfterViewInitは子コンポーネントを含めたビューの描画が完了した際に呼び出される。

このフェーズでおこなう処理の例としては、描画されたコンポーネントの幅を取得してアイコンを差し替えるなどといったものが想定できる。

ngOnDestroy

ngOnDestroyはコンポーネントがメモリから破棄される際に呼び出される。

ここでは、設定したタイマーを解除したり、購読しているSubscriptionを解除(unsubscribe)したりといったコンポーネントの後処理をおこなう。