// Code goes here
angular.module('myApp',[]);

var Q;
var HobbiesCtrl = (function () {
    
    var setQ  = function setQ(q){    Q = q;}
    function HobbiesCtrl($scope,$q,businessRules) {
        setQ($q)
      
        this.mainValidator = businessRules;
        this.data = {};
       
     
        
        //fill list of hobbies with one empty item to indicate there are some hobbies to fill in
        if (this.data.Hobbies === undefined)
            this.data.Hobbies = [{}];
        this.notifyCollectionChanged(true);
    }
    var  frequencyOptions =  [
                { text: 'Daily', value: "Daily"   },
                { text: 'Weekly', value: "Weekly" },
                { text: 'Monthly', value: "Monthly" }
            ];
    Object.defineProperty(HobbiesCtrl.prototype, "hobbyFrequencyOptions", {
        /*
        Return hobbies frequency options.
        */
        get: function () {
            return frequencyOptions;
        },
        enumerable: true,
        configurable: true
    });

Object.defineProperty(HobbiesCtrl.prototype, "HobbiesCountRule", {
        /*
        Return hobbies frequency options.
        */
        get: function () {
            var hobbiesCountRule = this.mainValidator.Rules["Hobbies"];
            if (hobbiesCountRule === undefined) hobbiesCountRule = this.mainValidator.Validators["Hobbies"];
            return hobbiesCountRule;
        },
        enumerable: true,
        configurable: true
    });
    /*
    Add new hobby to list of hobbies.
    */
    HobbiesCtrl.prototype.addHobby = function () {
        this.data.Hobbies.push({});
        this.notifyCollectionChanged();
    };
    
    

    /*
    Remove selected hobby from list of hobbies.
    */
    HobbiesCtrl.prototype.removeHobby = function (hobby) {
        this.data.Hobbies.splice(this.data.Hobbies.indexOf(hobby), 1);
        this.notifyCollectionChanged();
    };


    /*
    Hook function for actions before saving is done.
    */
    HobbiesCtrl.prototype.Save = function () {
      this.mainValidator.ValidationResult.SetDirty();
      var result = this.mainValidator.Validate(this.data);
      if (result.HasErrors) return;
      
      alert(angular.toJson(this.data));
    };
    
    HobbiesCtrl.prototype.Reset = function () {
        this.mainValidator.ValidationResult.SetPristine();
    };

    /*
    Notify that collection was changed. Conditionally call validation for collection.
    */
    HobbiesCtrl.prototype.notifyCollectionChanged = function (ignoreValidation) {
        this.mainValidator.Children["Hobbies"].RefreshRows(this.data.Hobbies);
        if (!ignoreValidation){
           this.HobbiesCountRule.Validate(this.data.Hobbies);
        }
    };
    
    
    return HobbiesCtrl;
})();



angular.module('myApp',[]).controller("hobbiesCtrl",HobbiesCtrl);
<!DOCTYPE html>
<html ng-app="myApp">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <link data-require="bootstrap-css@3.1.1" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.2.x" src="https://code.angularjs.org/1.2.22/angular.js" data-semver="1.2.22"></script>
    <script data-require="underscore.js@1.6.0" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
    <script data-require="jquery@*" data-semver="2.1.1" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script data-require="bootstrap@3.1.1" data-semver="3.1.1" src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
    <script src="https://rawgit.com/flesler/hashmap/master/hashmap.js"></script>
    <script src="https://rawgit.com/rsamec/business-rules-engine/master/dist/business-rules-engine.js"></script>
    <script src="app.js"></script>
    <script src="br-jsonSchema.js"></script>
    <script src="directives.js"></script>
  </head>

  <body ng-controller="hobbiesCtrl as va">
    <br />
    <div class="container-fluid">
      <div class="col-xs-8">
        <form class="form-horizontal">
          <div class="form-group">
            <label class="col-xs-2 control-label">Name</label>
            <div class="col-xs-4 col-sm-3 col-md-2">
              <input type="text" class="form-control" placeholder="FirstName" ng-model="va.data.Person.FirstName" rule="va.mainValidator.Children['Person']" />
              <div val-result="va.mainValidator.Children['Person'].Rules['FirstName']"></div>
            </div>
            <div class="col-xs-4 col-sm-4 col-md-3">
              <input type="text" class="form-control" placeholder="LastName" ng-model="va.data.Person.LastName" rule="va.mainValidator.Children['Person']" />
              <div val-result="va.mainValidator.Children['Person'].Rules['LastName']"></div>
            </div>
          </div>
          <div class="form-group">
    <label class="col-xs-2 control-label">Email</label>
    <div class="col-xs-6 col-sm-7 col-md-5">
        <input type="text" class="form-control" rule="va.mainValidator.Children['Person'].Children['Contact']" ng-model="va.data.Person.Contact.Email" placeholder="Email">
       <div val-result="va.mainValidator.Children['Person'].Children['Contact'].Rules['Email']"></div>
    </div>
