<html>

  <head>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular-messages.min.js"></script>
    <script src="script.js"></script>
  </head>

  <body ng-app="app" ng-controller="Main as ctrl" ng-cloak="">
    
    <form class="container" name="sampleForm" novalidate="" ng-submit="ctrl.submitHandler()">
      
      <h1>ngMessages Sample</h1>
      <div class="form-group">

        <section class="container" name="checkboxes" ng-model="ctrl.checkboxes" cm-check-boxes-required="">

          <h2 class="title title--required">CheckBoxes (custom validator)</h2>
  
          <div class="bg-danger" ng-messages="sampleForm.checkboxes.$error" ng-if="sampleForm.$submitted">
            <p class="text-danger padding--10px" ng-message="checkBoxesRequired">1 つ以上選択してください</p>
          </div>
      
          <div class="checkbox" ng-repeat="item in ctrl.checkboxes">
            <label class="checkbox-inline">
              <input type="checkbox" class="checkbox-inline" ng-model="item.value" />{{item.label}}
            </label>
          </div>
  
        </section>
  
        <section class="container">
          
          <h2 class="title title--required">TextInput (normal validator)</h2>
  
          <div class="bg-danger" ng-messages="sampleForm.myName.$error" ng-if="sampleForm.$submitted">
            <p class="text-danger padding--10px" ng-message="required">必須項目です</p>
            <p class="text-danger padding--10px" ng-message="minlength">5 文字以上入力してください</p>
            <p class="text-danger padding--10px" ng-message="maxlength">20 文字以上入力できません</p>
          </div>
          
          <div class="form-group">
            <input type="text"
                   name="myName"
                   ng-model="name"
                   ng-minlength="5"
                   ng-maxlength="20"
                   ng-required="true" />
          </div>
  
        </section>
        
      </div>

      <section class="container top-buffer">
        <input type="submit" class="btn btn-primary" value="CHECK" />
        <button class="btn btn-default" ng-click="ctrl.resetHandler($event)">RESET</button>
      </section>

    </form>
  </body>

</html>
(function() {
  'use strict';
  angular.module('app', ['ngMessages'/*'ui.bootstrap'*/], function(){
  })
  .config(function(/*$locationProvider, $logProvider, $httpProvider*/) {
  })
  .directive('cmCheckBoxesRequired', function() {
    return {
     'restrict' : 'A',
     'require'  : '?ngModel',
     'link'     : function(scope, elm, attr, ctrl) {

        if (!ctrl) {
          return;
        }

        var invalid = false;

        var watcher;

        var jqContainer = angular.element(elm);
        
        var checkVlidatate = function() {
          var result = jqContainer.find('input[type=\'checkbox\']:checked');
          return !angular.equals(result.length, 0);
        };

        watcher = scope.$watch(attr.ngModel, function(newValue, oldValue) {
          if(!newValue || !oldValue) {
            return;
          } else if (!angular.equals(newValue, oldValue)) {
            invalid = checkVlidatate();
            ctrl.$setValidity('checkBoxesRequired', invalid);
          }
        }, true);
        
        ctrl.$validators.checkBoxesRequired = function(modelValue, viewValue) {
          return invalid;
        };
        
        var destroy = scope.$on('$destroy', function() {
          watcher();
          destroy();
        });

      }
   };
  })
  .controller('Main', function($scope, $log) {
    $log.debug('Main');
    
    var scope = $scope;

    var ctrl  = this;
    
    ctrl.checkboxes = [
      {'value' : false, 'label' : '選択肢 1' },
      {'value' : false, 'label' : '選択肢 2' },
      {'value' : false, 'label' : '選択肢 3' },
      {'value' : false, 'label' : '選択肢 4' }
    ];
    
    ctrl.submitHandler = function() {
      $log.debug('ctrl.submit ', scope);
    };
    
    ctrl.resetHandler = function(event) {
      event.preventDefault();
      var form  = scope.sampleForm;
      form.$setUntouched();
      form.$setPristine();
    };
    
  });
}());

.title--required:after {
   content : "*";
   color : red;
   padding-left : 10px;
}
.padding--10px {
  padding : 10px;
}