ActionSheetのボタンをCSSの属性セレクタでエレガントに実装してみる

![y8Sf.jpg](https://qiita-image-store.s3.amazonaws.com/0/3516/704cd776-d91e-6730-01b0-f7cf70dc7c46.jpeg)

## やりたいこと

- あるコンポーネントにボタンが複数含まれているものをつくりたい
- 汎用化してボタンの数は可変にしたい
- だがしかしCSSやイベントリスナーの記述をシンプルにしたい

## デモ

[CSS ActionSheet Demo](http://jsdo.it/yd_niku/y8Sf)

## 実装ポイント

### CSSの命名規則と属性セレクタで要素をグルーピングする

増えうる複数のアクション系のボタンに対して`.btn-action_1`,`.btn-action_2`,`.btn-action_3`……という具合に連番にしておき`*[class^=btn-action]`という指定をするとbtn-actionで開始する要素にマッチすることができます。

こうすることでCSS的に特定の名前をつけつつも似たような連番のクラスが増えたり、`class="btn-actoin btn-action_1"`というふうに冗長な記述をする必要がなくなります。
(`nth-child(N)`使えよ的な話もあるにはあるのですが……)

### CSSの指定をJS側のイベントのセレクターにも利用する

CSSのマッチができるということは、jQueryのセレクターでも利用できるということです。
つまり`on(type, delegate, listener)`メソッドの第二引数にセレクターを指定することで、コンポーネント以下の増えうるボタン群のイベントを一括で管理できます。またボタンが増えても変更する必要がありません。

```lang:javascript
// ボタンが増えると厄介
function listener1(event) { /* ロジック*/ }
function listener2(event) { /* ロジック*/ }
function listener3(event) { /* ロジック*/ }
$('.action-sheet .btn-action_1').on('click', listener1);
$('.action-sheet .btn-action_2').on('click', listener2);
$('.action-sheet .btn-action_3').on('click', listener3);
```

```lang:javascript
// 一つの指定で済むし、ボタンが増えても変更がいらない
function listener(event) { /* 分岐ロジック */ }
$('.action-sheet').on('click', '*[class^=btn-action]', listener);
```

### クリックされたボタンを見分ける

押されたボタンのクラスから正規表現で末尾の数字を取り出します。

```lang:javascript
function listener(event) {
    var $btn = $(event.currentTarget);// jQueryイベント
    // CSSからアクションボタンの番号を取り出す
    var matchedClass = $btn.attr('class').match(/btn\-action_(\d*)/);
    if(!matchedClass) return false;
    var actionType = parseInt(matchedClass[1]);
    if(isNaN(actionType)) return false;

    // actionTypeによって処理を変える
}
```

こんな感じでコンポーネント化できそうだなーということでBackbone.jsのViewで部品化してみました。

## Backbone.jsで作ってみた。

[CSS ActionSheet Demo using Backbone.js](http://jsdo.it/yd_niku/qcZd)

コンポーネント化する場合、ボタンにアクションを割り当てるインターフェイスが必要になるので、ここではボタンの番号に対応した関数を実行するような仕組みにしました。

```lang:javascript
var actionSheet = new ActionSheet({ el: '.action-sheet'});
actionSheet.open();
actionSheet.attachAction(function() {
    alert('Action！0');
}, 0);
actionSheet.attachAction(function() {
    alert('Action！1');
}, 1);
actionSheet.attachAction(function() {
    alert('Action！2');
}, 2);
```

また`View#initialize`で複雑なイベント処理をしなくても`View.events`にセレクタを書くだけでシンプルに記述できます。(これがやりたかった)

```lang:javascript
    var ActionSheet = Backbone.View.extend({
        events: {
            'click [class^="btn-action"]': 'action',
// 中略...
        },
        initialize: function(options) {
// 中略...
            this._actions = [];
// 中略...
        },
        // ボタンの番号でアクションを設定する
        attachAction: function(func, atIndex) {
          this._actions[atIndex] = func;
        },
        // 押されたボタンによって登録されたアクションを実行する
        action: function(event) {
            var $btn = $(event.currentTarget);
            var matchedClass = $btn.attr('class').match(/btn\-action_(\d*)/);
            if(!matchedClass) return false;

            var actionType = matchedClass[1];
            
            if(isNaN(actionType)) return false;
                        
            if(this._actions[actionType]) {
                this._actions[actionType]();
            }
            
            return false;
        },
// 中略...
    });
```

## その他のアイディア

- JS利用が前提ならclass名は素直に`btn-action`とし、`data-action-type="0"`とかそれ用の属性で判定するほうがよいのかも。
- ボタンの処理の対応はactionイベントを発火させてイベント駆動でもいいかも(分岐自体も外出しする形になるけど。)

# 宣伝

[ワンパクではこういったパーツの作り込みや設計が大好きなひとを募集しています！！](https://www.find-job.net/list/j109182.html)