</div>
          
          <div class="row">
            <div class="col-xs-2">
              <div class="btn btn-primary" ng-click="va.addHobby()">
                Add +
          </div>
            </div>
            <div class="col-xs-10">
              <div class="validation-error">{{va.HobbiesCountRule.ErrorMessage}}</div>
            </div>
          </div>
          <div ng-repeat="hobby in va.data.Hobbies">
            <div class="form-group">
              <label class="col-xs-2 control-label">Name</label>
              <div class="col-xs-8 col-sm-7 col-md-5">
                <div class="input-group">
                  <input type="text" class="form-control" ng-model="hobby.HobbyName" placeholder="Name" rule="va.mainValidator.Children['Hobbies'].Rows[$index]" />
                  <span class="input-group-btn">
                    <button class="btn btn-default" type="button" ng-click="va.removeHobby(hobby)">X</button>
                    <!--<i class="fa fa-times"></i>-->
                  </span>
                </div>
                <div ng-show="va.mainValidator.Children['Hobbies'].Rows[$index].ValidationResult.HasErrors" class="validation-error">
                    {{va.mainValidator.Children['Hobbies'].Rows[$index].ValidationResult.ErrorMessage}}
                </div>
              </div>
            </div>
            <div class="form-group">
              <label class="col-xs-2 control-label">Frequency</label>
              <div style="display: table" class="col-xs-4 col-sm-3 col-md-2">
                <select class="form-control" ng-model="va.data.Hobbies[$index].Frequency" ng-options="obj.value as obj.text for obj in va.hobbyFrequencyOptions"></select>
              </div>
            </div>
            <div class="form-group">
              <div class="col-xs-offset-2 col-xs-8">
                <div class="checkbox">
                  <label>
                    <input type="checkbox" ng-model="va.data.Hobbies[$index].Paid" />
Is this a paid hobby?</label>
                </div>
              </div>
            </div>
            <div class="form-group">
              <div class="col-xs-offset-2 col-xs-8">
                <div class="checkbox">
                  <label>
                    <input type="checkbox" ng-model="va.data.Hobbies[$index].Recommendation" />
Would you recommend this hobby to a friend?</label>
                </div>
              </div>
            </div>
          </div>
          <div class="form-group">
            <div class="col-xs-offset-2 col-xs-8">
              <button type="button" class="btn btn-primary" ng-click="va.Save()">Save</button>
            </div>
          </div>
        </form>
      </div>
      <div class="col-xs-4">
        <pre>{{va.data |json}}</pre>
      </div>
     

    </div>
  </body>

