Leaflet でモーダルウィンドウを呼び出す時の注意

ウェブブラウザ上で、[Leaflet](http://leafletjs.com/) を使用して、モーダルウィンドウを出そうとした時に少しハマった点があったので、その解決策を記載します。ちなみに、Leaflet とは Google Maps や 地理院地図 などの地図サービスを JavaScript から操作するためのライブラリです。

## 前提と実装環境
### 前提
今回の実装の前提は以下の通りになります。

- Leaflet で設定した L.circle （以下、点）をクリックするとモーダルウィンドウが現れる
- モーダルウィンドウ内には入力欄があり、モーダルウィンドウを閉じると情報を保存する
- 再度、点をクリックすると保存された情報が表示される

### ハマったときに実装していた環境
実装していたときの環境は、 以下の通りです。ウェブブラウザのクライアントサイドでも、 Node.js 由来の箇所以外は同様だと思われます。

<dl>
<dt>言語</dt>
<dd>TypeScript 1.6</dd>
<dt>モーダルウィンドウを呼び出すために使用したプラグイン</dt>
<dd>leaflet-modal 0.1.3</dd>
<dt>実行</dt>
<dd>Electron 0.33.7</dd>
</dl>

## モーダルウィンドウを呼び出すために使用したプラグイン
今回、モーダルウィンドウを呼び出すのに使用したプラグインは [leaflet-modal](https://www.npmjs.com/package/leaflet-modal) にしました。
理由は以下の通りになります。

- Leaflet のプラグインなので、 Leaflet と相性が良い
- HTML を含めたコードの記述量が比較的少なく済む
- テンプレートを使用できるので、 Jade などからテンプレートを呼び出せば、 JavaScript に HTML の記述が不要で済む

## モーダルウィンドウ実装時の注意

### Leaflet 由来の注意点

Leaflet で `L.circle` は追加した点に対して class は追加できても、 **idは設定できない** ので、各々の `L.circle` を追加する際に一意に扱えるような class を追加する必要があります。これをしないと、モーダルウィンドウを呼び出す時にどの点の情報なのかを得るのが難しくなってしまいます。class に追加する値は、下記のように点の中心の緯度・経度を使った値にすると他の点とバッティングしづらくなると思います。class には、各点の状態も追加しておくと後で処理を行いやすいと思います。

```js:add-point-specified-class.js
let circle = L.circle(
  [Number(latlng[0]), Number(latlng[1])],
  20,
  {
    className : `point-${latlng[0]}-${latlng[1]} input-nothing`
  }
);

circle.on(
  'click',
  circleClicked // クリック時の動作、モーダルウィンドウを開く
);

circle.addTo(map);
```

### leaflet-modal 由来の注意点
leaflet-modal では、以下で列挙することに注意する必要があります。

- モーダルウィンドウを閉じる処理の記述
- モーダルウィンドウを閉じるときの保存処理

#### モーダルウィンドウを閉じる処理
点をクリックしてモーダルウィンドウを開く時の処理に関しては、 `L.Map.openmodal()` で定義することが可能です。しかし、 **モーダルウィンドウを閉じる時の処理は `L.Map.closeModal()` に記述することはできません。** モーダルウィンドウを閉じるイベント時に処理を書こうとしても、 **`modal.hide` は閉じるアニメーションが行われている間に何度か呼ばれてしまうので、1回だけ処理したいようなことには向いていません。** 唯一可能なのは、 `L.Map.openModal` で指定するオプションの中にある、 `onHide` に処理を記述することです。ここに記述することで、閉じた時に一回だけ呼ぶ処理を書くことができます。
クリック時にイベントの対象を上記で設定した一意に扱えるような class を `event.originalEvent.target.classList` から取得しようとすると、変数も長く冗長になってしまうので、 `event.target.getLatLng()` を使って緯度経度を求め、その値から class を取得すると短く書くことができると思います。

####モーダルウィンドウを閉じるときの保存処理
各点のデータの保存は、モーダルウィンドウを閉じるときにクリックした点を識別するための名前とデータの紐付けを行って、 sessionStorage に入れれば実現することができます。しかし、先ほど出てきた `onHide` では **クリックイベントの発火時にはあったクリックされた要素のデータが存在しないため、どの点に対して設定した情報なのかを紐付けられなくなってしまいます。** そこで、点をクリックしたときにどの点がクリックされたかの情報を一時的に保存し、 `onHide` で保存するときにその情報を呼び出し、保存が終わったら情報を削除することで、点とデータの紐付けを行うことができます。

2点を踏まえて点をクリックしたときの処理をコードにすると以下のようになると思います。

```js:clicked-event.js
function circleClicked(event){
  let targetLatLng = event.target.getLatLng();
  let targetItem = `point-${targetLatLng.lat}-${targetLatLng.lng}`;

  // 保存されている値を取り出す
  let pointData = JSON.parse(sessionStorage.getItem(targetItem));
  pointData = pointData || {'data' : ''};

  map.openModal({
    content: inputFormFn(pointData), // モーダルに表示するHTMLのテンプレート
    onHide : function(event){
      let targetItem = sessionStorage.getItem('target-item');

      // 値の保存など閉じる時の処理を書く
      let modalContent = 
        document.querySelector(`#map > div.leaflet-control-container > div.leaflet-modal > div.${event.modal.options.MODAL_CLS} > div > div`);
      let input = {'data' : ''};
      input.data = modalContent.querySelector('#modal-input').value;
                    
      sessionStorage.setItem(targetItem, JSON.stringify(input));

      // 点の状態を変更する
      let targetElm = document.querySelector(
        `#map > div.leaflet-map-pane > div.leaflet-objects-pane > div.leaflet-overlay-pane > svg > g > path[class~="${targetItem}"]`
      );

      if(input.data.length && input.place !== '') {
        targetElm.classList.add('input-complete');
        targetElm.classList.remove('input-nothing');
      }
      else {
        targetElm.classList.remove('input-complete');
        targetElm.classList.add('input-nothing');
      }

      // ターゲットを取得するための値を削除する
      sessionStorage.removeItem('target-item');
    },
  });

  // ターゲットを取得するための値を設定する
  sessionStorage.setItem('target-item', targetItem);
}
```

### TypeScript 使用時の注意
TypeScript で leaflet-modal を呼び出そうとする場合、まだ型定義ファイルが tsd に登録されていない（2015/11/08時点）ため、下記のような定義ファイルを作成する必要があります。

```ts:leaflet-modal.d.ts
declare module L {
	export interface Map {
		openModal(option: {}): void;
		closeModal(): void;
	}
}
```

## 動作デモ
[デモ](https://codepen.io/tom-konda/pen/QeKLXb)

## まとめ
ここまでの内容をまとめると以下の通りになります。

* leaflet を使って、 `L.circle` には id が設定できないので、 class 名を工夫して識別できるようにする
* leaflet-modal でモーダルウィンドウを呼び出す場合は、モーダルウィンドウを閉じるときの処理で工夫が必要になってくる
* 今のところ、 TypeScript で実装する場合に定義ファイルの作成が別途必要になる