Riotでカウントアップタイマーを作ってみる

[前回](http://qiita.com/t-yng/items/a6a46845cefc6bd60233)はReact+Reduxを使って、カウントアップタイマーを作ってみました。

今回はReactと同様にコンポーネントベースの軽量なViewライブラリである[Riot](http://riotjs.com/ja/)を使って、同様のアプリケーションを作ってみたいと思います。

[t-yng/riot-timer](https://github.com/t-yng/riot-timer)

![20161207231006.png](https://qiita-image-store.s3.amazonaws.com/0/54838/21d6d0f2-54d0-ea29-7081-50ef85eb68ba.png)

## 開発環境の構築

前回と同様にビルドツールとしてwebpackを使い、babel-loaderを使ってES6のコンパイルをしていきます。  
またRiotではカスタムタグと呼ばれるもので、コンポーネントを定義していきます。
そのため、カスタムタグファイルを読み込むために、tag-loaderを利用します。

```
$ npm i --save-dev webpack babel babel-core babel-loader tag-loader
```

webpackの設定をしていきます。
`webpack.config.js`を次の内容で作成します。

```javascript:webpack.config.js
module.exports = {
  entry: './js/main.js',
  output: {
    path: 'dist',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      { test: /\.js$|\.tag$/, exclude: /node_modules/, loader: 'babel-loader' },
      { test: /\.tag$/, exclude: /node_modules/, loader: 'tag-loader' }
    ]
  }
}
```

次にRiotをインストールします。

```
$ npm i --save riot
```

`pakcage.json`にwebpackを利用したビルドスクリプトを追記しておきます。

```json:package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
```

## ディレクトリ構成
今回のアプリケーションのディレクトリ構成です。

```bash
$ tree -L 2 -I 'node_modules'
.
├── dist
│   └── bundle.js
├── index.html
├── js
│   └── main.js
├── package.json
├── tags
│   ├── timer-button.tag
│   └── timer.tag
└── webpack.config.js
```

## カスタムタグの実装
Riotではカスタムタグを実装することで、コンポーネントを定義することができます。

> カスタムタグは、関連するHTMLとJavaScriptをくっつけて再利用可能なコンポーネントとしてまとめます。   
[Riot.js](http://riotjs.com/ja/)

コンポーネントの構成は前回と同様にします。

![20161207231207.png](https://qiita-image-store.s3.amazonaws.com/0/54838/c22c17ac-36ec-2301-84a3-2a158289cd3e.png)

### ボタンのカスタムタグを実装
ボタン部分のカスタムタグ`timer-button.tag`を実装していきます。

`opts.title`はボタンに表示するテキストです。
`opts.observable`はタグのイベントを監視するための、riotが提供しているオブジェクトです。
`opts.event`はボタンに割り当てる役割(START, STOP, RESET)を表しています。

ボタンのクリックイベント時に、obsarvableインスタンスを通して、`opts.event`をイベント発火させることで、タイマータグにて、どのボタンが押されたかを通知します。

- [オブザーバブル · Riot.js](http://riotjs.com/ja/api/observable/)
- [【Riot.js】タグ間で動作を連動させる](http://qiita.com/iwamatsu0430/items/49ea5ad14c631f323d22)

```html:tags/timer-button.tag
<timer-button>
  <button type="button" onclick={ click }>{opts.title}</button>

  <script type="text/javascript">
    this.observable = opts.observable
    this.event = opts.event

    this.click = () => {
      this.observable.trigger(this.event)
    }
  </script>
</timer-button>
```

### タイマーのカスタムタグを実装
続いてタイマーの時間を表示するカスタムタグ`timer.tag`を実装していきます。

タイマータグでは、obsarvableを利用してボタンのイベントを監視します。そして、クリックされたボタンに応じて処理を実行していきます。
タイマー時間の表示の更新は、`timerTag.update({time: timerTag.time + 1})`とカスタムタグの内部状態を新たに更新して、表示を更新するすることで行なっています。

```html:tags/timer.tag
<timer>
  <span>{createTimeText(time)}</span>

  <script type="text/javascript">
    const self = this
    this.time = opts.time
    this.timerID = opts.timerID
    this.observable = opts.observable

    // スタートボタンのイベントを監視
    this.observable.on('start', () => startTimer(self))

    // ストップボタンのイベントを監視
    this.observable.on('stop', () => stopTimer(self))

    // リセットボタンのイベントを監視
    this.observable.on('reset', () => resetTimer(self))

    /**
     * 時間(秒)をテキストに変換
     * @param {number} time タイマーが示す時間の秒数
     */
     this.createTimeText = time => {
      // 秒数から時間を取得
      const hour = second2Hour(time)

      // 秒数から分を取得
      const minute = second2Minute(time)

      // 秒数を60進数に変換
      const second = time - (hour * 3600 + minute * 60)

      // タイマーの時間を示すテキストを生成
      return [hour, minute, second].map(zeroPadding).join(':')
    }

    /*
     * 引数で渡された数値をゼロ埋めした2桁の文字列を返す
     * @param {number} num ゼロ埋めをしたい数値
     * @return ゼロ埋めされた文字列
     */
    function zeroPadding(num) {
      return ('00' + num).slice(-2)
    }

    /**
     * 秒数から時間を取得
     * @param {number} seconds 秒数
     */
    function second2Hour(seconds) {
      return Math.floor(seconds / 3600)
    }

    /**
     * 秒数から分を取得
     * @param {number} seconds 秒数
     */
    function second2Minute(seconds) {
      return Math.floor(seconds / 60)
    }

    /**
     * タイマーを開始する
     */
    function startTimer(timerTag) {
      // 定期実行のタスクが無い時だけ、定期実行を開始
      if(!timerTag.timerID) {
        const timerID = setInterval(() => updateTimer(timerTag), 1000)
        timerTag.update({ timerID })
      }
    }

    /**
     * タイマーを止める
     */
    function stopTimer(timerTag) {
      if(timerTag.timerID) {
        clearInterval(timerTag.timerID)
      }
    }

    /**
     * タイマーをリセットする
     */
    function resetTimer(timerTag) {
      timerTag.update({ time: 0 })
    }

    /**
     * タイマーを1秒更新する
     * @param {number} time 更新前の秒数
     * @return 更新後の秒数
     */
    function updateTimer(timerTag) {
      // 明示的にupdate()を呼ぶ必要がある
      // http://riotjs.com/ja/api/#section-5
      timerTag.update({time: self.time + 1})
    }

  </script>

  <style>
    span {
      font-size: 30px;
      color: white;
    }
  </style>
</timer>
```

### HTMLの作成

タイマーを表示する`index.html`を作って、作成したカスタムタグを埋め込んでいきます。

```html:index.html
<body>
  <div class="content">
    <timer></timer>
    <div class="button-list">
      <timer-button id="start-button"></timer-button>
      <timer-button id="stop-button"></timer-button>
      <timer-button id="reset-button"></timer-button>
    </div>
  </div>

  <script src="dist/bundle.js" charset="utf-8"></script>
</body>
```

### コンポーネントのマウント
最後に、HTMLに埋め込んだカスタムタグをマウントする処理を`main.js`に実装していきます。

マウントの仕方はReactを大きくは変わらないです。
別ファイルで定義した、カスタムタグを読み込んで、それぞれ初期値を指定することで、コンポーネントをマウントしています。
特別なことは特に無いですが、注意点としてはイベント監視のために、共通のobsarvableインスタンスを渡しているところです。

以上でカウントアップタイマーの完成です！

```javascript:js/main.js
require('../tags/timer.tag')
require('../tags/timer-button.tag')
const riot = require('riot')

const timerObservable = riot.observable()

riot.mount('timer', {
  time: 0,
  observable: timerObservable
})

riot.mount('#start-button', {
  title: 'START',
  event: 'start',
  observable: timerObservable
})

riot.mount('#stop-button', {
  title: 'STOP',
  event: 'stop',
  observable: timerObservable
})

riot.mount('#reset-button', {
  title: 'RESET',
  event: 'reset',
  observable: timerObservable
})
```