<!DOCTYPE html>
<html ng-app="my-app">

  <head>
    <link data-require="foundation@*" data-semver="5.0.0" rel="stylesheet" href="//cdn.jsdelivr.net/foundation/5.0.0/css/normalize.css" />
    <link data-require="foundation@*" data-semver="5.0.0" rel="stylesheet" href="//cdn.jsdelivr.net/foundation/5.0.0/css/foundation.css" />
    <link data-require="foundation@*" data-semver="5.0.0" rel="stylesheet" href="//cdn.jsdelivr.net/foundation/5.0.0/css/foundation.min.css" />
    <script data-require="jquery@*" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
    
    <script data-require="angular.js@*" data-semver="1.2.9" src="http://code.angularjs.org/1.2.9/angular.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-animate.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body>
    <h1>AngularJS Validation Example</h1>
    <form with-errors name="signUpForm" ng-controller="SignUpCtrl as c" ng-submit="c.signup(c.user)">
      <fieldset>
        <label>Username:</label>
        <input name="username" type="text" ng-model="c.user.username" required />
        <fielderrors for="username"></fielderrors>
        
        <label>Password:</label>
        <input name="password" type="password" ng-model="c.user.password" required />
        <fielderrors for="password"></fielderrors>
        
        <button ng-disabled="signUpForm.$invalid">Submit</button>
      </fieldset>
    </form>
  </body>

</html>
var m = angular.module('my-app', []);

m.controller('SignUpCtrl', [
  '$scope', '$q', 'setFormErrors',
  function($scope, $q, setFormErrors) {
    var serverErrors = this.serverErrors = {};
    
    this.signup = function(user) {
      fakeUserCreate(user).then(function() {
        // Success
      }, function(errors) {
        // Failed
        setFormErrors({
          formName: 'signUpForm',
          fieldErrors: errors
        });
      });
    };
    
    function fakeUserCreate() {
      return $q.reject({username: ['This username is taken']});
    }
  }
]);

m.factory('setFormErrors', function() {
  // Registered withErrors controllers
  var withErrorCtrls = [];
  
  // The exposed service
  var setFormErrors = function(opts) {
    var fieldErrors = opts.fieldErrors;
    var ctrl = withErrorCtrls[opts.formName];
        
    Object.keys(fieldErrors).forEach(function(fieldName) {
      ctrl.setErrorsFor(fieldName, fieldErrors[fieldName]);
    });
  };
  
  // Registers withErrors controller by form name (for internal use)
  setFormErrors._register = function(formName, ctrl) {
    withErrorCtrls[formName] = ctrl;
  };
  
  return setFormErrors;
});

m.directive('withErrors', ['setFormErrors', function(setFormErrors) {
  return {
    restrict: 'A',
    require: 'withErrors',
    controller: ['$scope', '$element', function($scope, $element) {
      var controls = {};
      
      this.addControl = function(fieldName, ctrl) {
        controls[fieldName] = ctrl;
      };
      
      this.setErrorsFor = function(fieldName, errors) {
        if (!(fieldName in controls)) return;
        return controls[fieldName].setErrors(errors);
      };
      
      this.clearErrorsFor = function(fieldName, errors) {
        if (!(fieldName in controls)) return;
        return controls[fieldName].clearErrors(errors);
      };
    }],
    link: function(scope, element, attrs, ctrl) {
      // Make this form controller accessible to setFormErrors service
      setFormErrors._register(attrs.name, ctrl);
    }
  }; 
}]);

m.directive('input', function() {
  return {
    restrict: 'E',
    require: ['?ngModel', '?^withErrors'],
    scope: true,
    link: function(scope, element, attrs, ctrls) {
      var ngModelCtrl = ctrls[0];
      var withErrorsCtrl = ctrls[1];
      var fieldName = attrs.name;
      
      if (!ngModelCtrl || !withErrorsCtrl) return;
      
      // Watch for model changes and set errors if any
      scope.$watch(attrs.ngModel, function() {
        if (ngModelCtrl.$dirty && ngModelCtrl.$invalid) {
          withErrorsCtrl.setErrorsFor(fieldName, errorMessagesFor(ngModelCtrl));
        } else if (ngModelCtrl.$valid) {
          withErrorsCtrl.clearErrorsFor(fieldName);
        }
      });
      
      // Mapping Angular validation errors to a message
      var errorMessages = {
        required: 'This field is required'
      };
      
      function errorMessagesFor(ngModelCtrl) {
        return Object.keys(ngModelCtrl.$error).
          map(function(key) {
            if (ngModelCtrl.$error[key]) return errorMessages[key];
            else return null;
          }).
          filter(function(msg) {
            return msg !== null;
          });
      }
    }
  }  
});

m.directive('fielderrors', function() {
  return {
    restrict: 'E',
    replace: true,
    scope: true,
    require: ['fielderrors', '^withErrors'],
    template: 
      '<div ng-repeat="error in errors">' +
        '<small class="error">{{ error }}</small>' +
      '</div>',
    controller: ['$scope', function($scope) {
      $scope.errors = [];
      this.setErrors = function(errors) {
        $scope.errors = errors;
      };
      this.clearErrors = function() {
        $scope.errors = [];
      };
    }],
    link: function(scope, element, attrs, ctrls) {
      var fieldErrorsCtrl = ctrls[0];
      var withErrorsCtrl = ctrls[1];
      withErrorsCtrl.addControl(attrs.for, fieldErrorsCtrl);
    }
  };
});
body {
  padding: 5px;
}

input[type] {
  margin-bottom: 0;
}

label, button {
  margin-top: 1em;
}