Aurelia hack 03 ～ライフサイクル～

今回は各ページや画面部品（まとめて Component と呼びます）のライフサイクルについて解説します。

# Component ライフサイクル

ページや部品が画面にバインドされるときや表示されるとき、または画面から消えてゆくときに任意のロジックを実行したいという場面はよくありますよね。そんな時に任意に定義できるホックメソッドがあります。

## Component ライフサイクルホック

```js:例
import {inject} from 'aurelia-framework'
import {SomeService} from 'some-service'

@inject(SomeService)
export class SomeComponent {
	
	constructor(someService) {
		// コンストラクタは DI のためのものと心得る
		this.someService = someService;
	}
	
	created(owningView, myView) {
		// constructor の次、View が生成されたときに呼ばれる
	}
	
	bind(bindingContext, overrideContext) {
		// View との data binding が有効になったときに呼ばれる
	}
	
	attached() {
		// DOM に組み込まれたとき（表示されたとき）に呼ばれる
	}
	
	detached() {
		// DOM から消えたときに呼ばれる
	}
	
	unbind() {
		// View との data binding が解除されたときに呼ばれる
	}
	
}
```

### constructor

基本的に Component を自分でインスタンス化することはありません。Component をインスタンス化するのは DI コンテナの仕事で、依存を解決するという側面を持っています。
`@inject` で宣言することで、コンストラクタの引数として必要なサービスが手に入ります。DI を使う場合は必ず書くことになります。

### created

View, ViewModel が生成され、Aurelia の内部的なコントローラに登録されたタイミングで呼び出されます。
第一引数 `owningView` には Component が配置される View
第二引数 `myView` には ViewModel に紐づく View がそれぞれ渡されます。

### bind

View と ViewModel のデータバインディングが有効になったときに呼び出されます。
第一引数 `bindingContext` にはこの Component がバインドされる際のコンテキスト情報が渡されます。
第二引数の `overrideContext` はよりテクニカルな目的のためにあります。
親階層をトラバースするための情報を含むとともに、任意のコンテキストプロパティを追加するために使用することができます。

### attached

こちらはなにかと使うことが多いです。
この Component が晴れて DOM に追加されたときに呼び出されます。
DOM にアクセスする必要があるならこのタイミングです。

たとえば View で `ref` を使うことで任意の要素にアクセスできるようになります。

```html:View
<template>
	<p ref="elm">サンプル</p>
</template>
```

```js:ViewModel
export class Sample {

	attached() {
		console.log(this.elm);
		// -> <p ref="elm" class="au-target" au-target-id="3">サンプル</p>
	}

}
```

### detached

DOM から取り除かれたときに呼び出されます。
後始末として必要な処理がある場合に便利です。

### unbind

detached よりも後に呼び出されますが、違いはまだよくわかりません。。。
名前からしてデータバインディングが解除された後に呼び出されるものと思われます。

# 画面遷移ライフサイクル

こちらはルーティングによって画面遷移が起きるときに呼び出されるメソッドです。

## 画面遷移ライフサイクルホック

```js:例
export class SomePage {
	
	canActivate(params, routeConfig, navigationInstruction) {
		// このページに遷移しようとするときに呼ばれる
		// false を返却することでキャンセルすることができる
	}
	
	activate(params, routeConfig, navigationInstruction) {
		// このページに遷移するときに呼ばれる
		// ルート設定に '/:name' と書けば、このようにパラメータを取得できる。
		console.log(params.name);
	}
	
	canDeactivate() {
		// ほかのページに遷移しようとするときに呼ばれる
		// false を返却することでキャンセルすることができる
	}
	
	deactivate() {
		// ほかのページに遷移するときに呼ばれる
	}
	
}
```

### canActivate

このページに遷移してもよいかどうかをルータに教えるためのホックです。
false を返却すれば画面遷移はキャンセルされ、ブラウザの現在の URL も遷移する前のものに戻ります。
第一引数 `params` はルーティング設定時にパラメータ化した部分の値がオブジェクトとして渡されます。

```js:パラメータ化の例
export class App {
  configureRouter(config, router) {
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'welcome'], name: 'welcome',      moduleId: 'welcome',      nav: true, title: 'Welcome' },
	  { route: 'user/:name', name: 'user', moduleId: 'user' } // <- :name の部分がパラメータ
    ]);

    this.router = router;
  }
}

```

第二引数 `routeConfig` は該当するルート設定、
第三引数 `navigationInstruction` はなんやかんやいろんな情報が含まれています。（`console.log(navigationInstruction)` で表示してみるとよいです）

`routeConfig.navModel` でグローバルナビゲーションに対応するモデルが取れます。
`routeConfig.navModel.setTitle(param.name);` とすれば動的にメニューの表示を変更することもできます。

### activate

このページに遷移するとき、準備をするためのホックです。
引数は canActivate とまったく同じです。

### canDeactivate

このページから離れてもよいかどうかをルータに教えるためのホックです。
false を返却すれば画面遷移はキャンセルされ、このページを表示し続けます。

### deactivate

このページから離れるとき、後処理をするためのホックです。

## 非同期処理

### Promise を使う

画面遷移ライフサイクルホックは、すべて非同期処理に対応しています。
例えばホックのロジックの中でサーバと通信する場合、そのままだと通信が終わるまで待ってくれませんよね。

```js
activate(params) {
	this.client.get('api/getItem', { id: params:id })
		.then(res => this.item = res.item);
}
```

上記の例では、通信が終わる前に画面遷移が完了してしまいます。もちろんそれでもよい場合は少なくないかもしれませんが、画面に表示する内容が準備できるまで待ってほしいということもあります。
そういう時は、Promise を返せばOKです。

```js
activate(params) {
	return new Promise((fulfilled, rejected) => {
		this.client.get('api/getItem', { id: params:id })
			.then(res => {
				this.item = res.item;
				fulfilled();		// このタイミングで遷移が完了する
			});
	}); 
}
```

canActivate, canDeactivate でも同じく Promise を返却しておいて、`fulfilled(false)` とすればキャンセルすることが可能です。

### ES7 async/await を使う

Aurelia で async/await を使う場合、 `build/babel-options.js` に `"runtime"` を追記します。

```js:build/babel-options.js
module.exports = {
  modules: 'system',
  moduleIds: false,
  comments: false,
  compact: false,
  stage: 2,
  optional: [
    "es7.decorators",
    "es7.classProperties",	// <- カンマを忘れずに
    "runtime"				// <- 追記 これで async/await が使えるようになる
  ]
};

```

> `gulp watch` を実行している場合は、一度 Ctrl+C で止めて再起動してください。

例としてひな形 (skeleton-navigation) の users ページの activate を async/await で書き直してみます。

```js:src/users.js(元コード)
// 省略

@inject(HttpClient)
export class Users {
  heading = 'Github Users';
  users = [];
  
  // 省略

  activate() {
	// Promise を返却している
    return this.http.fetch('users')
      .then(response => response.json())
      .then(users => this.users = users);
  }
}
```

```js:src/users.js(async/await)
// 省略

@inject(HttpClient)
export class Users {
  heading = 'Github Users';
  users = [];
  
  // 省略

  // Promise が返却される
  async activate() {
    let res = await this.http.fetch('users');
    this.users = await res.json();
  }
}
```

async を付けることで必ず Promise が返却されるようになりますので、Aurelia 側から見たら挙動は変わっていません。
同期処理と同じように記述できるので非常にシンプルになりますね。
さらに try ~ catch でエラーハンドリングしてあげるとよいかと思います。