Edited at

東京メトロAPI+vue.js = トイレ検索

More than 3 years have passed since last update.

東京メトロが持つデータがコンテスト向けに公開されているので、vue.jsを触るネタとして遊んでみた。

https://developer.tokyometroapp.jp

題材はトイレ探し。割と自分が困った体験として、トイレがそもそも改札の中にあるのか外にあるのかがわかんね、ってのがあるので、トイレの場所を改札内外で分けて見れるアプリを作ってみた。

※東京メトロAPIの仕様情報は開発者登録しないと見れない情報なので、ここには書きません。


公開サイト

https://where-is-a-bathroom.herokuapp.com/

で動いています。(バギーだとは思うが)

基本モバイル端末から見ることを前提としてます。

使い方としては、画面上のリストから駅を選択すると、その駅内にあるトイレ情報が見れます。どうやらバリアフリー対応しているものしか東京メトロのAPIから取れないっぽいので、ただのトイレは表示されません。


vue.js

sinatraで書いたので生htmlではないがタグはこんな感じ。bootstrap使ったのでクラス名はそんな感じです。

%div.container-fluid

%div.row
%div.form-group
%div.col-sm-10
%select.form-control.stations{"v-model" => "selected", "v-on" => "change: onChange(this)"}
%option{"v-repeat" => "stations", "value" => "{{name}}"} {{title}}
%div.row.facilities
%div.inside-gate
%h3 改札内:{{insideCount}}
%div.list-group
%div.list-group-item{"v-repeat" => "facilities | filterBy 'true' in inside"}
%h4.list-group-item-heading {{name}}
%p.list-group-item-text
%span{"v-repeat" => "assistant"} {{{$value | pictgram}}}
%div.outside-gate
%h3 改札外:{{outsideCount}}
%div.list-group
%div.list-group-item{"v-repeat" => "facilities | filterBy 'false' in inside"}
%h4.list-group-item-heading {{name}}
%p.list-group-item-text
%span{"v-repeat" => "assistant"} {{{$value | pictgram}}}

タグの属性にどんなデータをバインドするか、表示する際にはどんなフィルターをかけるか、などを記述していく。

selectタグはv-modelを利用して双方向バインディングにした。これにより、セレクトボックスでの変更が即座にデータにも反映される。

トイレのデータとしては改札内/外はただの属性値の違いでしかないが、見た目上は表示を分けたい。リソースとして異なるわけではないのでデータ側に書くのもなぁと思ったら、filterByが使える。

filterByを使うと、引数に指定した値とマッチする要素のみが表示対象となる。ここでは、トイレが改札内にあるかをあらわすinsideキーを対象キーに指定している。指定しないと、データの持つすべての値がマッチング対象となるみたい。マッチロジックの詳細は調べてないので不明(数値と文字列の場合どうなるのか?とか)

pictgramは独自に指定したフィルターで、トイレの補助施設をあらわす文字列からimgタグを生成する。{{{}}}と3つの{で囲むと、エスケープされずにそのまま出力されるので、使うときは注意がいる。

// トイレ情報View

facilitiesView = new Vue({
el: ".facilities",
data: {
facilities: []
},
computed: {
insideCount: function() {
return _.filter(this.facilities, function(facility) {
return facility.inside;
}).length;
},
outsideCount: function() {
return _.filter(this.facilities, function(facility) {
return !facility.inside;
}).length;
}
},
methods: {
setStation: function(name) {
// FACILITIESはあらかじめ定義しておく
this.$data.facilities = FACILITIES[name];
}
}
});

// 駅選択
stationsView = new Vue({
el: ".stations",
data: {
stations: [],
selected: "" // 選択された駅
},
methods: {
updateStations: function() {
$data = this.$data;
navigator.geolocation.getCurrentPosition(function(position) {
// 最寄駅検索
$.getJSON("/stations?lat=" + position.coords.latitude +
"&lon=" + position.coords.longitude).done(function(stations){
$data.stations = stations;
if(stations.length > 0) {
$data.selected = stations[0].name
facilitiesView.setStation($data.selected);
}
});
});
},
onChange: function() {
facilitiesView.setStation($data.selected);
}
}
});

改札内/外のトイレの個数は、computedを使って都度都度計算するようにした。

一つこのやり方でよいのかどうか疑問だったのが、二つのViewModelで連携させる方法。stationsViewで選択された駅をfacilitiesViewに渡すのに、直接参照を持たせている。関連性は強いので良い気もするが、この手の連携で参照持たせ始めると規模が大きくなるにつれて複雑になってきて死ぬことになる。ドキュメントを読めばきっとうまい方法があるのでしょう(楽観)


感想

データに関すること、見た目に関すること、と異なる関心ごとがhtmlとjsでうまく住み分けできそうな感じ。しかも、html側にifforみたいな制御ロジックがぐちゃぐちゃ入るのではなく、タグを使って記述的に書けるのも良い。学習コストもそんなに高くなさそう。ただ、何でもjsで書く世界にどっぷりはまっていると、タグで記述的に表現するのに最初戸惑うかも。

素のbackbone.jsとの比較はあまり意味がない(いろいろプラグインがあるので)けど、vue.js単体のみでそれが実現できるのはやはり良い。