AngularJS での複数チェックボックスの処理

*※AngularJS 1.4.8 です*

[AngularJS](https://angularjs.org/)で、複数チェックボックスの処理するのがどうもよくわからなかったので初心者的メモ。
例えば「どれかチェック必須」とか「2つ以上チェック必須」とかがググってもよくわからなかったので、コントローラを自作してしまった。

例えば [Twitter Bootstrap](http://getbootstrap.com/) を使ってるとして、1つ以上チェックしてなければ `form-group` に `has-error` のclassを付けたいという場合。

```js:JavaScript側（コントローラ）
app.controller('checkBoxGroup', ['$scope', '$element', '$parse', function ($scope, $element, $parse) {
    // html の方で「self」という文字で状態を検出できるようにする
    var self = {
        touched: false,
        required: (undefined != ($element.attr('required') || $element.attr('ng-required'))),
        values: []
    };

    $scope.self = self;

    // 配下にある ng-model を取得
    $element.find('[ng-model]').each(function() {
        var item = angular.element(this);

        if (item.is(':checked') && !item.attr('ng-checked')) {
            // checked があればスコープに割当て
            $parse(item.attr('ng-model')).assign($scope, true);

            // 値を追加
            self.values.push(item.val());
        }

        // クリックされた場合
        item.on('click', function() {
            // 一度でも触れば touched を true に
            if (false == self.touched) {
                self.touched = true;
            }

            if (item.is(':checked')) {
                // 値を追加
                self.values.push(item.val());
            } else {
                // 値を削除
                var i = self.values.indexOf(item.val());
                if (i >= 0) {
                    self.values.splice(i, 1);
                }
            }
        });
    });
}]);
```

```html:HTML側
<div class="form-group" ng-controller="checkBoxGroup" ng-class="{'has-error': self.required && self.touched && self.values.length < 1}" required>
    <label class="control-label">どれかチェックしてね</label>
    <div class="checkbox">
        <label><input type="checkbox" name="chk[]" value="項目1" ng-model="chk1" ng-required="self.required && self.values.length < 1">項目1</label>
        <label><input type="checkbox" name="chk[]" value="項目2" ng-model="chk2" ng-required="self.required && self.values.length < 1">項目2</label>
        <label><input type="checkbox" name="chk[]" value="項目3" ng-model="chk3" ng-required="self.required && self.values.length < 1" checked>項目3</label>
        <label><input type="checkbox" name="chk[]" value="項目4" ng-model="chk4" ng-required="self.required && self.values.length < 1">項目4</label>
    </div>
</div>
```


- JavaScript側では `ng-controller` が指定された要素内にあるチェックボックスを検出して、HTML側でチェック状態が読めるように処理をしてる。
- HTML側ではJavaScriptで処理されたチェックボックスの状態（この場合 `self` という変数）を使って監視してる。
  - `self.touched` で「1度でもクリック（チェック）されれば」という処理を監視して、初期状態では何もしないようにしてる。
  - `required` または `ng-required` があれば配下のチェックボックスを必須項目とする。
- `self.values.length < 1` により「1つ以上チェックされていなければ」という条件になる。  
  2つ以上なら `self.values.length < 2` 。
- チェックボックスの `ng-model` の名前はユニークにする必要がある。
- ついでに、チェックボックスに `checked` があればデフォルトでチェックを入れる。  
  - AngularJS管理下では `checked` ではなく `ng-init="ngModel名=true"` みたいにしないとデフォルトで checked にならないので。

-----

## 追記

「AngularJS管理下では `checked` が効かない」という点について、`input type="text"` とかの `value` とか `select` の `selected` とかも効かないようなので以下のようなスニペットも作ってみた。
手探りなのでこれも正解かどうかはわからない。

```js
// さいしょ
var app = angular.module('app', []);

// 直後にこれ実行しておく
app.run(['$rootScope', '$rootElement', '$parse', function($rootScope, $rootElement, $parse) {
    $rootElement.find('[ng-model]').each(function(i, e) {
        var item = angular.element(this);
        var type = item.attr('type')||this.tagName.toLowerCase();

        if (/^(checkbox|radio)$/.test(type)) {
            if (item.is(':checked') && !item.attr('ng-checked')) {
                $parse(item.attr('ng-model')).assign($rootScope, true);
            }
        } else if (item.val()) {
            $parse(item.attr('ng-model')).assign($rootScope, item.val());
        }
    });
}]);
```


-----

普通に一般的な機能なので、たぶん車輪の再開発してるんじゃないかと思うけど・・・w