Aurelia で Date Range Picker を使う

今回は Aurelia で Date Range Picker を使う方法について書いていきます。

最終的に、下図の「Expiry Period」のようになります。
日付を範囲選択したい場合に便利ですので、宜しければご覧ください。

![20180712_160701.png](https://qiita-image-store.s3.amazonaws.com/0/89904/ae5764f0-ff07-fb4e-4f87-1e73e77f7e08.png)

また、解説不要な人のために GitHub リポジトリの URL を載せておきますね。
https://github.com/phayacell/aurelia-daterangepicker

該当のコミットはこちら。
https://github.com/phayacell/aurelia-daterangepicker/commit/23643928914ce9e98958e04f5ec20c5907092b74

## [Date Range Picker](http://www.daterangepicker.com/) とは

> A JavaScript component for choosing date ranges, dates and times.

[jQuery](https://jquery.com/) と [Moment.js](https://momentjs.com/) に依存する日付範囲選択のためのツールですね。時刻も含めて選べます。
動くサンプルは [Examples](http://www.daterangepicker.com/#examples) に載っているので、イメージを掴みやすいです。

## TL;DR

- 日付の範囲選択を Datepicker でゴニョゴニョするのが疲れた人は Date Range Picker がオススメだよ
- Aurelia でどう使うのか、導入する手順を書いたよ

## 対象読者

Aurelia で Date Range Picker を使おうと考えている人。

## 前提環境

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.11.6
BuildVersion:	15G22010
```

```bash
$ yarn -v
1.7.0
```

## 導入するもの

- [Date Range Picker](http://www.daterangepicker.com/)

## 事前準備

[Aurelia - Contact Manager Tutorial](https://aurelia.io/docs/tutorials/creating-a-contact-manager/) を済ませておいてください。[^1]
今回はそちらに「有効期限」を追加する形で実装していきます。

Tutorial をやるのが面倒な人は、以下で引っ張ってきても良いです。
Date Range Picker を導入する直前の状況を再現できます。

```bash
$ git clone git@github.com:phayacell/aurelia-daterangepicker.git
$ cd aurelia-daterangepicker
$ git checkout 47e25155d22943a373266bd9ce79b021a1c8eae4
```

## 導入手順
### 1. Date Range Picker をインストール

npm で管理されているので、インストールは楽ちんです。
私は yarn を使っているので以下のコマンドです。npm の人は適宜読み替えてください。

```bash
$ yarn add daterangepicker
```

### 2. Date Range Picker を扱う Custom Element を作成

`src` 配下に、以下の3ファイルを作成してください。

- `date-range-picker.html`
- `date-range-picker.js`
- `date-range-picker.css`

以降、それぞれに記載する内容と解説を書いていきます。

#### src/date-range-picker.html

必要な css を読み込んで、側を作るだけなので、そこまで難しくないです。

`.form-control` クラスを設定しているので [Bootstrap の Form controls](https://getbootstrap.com/docs/4.1/components/forms/#form-controls) に則ることができるようにしてあります。
`.input-group` クラスなども適用できるってことですね。

```html:src/date-range-picker.html
<template class="form-control">
  <require from='daterangepicker/daterangepicker.css'></require>
  <require from='./date-range-picker.css'></require>

  <div class="date-range-picker">
    <span class="date-range">
      <time datetime="${startDate}">${startDate}</time>
      <span>-</span>
      <time datetime="${endDate}">${endDate}</time>
    </span>
  </div>
</template>
```

#### src/date-range-picker.js

いくつかのブロックに分けて解説するので、先に完成形を書いておきます。

```js:src/date-range-picker.js
import { inject, bindable, bindingMode } from 'aurelia-framework';
import $ from 'jquery';
import moment from 'moment';
import 'daterangepicker';

@inject(Element)
export class DateRangePicker {
  @bindable({defaultBindingMode: bindingMode.twoWay}) startDate;
  @bindable({defaultBindingMode: bindingMode.twoWay}) endDate;
  @bindable({defaultBindingMode: bindingMode.oneTime}) options;

  constructor(element) {
    this.element = element;
  }

  created(owningView) {
    this.owningView = owningView;
  }

  attached() {
    this.options = $.extend(true, {
      locale: {
        format: moment.HTML5_FMT.DATE
      }
    }, this.options);

    this.options = $.extend(this.options, {
      startDate: moment(this.startDate, this.options.locale.format),
      endDate: moment(this.endDate, this.options.locale.format)
    });

    $(this.element).daterangepicker(this.options, (start, end) => this.apply(start, end));
  }

  apply(startDate, endDate) {
    this.startDate = startDate.format(this.options.locale.format);
    this.endDate = endDate.format(this.options.locale.format);
  }

  startDateChanged(newValue) {
    if (this.owningView.isAttached) {
      $(this.element).data('daterangepicker').setStartDate(newValue);
    }
  }

  endDateChanged(newValue) {
    if (this.owningView.isAttached) {
      $(this.element).data('daterangepicker').setEndDate(newValue);
    }
  }
}
```

まずは import 部分。
使用する jQuery, Moment.js を読み込み、Date Range Picker を読み込ませて jQuery に Mixin しています。

```js:src/date-range-picker.js
import { inject, bindable, bindingMode } from 'aurelia-framework';
import $ from 'jquery';
import moment from 'moment';
import 'daterangepicker';
```

次に宣言。
`startDate` と `endDate` は双方向バインドする必要があるので `bindingMode.twoWay` を指定。
`options` は Date Range Picker のオプションを利用側から扱えるようにしています。オプションの詳細は [Options](http://www.daterangepicker.com/#options) で。

```js:src/date-range-picker.js
@inject(Element)
export class DateRangePicker {
  @bindable({defaultBindingMode: bindingMode.twoWay}) startDate;
  @bindable({defaultBindingMode: bindingMode.twoWay}) endDate;
  @bindable({defaultBindingMode: bindingMode.oneTime}) options;
```

次にコンストラクタとか。
`element` は Date Range Picker を使うために必要です。jQuery Plugin を使う場合によく見ますね。
`owningView ` は `startDate` と `endDate` の変更監視メソッド内で attached 後かどうかを判定するために使います。

```js:src/date-range-picker.js
  constructor(element) {
    this.element = element;
  }

  created(owningView) {
    this.owningView = owningView;
  }
```

次に `attached` です。

`$.extend(true, {/* default */}, {/* override */})` の形で書いていますので、bind された options の値を優先させます。
また、`startDate` と `endDate` は twoWay bind で渡された値を優先させています。

options の設定が終わったら、Date Range Picker の初期化。
値が反映された時のメソッドは `apply(start, end)` で定義してあります。

また、`moment.HTML5_FMT.DATE` は [Special Formats](https://momentjs.com/docs/#/parsing/special-formats/) を参考にしています。

```js:src/date-range-picker.js
  attached() {
    this.options = $.extend(true, {
      locale: {
        format: moment.HTML5_FMT.DATE
      }
    }, this.options);

    this.options = $.extend(this.options, {
      startDate: moment(this.startDate, this.options.locale.format),
      endDate: moment(this.endDate, this.options.locale.format)
    });

    $(this.element).daterangepicker(this.options, (start, end) => this.apply(start, end));
  }
```

次に Date Range Picker の反映処理。
引数で渡ってくる値は Moment.js のインスタンスになっていますので、これを文字列に変換しています。
twoWay bind している `startDate` と `endDate` に設定しているので、この Custom Element の利用側の変数にも反映されます。

```js:src/date-range-picker.js
  apply(startDate, endDate) {
    this.startDate = startDate.format(this.options.locale.format);
    this.endDate = endDate.format(this.options.locale.format);
  }
```

最後に `startDate` と `endDate` の変更検知メソッド。
Aurelia では [Observable Properties](https://aurelia.io/docs/binding/binding-observable-properties/) を使って変数の変更検知を行うことができますが、bind される変数も使えます。
ここでは、この Custom Element 外から値を変更された場合に Date Range Picker の選択範囲に反映されるようにしています。

ちょっと特殊なのは `if (this.owningView.isAttached)` ですね。
`$(this.element).data('daterangepicker')` は attached されていないと存在しないため、この判定を行っています。

```js:src/date-range-picker.js
  startDateChanged(newValue) {
    if (this.owningView.isAttached) {
      $(this.element).data('daterangepicker').setStartDate(newValue);
    }
  }

  endDateChanged(newValue) {
    if (this.owningView.isAttached) {
      $(this.element).data('daterangepicker').setEndDate(newValue);
    }
  }
```

#### src/date-range-picker.css

最後にスタイルの設定です。
flexbox を使ったり、擬似クラスの before/after を使ったりしています。
あと、チュートリアルで組み込まれていた [Font Awesome](https://fontawesome.com/) も使っています。

```css:src/date-range-picker.css
.date-range-picker {
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.date-range-picker::before {
  font-family: FontAwesome;
  content: '\f073';
  padding-right: .5rem;
  flex: 0 0 auto;
}
.date-range-picker::after {
  font-family: FontAwesome;
  content: '\f0d7';
  padding-left: .5rem;
  flex: 0 0 auto;
}

.date-range-picker .date-range {
  flex: 1 1 auto;
  display: flex;
  justify-content: space-around;
  align-items: center;
}
```

### 3. 作成した Custom Element を使う

いよいよ Contact で「有効期限」を扱える手順です。

Contact Manager では WebApi に初期データが作られているので、これに有効期限の from と to を追加します。

```js:src/web-api.js
let contacts = [
  {
    id: getId(),
    firstName: 'John',
    lastName: 'Tolkien',
    email: 'tolkien@inklings.com',
    phoneNumber: '867-5309',
    expiryFrom: '2017-01-01',
    expiryTo: '2019-01-01'
  },
  {
    id: getId(),
    firstName: 'Clive',
    lastName: 'Lewis',
    email: 'lewis@inklings.com',
    phoneNumber: '867-5309',
    expiryFrom: '2017-01-01',
    expiryTo: '2019-01-01'
  },
  {
    id: getId(),
    firstName: 'Owen',
    lastName: 'Barfield',
    email: 'barfield@inklings.com',
    phoneNumber: '867-5309',
    expiryFrom: '2017-01-01',
    expiryTo: '2019-01-01'
  },
  {
    id: getId(),
    firstName: 'Charles',
    lastName: 'Williams',
    email: 'williams@inklings.com',
    phoneNumber: '867-5309',
    expiryFrom: '2017-01-01',
    expiryTo: '2019-01-01'
  },
  {
    id: getId(),
    firstName: 'Roger',
    lastName: 'Green',
    email: 'green@inklings.com',
    phoneNumber: '867-5309',
    expiryFrom: '2017-01-01',
    expiryTo: '2019-01-01'
  }
];
```

そしたら、ContactDetail で扱えるようにします。

```html:src/contact-detail.html
<template>
  <require from="./date-range-picker"></require>

<!-- 中略 -->

        <div class="form-group row">
          <label class="col-sm-3 col-form-label">Expiry Period</label>
          <div class="col-sm-9">
            <date-range-picker start-date.bind="contact.expiryFrom" end-date.bind="contact.expiryTo"></date-range-picker>
          </div>
        </div>
```

### 4. `au run` する

これで準備完了です。
Aurelia を起動すれば、「有効期限」が追加され、Date Range Picker で扱えるようになっています。

```bash
$ au run
```

もし上記で起動できなければ `au run --watch` を試してください。

http://localhost:3000/ にアクセスすると、以下のようになっているはずです。

![20180712_160701.png](https://qiita-image-store.s3.amazonaws.com/0/89904/ae5764f0-ff07-fb4e-4f87-1e73e77f7e08.png)

これで完了です。お疲れ様でした。

## おわりに

これで Aurelia で Date Range Picker を扱えるようになりました。
これがあれば Datepicker を2つ使ったまどろっこしい日付範囲選択から脱却できますね。

もし Date Range Picker を日本語化したい場合は、以下の記事を参考にすると良いと思います。
[コピペで動く。bootstrap-daterangepickerの導入と日本語化](https://qiita.com/tkosuga@github/items/a5ad953c691e27e27c65)

それでは、良い Aurelia 生活を。

[^1]: 前はチュートリアルなかった気がしたんですけど、少し見ない間にどんどん変わっていきますね。さすが [未来志向フレームワーク](https://paiza.hatenablog.com/entry/2015/03/02/AngularJS%E3%81%AF%E3%82%82%E3%81%86%E5%8F%A4%E3%81%84%EF%BC%9F%E6%9C%AA%E6%9D%A5%E5%BF%97%E5%90%91%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AFAurelia.JS%E3%82%92%E8%A9%A6%E3%81%97)。