</html>
/* Put your css in here */
.validation-error{
  color:darkred;
}
var app = angular.module('myApp');
app.directive('rule', function ($parse) {
    return {
        require:'ngModel',
        restrict: 'A',
        link: function (scope, element, attrs, ctrl) {
            if (ctrl===undefined) return;

            //parse ngModel expression
            var lastIndexOf = attrs.ngModel.lastIndexOf('.');
            if (lastIndexOf === -1) return;

            //set model expression
            var parentModel = attrs.ngModel.substr(0,lastIndexOf);
            //set property name
            var propertyName = attrs.ngModel.substr(lastIndexOf + 1);

            //create rule
            var rule = scope.$eval(attrs.rule);

            //create parent getter
            var getter = $parse(parentModel);

            //when value changed then validate property
            ctrl.$viewChangeListeners.push(function(){

                //execute validation
                rule.ValidateProperty(getter(scope),propertyName);

                //set dirty flag for correct display
                rule.ValidationResult.Errors[propertyName].IsDirty = true;
            });
        }
    };
});

app.directive('field', function ($timeout,$parse) {
    return {
        require:'ngModel',
        restrict: 'E',
        replace:true,
        scope:true,
        templateUrl:'field.tpl.html',
        link: function (scope, element, attrs, ctrl) {
           if (ctrl== undefined) return;

            // we can now use our ngModelController builtin methods
            // that do the heavy-lifting for us

            var inputs = element.find('input');
            var inputEl = inputs.eq(0);

            // when model change, update our view (just update the input value)
            ctrl.$render = function() {
                inputEl.val(ctrl.$viewValue);
            };

            // update the model then the view
            var updateModel = function () {
                // call $parsers pipeline then update $modelValue
                ctrl.$setViewValue(inputEl.val());
                // update the local view
                ctrl.$render();
            }
            scope.label = attrs.label;

            bindToOnChanged(scope, inputs, attrs, updateModel);

            var lastIndexOf = attrs.ngModel.lastIndexOf('.');
            var parentModel = attrs.ngModel.substr(0,lastIndexOf);
            var propertyName = attrs.ngModel.substr(lastIndexOf + 1);

            var rule = scope.$eval(attrs.rule)
            var getter = $parse(parentModel);

            ctrl.$viewChangeListeners.push(function(){
                rule.ValidateProperty(getter(scope),propertyName);
                rule.ValidationResult.Errors[propertyName].IsDirty = true;
            });

            scope.getErrorMessage = function() {
                return rule.ValidationResult.Errors[propertyName].ErrorMessage;
            }
        }
    };
});

app.directive('valResult', function () {
    return {
        restrict: 'A',
        scope:{
            valResult:'='
        },
        //template:'<div class="validation-error"></div>'
        link: function (scope, element, attrs) {
            element.addClass("validation-error");

         
            var errorChanged =  function(){
                return scope.valResult.ErrorMessage;
            }
            
            scope.$watch(errorChanged,function(){
                element.html(scope.valResult.ErrorMessage);
            },true)
        }
    };
});


function bindToOnChanged(scope, element, attrs, bindTo) {
    //bind to event according type of element
    var tagName = element[0].tagName.toLowerCase();
    if (tagName === 'select')
    {
        element.bind("change", function ()
        {
            scope.$apply(bindTo);
        });
    }
    else if (tagName === 'input')
    {
        if (attrs.type === 'radio' || attrs.type === 'checkbox')
        {
            element.bind("click", function ()
            {
                scope.$apply(bindTo);
            })
        }
        else
        {
            element.bind("blur", function ()
            {
                scope.$apply(bindTo);
            })
        }
    }
    else if (tagName === 'textarea')
    {
        element.bind("blur", function ()
        {
            scope.$apply(bindTo);
        })
    }
    else
    {
    }
}



