RailsAPI×AngularでかっこいいWebアプリを作（ろうと試み）る【AngularMaterialの$mdDialogをものすごく使いやすくする話しかしないのでRails全く関係無い編】


[RailsAPI×AngularでかっこいいWebアプリを作（ろうと試み）る【API呼んでみる編】](http://qiita.com/shitake/items/1ea7f274976c2d674879)の続きです。

## 前回のあらすじ

RailsAPIアプリでAPI作ってAngularで呼ぶ、それだけだ。

今回はシリーズ最後なので思いきってRailsの話一切しません。

[ソース](https://github.com/4take/rails-api-angular)置いとくので参考にどうぞ。

## [AngularMaterial](https://material.angularjs.org/latest/)はご存知？

Googleが提唱するマテリアルデザインをAngularJSで使うためのUIコンポーネントライブラリ。
欲しいコンポーネントはあらかた揃っているし気軽に使えるしかっこいいしとっても重宝しています。ただ個人的にめっちゃ痒いのに手が届かないところが多々有りまして。せっかくなのでそのうちのひとつのオレオレ解決方法をご紹介させていただきたい。

## [$mdDialog](https://material.angularjs.org/latest/demo/dialog)使いづらい問題
AngularMaterialではダイアログを操作するためのサービス`$mdDialog`が用意されているのですが、これが絶妙に使いづらい。Alert,Confirmは特に問題無いがダイアログの中身をカスタムしようとすると急にムムム？な気持ちになる。

というのもカスタムするためにこんな実装が必要なのです。

 - ダイアログに表示するViewを定義する
 - ダイアログに表示するViewのActionをハンドリングするControllerを定義する
 - 呼び出し元の画面とは$scopeを通じてやり取りする

この場合、ダイアログの数だけViewとControllerが量産される。
さらにせっかくControllerで制御しているにも関わらず`scopeに変数を生やしまくる`という時代の流れをダッシュで駆け戻るような勇気と行動が必要になる。

もう文章にすると全く分からなかったので久しぶりにExcel使って図を書いてみた。

#### 現状のscopeカオスまっしぐらの図
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/da6e90aa-8f94-eb63-920e-8bacca69ae58.png" width="300px"/>

そうそうこんな感じ。

理想はscopeとっぱらってこうなること。

#### あったらいいな、の図

<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/d933ce42-a441-f352-f05e-1f09735dcd4e.png" width="300px"/>

こうなるとControllerとViewをあえて分ける必要も無いので、定義したComponentを直接Dialogにつっこめるといちばんスッキリする。

#### もっとあったらいいな、の図
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/2eba2640-2f99-719d-ef71-0ccd0526d986.png" width="300px"/>

### ということでこれを実現するために`$mdDialog`をラップした`dialog`というServiceを作った。

まずは手っ取り早く使い方をご紹介。

```
parentからダイアログで表示するchildに初期値を渡す
↓
childで変更した値をparentに通知する
↓
parentで通知を受け取って値を保存してダイアログを閉じる
```

というフローを実現する場合これだけでOK

```parent.ts
///<reference path="../typings/bundle.d.ts"/>

import angular = require('angular')
import Dialog from '../services/dialog'

class Parent {
	private value: string = 'しょきち'

	static $inject = ['dialog']
	constructor(private dialog: Dialog) {
	}

	private showDialog() {
		this.dialog.show(
			'タイトル',
			`<child value="$ctrl.value" call-back="$ctrl.callBack(newValue)"/>`,
			{
				locals: {
					value: this.value,
					callBack: newValue=>{
						this.value = newValue
						this.dialog.cancel()
					}
				}
			}
		)
	}
}

angular.module('App').component('parent', {
	controller: Parent,
	bindings: {},
	template: `
	<md-button class="md-raised md-primary" ng-click="$ctrl.showDialog()">ダイアログ開くよ</md-button>
	<p>{{$ctrl.value}}</p>
		`
})
```

```child.ts
///<reference path="../typings/bundle.d.ts"/>

import angular = require('angular')

class Child {
	private value: string
	private callBack: (newValue: {newValue: string})=> void

	static $inject = []

	constructor() {
	}

	private save() {
		if(this.callBack) {
			this.callBack({newValue: this.value})
		}
	}
}

angular.module('App').component('child', {
	controller: Child,
	bindings: {
		value: '=',
		callBack: '&'
	},
	template: `
	<md-input-container>
		<label>Value</label>
		<input ng-model="$ctrl.value">
	</md-input-container>
	<md-button class="md-raised md-primary" ng-click="$ctrl.save()">保存</md-button>
		`
})
```

### 解説
ポイントは`parent.ts`のこの部分

```
this.dialog.show(
	'タイトル',
	`<child value="$ctrl.value" call-back="$ctrl.callBack(newValue)"/>`,
	{
		locals: {
			value: this.value,
			callBack: newValue=>{
				this.value = newValue
				this.dialog.cancel()
			}
		}
	}
	)
```

第二引数でダイアログの中身を定義する際に出て来る`$ctrl`。
こいつに生やすプロパティ名・その中身の組み合わせを第三引数の`locals`で指定する。

```
$ctrlにvalueというプロパティを生やして、そこにparentが現在保持しているvalueプロパティの値を渡してね。
```
という指定をすると後は`dialog`サービスが勝手に値の受け渡しを自動でしてくれるという仕組み。

### 実行してみる
#### ① 初期値で`しょきち`が表示される
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/e8914b84-21d2-d191-bdc8-8f23a72fe028.png" width="300px"/>

#### ②ダイアログを開くとchildに初期値が渡っている
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/fa0d4a44-6050-4197-852e-4895922d788a.png" width="300px"/>

#### ③値を変更して・・・
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/74e704b9-2f72-1ead-3cab-c3fe7f4e10ea.png" width="300px"/>

#### ④保存を押すと、ほーらね！
<img src="https://qiita-image-store.s3.amazonaws.com/0/127115/1c6bb09c-3a15-c308-1c52-b48bd1186166.png" width="300px"/>

`dialog`内部を見ていただくと分かる通り`DialogController`が定義されていて、`locals`の情報を受け流しています。

```dialog.ts
///<reference path="../typings/bundle.d.ts"/>

import angular = require('angular')
import md = angular.material

export class DialogOptions {
	locals: any
}

export default class Dialog {
	static $inject = ['$mdDialog']
	constructor(private mdDialog: md.IDialogService) {
	}

	// template : 表示htmlをつっこむ。
	show(title: string, template: string, options?: DialogOptions) {
		this.mdDialog.show({
			controller: DialogCtrl,
			controllerAs: '$ctrl',
			bindToController: true,
			template:
				`'<md-dialog aria-label="dialog">
              <md-toolbar>
                <div class="md-toolbar-tools">
                  <h2> ${title} </h2>
                  <span flex></span>
                  <md-button class="md-icon-button" ng-click="$ctrl.cancel()" aria-label="close">
                    <ng-md-icon class="icon-white" icon="clear"></ng-md-icon>
                  </md-button>
                </div>
              </md-toolbar>
              <md-dialog-content>
                <md-content>
                 ${template}
                </md-content>
              </md-dialog-content>
            </md-dialog>'`,
			locals: {
				options: options && options.locals ? options.locals : {}
			},
			parent: angular.element(document.body),
			clickOutsideToClose: true
		})
	}

	cancel() {
		this.mdDialog.cancel()
	}
}
angular.module('App').service('dialog', Dialog);

class DialogCtrl {
	static $inject = ['$mdDialog', 'options']
	constructor(
		private mdDialog: ng.material.IDialogService,
		private options: any) {
		// DialogController経由で子Componentに渡す
		if (this.options) {
			for (let l in this.options) {
				(<any>this)[l] = this.options[l]
			}
		}
	}

	cancel = () => { this.mdDialog.cancel() }
}

```

### この方法を使うと

<Font color="blue">いいこと① ダイアログ専用のControllerとViewが量産されない</font>
<Font color="blue">いいこと② ダイアログ呼び出し元の$scopeが汚染されない</font>
<Font color="blue">いいこと③ 子コンポーネントはダイアログを一切意識する必要が無い</font>
<Font color="blue">いいこと④ AngularMaterial側でこの問題が解決された際にdialogサービスだけ直せば事足りる（たぶん）</font>

いいことばっかりと言いたいところですが<Font color="red">ほんのちょっとだけ黒魔術臭が漂っている気がしなくもないかな。</font>


あくまで参考までに。