Riot.js で bootstrap-carousel タグを実装

概要
----

Riot.js というライブラリがあって最近話題です。

* Riot.js http://riotjs.com/ja/
* Qiita の記事 http://qiita.com/search?q=riot

これを使って bootstrap の carousel を簡単に実装するためのカスタムタグを作ってみました。

https://gist.github.com/osamutake/f800cb0a7c638f058bded6702bc31356

確かにすごく簡単で、便利に使えそうです。

この記事は、次の記事とのマルチポストです：
http://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%2FRiot.js%2Fbootstrap-carousel%E3%82%BF%E3%82%B0%E3%82%92%E5%AE%9F%E8%A3%85

Bootstrap の carousel
----

Bootstrap を使うと、次のように画像が次々にスクロールする carousel を簡単に実装できます。

![bootstrap-carousel.gif](https://qiita-image-store.s3.amazonaws.com/0/15444/5ecfa708-ea40-58de-28de-5b9ac5d6ab55.gif)

実働サンプルはこちらです： http://jsbin.com/peqipi/2/edit?html,output

ただ簡単とはいえ
----

普通にやると、この程度のコードを書く必要があります。

```html
  <div id="carousel-example-generic" class="carousel slide" data-ride="carousel" data-interval="1500">
      <!-- Indicators -->
      <ol class="carousel-indicators">
          <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
          <li data-target="#carousel-example-generic" data-slide-to="1"></li>
          <li data-target="#carousel-example-generic" data-slide-to="2"></li>
      </ol>

      <!-- Wrapper for slides -->
      <div class="carousel-inner">
          <div class="item active">
              <img src="http://placehold.it/800x400" alt="Caption #1">
              <div class="carousel-caption">
                  <h2>Caption #1</h2>
              </div>
          </div>
          <div class="item">
              <img src="http://placehold.it/800x400" alt="Caption #2">
              <div class="carousel-caption">
                  <h2>Caption #2</h2>
              </div>
          </div>
          <div class="item">
              <img src="http://placehold.it/800x400" alt="Caption #3">
              <div class="carousel-caption">
                  <h2>Caption #3</h2>
              </div>
          </div>
      </div>

      <!-- Controls -->
      <a class="left carousel-control" href="#carousel-example-generic" data-slide="prev">
          <span class="fa fa-angle-left"></span>
      </a>
      <a class="right carousel-control" href="#carousel-example-generic" data-slide="next">
          <span class="fa fa-angle-right"></span>
      </a>
  </div>
```

* 11-28 行目に画像とキャプションが羅列されています
* 4-6 行目は中央の白丸にあたるもので、画像の数だけ並べます
* 32-37 行目は手動でスクロールするための左右のコントロールにあたります

例えば画像を１枚追加するとすると、11-28 行目と、4-6 行目を両方変えなければならず面倒ですし、何をやっているか見通しも悪いです。

Riot.js を使うと
----

上と同じ carousel を

```html
<bootstrap-carousel interval="1500">
  <img src="http://placehold.it/800x400" alt="Caption #1">
  <img src="http://placehold.it/800x400" alt="Caption #2">
  <img src="http://placehold.it/800x400" alt="Caption #3">
</bootstrap-carousel>
```

と書けるようになります！

画像の url と caption を列挙するだけ、 これ以上シンプルにはならない形です。これなら画像を追加したり、キャプションを変更するのも簡単にできます。

Riot.js のカスタムタグ
----

```html
<bootstrap-carousel interval="1500"> ... </bootstrap-carousel>
```

の部分がいわゆる「カスタムタグ」です。

Riot.js は html ソースに埋め込まれたこのようなカスタムタグを、 別に与えた変換規則を用いて html に変換してくれるライブラリです。

### _innerHTML の利用

よくある Riot.js のカスタムタグの利用例では、タグの中身を空にして、

```html
<bootstrap-carousel interval="1500" />
```

とか、

```html
<bootstrap-carousel interval="1500"></bootstrap-carousel>
```

のような形で使っています。

しかし、ドキュメントにもあるとおり Riot.js は通常の html をカスタムタグで囲って使うことも許しています。

```html
<bootstrap-carousel interval="1500">
  <!-- 任意の html -->
</bootstrap-carousel>
```

カスタムタグに囲われた innerHTML は通常 ``<yield />`` を呼び出して利用します。

ただその方法では、出力する html のどこかに、innerHTML をそのままの形で取り込むことしかできず、 利用目的が限られてしまいます。

実はこの内容には javascript からも ``this.root._innerHTML`` としてアクセス可能なため、 下記コードではこれを利用しています。（innerHTML ではなく _innerHTML であることに注意して下さい）

bootstrap-carousel タグの実装
----

以下のコードを bootstrap-carousel.tag として用意し、 riot.js で javascript へコンパイルして使います。

実動サンプルはこちら： http://jsbin.com/qoduteguli/edit?html,output

![bootstrap-carousel2.gif](https://qiita-image-store.s3.amazonaws.com/0/15444/11f8c755-f663-54c5-d3fe-71f1551f03bd.gif)

```html
<bootstrap-carousel>

  <div id="carousel-example-generic" class="carousel slide" data-ride="carousel"
    data-interval={ this.interval } data-pause={ this.pause }
    data-wrap={ this.wrap } data-keyboard={ this.keyboard }>
      <!-- Indicators -->
      <ol class="carousel-indicators">
          <li each={ this.items } data-target="#carousel-example-generic" data-slide-to={ index } class={ active: index==0 }></li>
      </ol>

      <!-- Wrapper for slides -->
      <div class="carousel-inner">
          <div each={ this.items } class={ item: true, active: index==0 }>
              <img src={ src } alt={ caption }>
              <div class="carousel-caption">
                  <h2>{ caption }</h2>
              </div>
          </div>
      </div>

      <!-- Controls -->
      <a class="left carousel-control" href="#carousel-example-generic" data-slide="prev">
          <span class="fa fa-angle-left"></span>
      </a>
      <a class="right carousel-control" href="#carousel-example-generic" data-slide="next">
          <span class="fa fa-angle-right"></span>
      </a>
  </div>
  
  <style scoped>
    .carousel-inner > .item {
      text-align: center;
    }
    .carousel-inner > .item > img {
      display: inline;
    }
    .carousel-inner .carousel-caption h2 {
      color: rgba(0,0,0,0.6);
      font-size: 40px;
      text-shadow: 0px 0px 5px rgba(255, 255, 255, 0.6),
                   0px 0px 10px rgba(255, 255, 255, 0.6);
    }
    .carousel-indicators li {
      background-color: rgba(0,0,0,0.4);
      box-shadow: 0 0 5px rgba(0,0,0,0.6);
      border-color: rgba(255,255,255,0.8);
      margin-left: 6px;
      margin-right: 6px;
    }
    .carousel-indicators li.active {
      border-color: rgba(255,255,255,0.8);
      background-color: rgba(255,255,255,0.8);
      margin-left: 6px;
      margin-right: 6px;
    }
    img {
      opacity: 0.8;
    }
  </style>

  <script type="coffee">
    @on 'update', ->
      @interval = opts.interval ? 5000
      @pause    = opts.pause    ? "hover"
      @wrap     = opts.wrap     ? true
      @keyboard = opts.keyboard ? true

      @items = for img, i in $(@root._innerHTML) when img.tagName == 'IMG'
        caption: img.alt
        src:     img.src
        index:   i
  </script>
</bootstrap-carousel>
```

* 3-28 行目が html ソース
    * { } で囲まれた部分が javascript として解釈されて、結果に置き換えられる
    * { value: expr } の部分は、expr の評価結果が true なら value に、さもなければ空になる
    *   each={ } のついたタグは与えられた配列と同じ数だけ繰り返される
* 30-59 行目が css ソース
    * scoped としておくと、カスタムタグの下の要素にのみ適用される
    * type="scss" などを指定して、scss などで書くこともできる
* 61-73 行目が javascript ソース
    * 変換前に一回呼ばれる
    * on('update', function(){ ... }); を呼ぶことで、値の更新時に処理を行える
    * 基本は this.variable に値を設定して、javascript で { this.variable } を参照する形
    * this.root._innerHTML でカスタムタグに囲まれた html ソースが得られる

コンパイルとマウント
----

上記のようなタグの定義は Riot.js で javascript に変換して呼び出すか、 riot.compile にソースを渡してやることでタグライブラリに取り込み、 riot.mount でカスタムタグを含んだ html へ適用します。

```javascript
  riot.compile(tag_source);
  riot.mount('*');
```

イベントやオブザーバブル、ルートなど
----

ここでは使いませんでしたが、Riot には他にもいろいろ機能があるようです。

今後も徐々に使いながら慣れていこうと思います。