angular.module('myApp').factory('businessRules',function(){
   var json = {
        Person: {
            type: "object",
            properties: {
                FirstName: { type: "string", title: "First name", required: true, maxLength: 15 },
                LastName: { type: "string", title: "Last name", required: true, maxLength: 15 },
                Contact: {
                    type: "object",
                    properties: {
                        Email: {
                            type: "string", title: "Email",
                            required: true,
                            maxLength: 100,
                            email: true
                        }
                    }
                }
            }
        },
        Hobbies: {
            type: "array",
            items: {
                type: "object",
                properties: {
                    HobbyName: { type: "string", title: "HobbyName", required: true, maxLength: 100 },
                    Frequency: { type: "string", title: "Frequency", enum: ["Daily", "Weekly", "Monthly"] },
                    Paid: { type: "boolean", title: "Paid" },
                    Recommedation: { type: "boolean", title: "Recommedation" }
                }
            },
            maxItems: 4,
            minItems: 2
        }
    };
    return new FormSchema.JsonSchemaRuleFactory(json).CreateRule("Main");
})
angular.module('myApp').factory('businessRules',function(){
   var json = {
        
        Person: {
            FirstName: {
                rules: {required: true, maxlength: 15}
            },
            LastName: {
                rules: { required: true, maxlength: 15 }
            },
            Contact: {
                Email: {
                    rules: {
                        required: true,
                        maxlength: 100,
                        email: true
                    }
                }
            }
        },
        Hobbies: [
            {
                HobbyName: {
                    rules: { required: true, maxlength: 100 }
                },
                Frequency: {
                    rules: { enum: ["Daily", "Weekly", "Monthly"]
                    }
                }},
            { maxItems: 4, minItems: 2}
        ]
    };
    
    return new FormSchema.JQueryValidationRuleFactory(json).CreateRule("Main");
})
angular.module('myApp').factory('businessRules',function(){
  var BusinessRules = (function () {
     
        function BusinessRules() {
            
            this.MainValidator = this.createMainValidator().CreateRule("Main");
        }
       
       
       BusinessRules.prototype.createMainValidator = function () {
            //create custom validator
            var validator = new Validation.AbstractValidator();

            validator.ValidatorFor("Person", this.createPersonValidator());
            validator.ValidatorFor("Hobbies", this.createItemValidator(), true);

            var hobbiesCountFce = function (args) {
                args.HasError = false;
                args.ErrorMessage = "";

                if (this.Hobbies === undefined || this.Hobbies.length < 2) {
                    args.HasError = true;
                    args.ErrorMessage = "Come on, speak up. Tell us at least two things you enjoy doing";
                    args.TranslateArgs = { TranslateId: 'HobbiesCountMin' };
                    return;
                }
                if (this.Hobbies.length > 4) {
                    args.HasError = true;
                    args.ErrorMessage = "'Do not be greedy. Four hobbies are probably enough!'";
                    args.TranslateArgs = { TranslateId: 'HobbiesCountMax'};
                    return;
                }
            };

            validator.Validation({ Name: "Hobbies", ValidationFce: hobbiesCountFce });

            return validator;
        };
        BusinessRules.prototype.createPersonValidator = function () {
            //create custom composite validator
            var personValidator = new Validation.AbstractValidator();

            //create validators
            var required = new Validators.RequiredValidator();
            var maxLength = new Validators.MaxLengthValidator(15);

            //assign validators to properties
            personValidator.RuleFor("FirstName", required);
            personValidator.RuleFor("FirstName", maxLength);

            personValidator.RuleFor("LastName", required);
            personValidator.RuleFor("LastName", maxLength);

            personValidator.ValidatorFor("Contact", this.createContactValidator());

            return personValidator;
        };
        BusinessRules.prototype.createContactValidator = function () {
            //create custom validator
            var validator = new Validation.AbstractValidator();
            validator.RuleFor("Email", new Validators.RequiredValidator());
            validator.RuleFor("Email", new Validators.MaxLengthValidator(100));
            validator.RuleFor("Email", new Validators.EmailValidator());
            return validator;
        };
        BusinessRules.prototype.createItemValidator = function () {
            //create custom validator
            var validator = new Validation.AbstractValidator();
            validator.RuleFor("HobbyName", new Validators.RequiredValidator());
            return validator;
        };
        return BusinessRules;
    })();
    return new BusinessRules().MainValidator;
})