Riot.jsで作る期間選択UI

Riot.js Advent Calendar の22日目の記事です。
石原と申します。フリーランスでWebやアプリのデザインと、フロントエンド周りの実装をしております。
この記事ではRiot.js を作って期間選択のUIを実装する流れを書いてみようと思います。
作るもの
コードはこのページに全部書くと見難そうなので下記のリポジトリに上げています。
Riot.js以外の使ったライブラリ
Riot.jsの良さを活用してさらっと簡単なコンポーネントを作ってみようと思ったので、.tagファイル以外のコードは極力省きたくていくつか他のライブラリも使っています
上記の3つもそうですが、Riot.jsもCDNを使用しています。
riot-period
コンポーネントの構成としては
- カスタムで詳細な日付を指定する時に使用する
riot-period-input - プルダウンを表示して「今月」とかで決まった期間を選択する
riot-period-select - それらをまとめた親のコンポーネントである
riot-period
の3つで動いています。
実際に使用する時には下記のような感じでriot-periodを呼び出すだけになっています。
<riot-period></riot-period>
<script src="./javascripts/components/riot-period.tag" type="riot/tag"></script>
<script>riot.mount('*')</script>riot-periodはfromとtoという値をそれぞれ持っていて期間の値を簡単に取り出す事ができるように作ってみました。
updateのタイミングで最新の値が取り出せるので、それを動的に使用するか、若しくは送信ボタンとかを隣につけて送信のタイミングで値を取得してフォームに投げて、グラフを再描画する時なんかに使います。
riot-period-input
1番小さいコンポーネント、riot-period-inputはinputタグだけでできています。
カスタムの日付入力のinputの要素になります。
日付の入力の煩わしさを感じさせないようにする
日付の選択UIは、PCから画面を操作する場合にカレンダーのUIを使いたいので、Pikaday プラグインを入れました。
しかしカレンダーのUIはスマホから操作している場合には逆に使いにくいですよね。
スマホではinputタグを[type=date]に設定しておくとOS搭載の日付選択UIが表示されますが、やはりデフォルトのものが一番使いやすいと思うのでスマホの時はそちらを表示させるように切り替えます。
mount時のタイミングにontouchstartの有無を確認して振り分けます。
this.on('mount', function() {
var input = this.root.querySelector('input')
if('ontouchstart' in window) {
// タップイベントがある場合にはinput[type=date]に変更
} else {
// タップイベントが無い場合にはPikadayプラグインを使用
}
})min、maxの値を設定して想定外の数値が入らないようにする
値が変更された際、想定外の数値(例えば期間のfromよりtoの方が小さい値になってしまわないように)が入らないように制御します。
スマホの場合は$(input).on('blur', function(e) { … })で、PCの場合には Pikaday の初期設定でonSelect: function(date) { … }のタイミングで、inputタグに設定したmin、maxの値と新しい値とを比較して必要に応じて訂正する処理を入れています。Moment.js のisBetweenを使うと便利です。
var input = this.root.querySelector('input')
var now = input.value
if(moment(now).isBetween(input.min, input.max)) {
//新しい値に変更する
} else {
//元の値に戻す
}また、Pikaday はinputタグが[type=text]になっており、保持している値もYYYY/MM/DDとなっています。
Moment.jsの初期化で使用できる書式はYYYY-MM-DDで、input[type=date]のvalueの値もYYYY-MM-DDなので、Pikadayを使用している場合にはアップデートの際にはそちらに合わせて書式を変更する手間がかかりました。といってもinput.value.replace(/\//g, "-“)で置換するだけですが。
riot-period-select
日付選択の項目でドロップダウンのメニューが勝手に閉じないようにする
期間選択のUIは「今月」とか「過去半年」とかの予め決められた期間を選択するボタンと、カスタムで期間の始まりと終わりをそれぞれ指定するUIの2つが入ったドロップダウンメニューで構成されています。
ドロップダウンの部分はBootstrapの機能なのですが、ドロップダウンメニューはデフォルトでは項目を触ったり領域外をクリックすると勝手に閉じるようになっています。
項目を何個か順にクリックして切り替えて考える事もあるでしょうし、カレンダーのUIから日付を選択する度にドロップダウンが閉じてしまうと困るので、勝手に閉じないようにBootstrapのイベントをe.stopPropagation()でキャンセルするようにしておきます。
$(document).on('click', '.dropdown-menu', function(e) {
e.stopPropagation()
});項目選択のUIで、クリックしたボタンからコンポーネントにデータ属性を介して値を渡す
項目選択のUIでは、years、monthsなどの期間のタイプとその期間の長さの2つの値を元に必要な日付を割り出すようにしています。
各項目はdata-period、data-period-typeの2つの値を持っていて、onclickのタイミングで受け取れる引数eのe.targetから対象のaタグが取得できますので、そこからデータ属性を介して必要な値を取得して計算します。
onclick(e) {
e.preventDefault()
var period = e.target.getAttribute('data-period')
var type = e.target.getAttribute('data-period-type')
...
}項目選択のUIで、現在選択されているリンクはアクティブの状態に切り替える
Riot.jsではDOMにクラスを設定する場合は、普通にhtmlを書く時と同様にclass=“className"と入力する事ができますが、動的にクラスを付け替えたい場合にはclass={className: true}等で指定する事ができます。
trueの箇所にはBooleanではなく変数を使う事が可能ですが、状況に応じて変化する場合などに関数をそのまま入れる事も可能です。
今回はボタンが持っている値をisActiveという関数に渡してボタンのクラスを付け替え、アクティブ状態を切り替えるように作ってみました。(デザイン上では太字になっているだけですが…)
<a class={dropdown-item: true, active: isActive(1, 'months')} ...>今月</a>isActive関数は渡した値と現在のコンポーネントが保持している値を比較してtrue、falseを返すという処理を行なっているだけです。
isActive(period, type) {
if(this.period == period && this.period_type == type) {
return true
} else {
return false
}
}aタグが持っているdata-period、data-period-typeの値を比較するのでthisとかでaタグ自体を渡せたら良いのですが、どうやら出来なさそうだったので引数でデータ属性の値を直接渡しています。
カスタムの日付入力項目のコンポーネントを設置する
カスタムの日付を入れるUIはriot-period-input要素を2つと決定ボタンを並べて作成しています。
<form onsubmit={ submit }>
<riot-period-input name="from" v={ from.format('YYYY-MM-DD') }></riot-period-input>
〜
<riot-period-input name="to" v={ to.format('YYYY-MM-DD') }></riot-period-input>
<button type="submit" class="btn btn-secondary">決定</button>
</form>期間の最大、最小の値を設定するのは本来ならばriot-period-inputの中に入れたい関数ですが、期間の始まりの日付は期間の終わりの日付の1日前にしたいのでmin、maxの値を更新するような関数はこっちに書いています。
setMinMax(self) {
var input_from = self.tags.from.root.querySelector('input')
var input_to = self.tags.to.root.querySelector('input')
...
}this.tags.tagNameでコンポーネントの中のタグが取得できるのでそこから中のinputを取得して値を書き換えています。
まとめ
以上のような感じで作成しています。 ざっくりと何をやっているかの説明だけを書いたので、詳細なコードは こちら をご覧ください。
Riot.jsは1つ1つの機能毎に気軽にコンポーネントを作る事ができ、後からコードを見返した時にも見通しも良くなるしシンプルなので非常に使い勝手が良くて好きです。 当初結構面倒な機能だと思ったのですが、200行に満たないコードでjsとhtmlを書く事が出来ました。
なんでこんなのを作ったかというと今人知れず個人的に作っているサービスで、( ログブック のアプリなのですが統計のグラフを表示したくて)簡単に期間を選択できるUIが欲しかった、という理由がありました。
23日の Riot.js Advent Calendar は clown0082 さんの記事 Riot.js + webpack + ES6(Babel, buble)での開発環境構築例 ※追記webpack2 です。
