var app = angular.module('plunker', ['ngMockE2E']);

app.controller('MainCtrl', function($scope) {
  $scope.model = {
    firstname: 'Bob',
    lastname: 'Barker'
  };
});

app.run(function($httpBackend) {
  $httpBackend.whenPOST('api/contact').respond(function(method, url, data) {
    var errors = [];
    data = JSON.parse(data);
    if (data.firstname === "") errors.push('First name was not supplied.');
    if (data.lastname === "") errors.push('Last name was not supplied.');
    if (data.firstname === "Jon") return [500, ["You can't use that name."]];
    return [200, errors];
  });
});


app.directive('contactValidator', function($q, $http) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {
      ngModel.$asyncValidators.contact = function(model) {
        var defer = $q.defer();

        if (model != null) {
          //$errors is a custom property I made up to attach to the form name.
          scope.contact.$errors = [];
          $http.post('api/contact', model).success(function(errorList) {
            if (errorList.length > 0) {
              scope.contact.$errors = errorList;
              defer.reject();
            } else {
              defer.resolve();
            }
          }).error(function(response) {
            scope.contact.$errors.push('There was a general exception. ' + response);
            defer.reject();
          });

        } else {
          //Assume validated if model is empty;
          defer.resolve();
        }

        return defer.promise;
      }

      //Setup a deep check on the entire model.
      scope.$watch(attrs.ngModel, function(model) {
        if (model != null) {
          ngModel.$validate();
        }
      }, true);

    }
  }
});
<!DOCTYPE html>
<html ng-app="plunker">

<head>
  <meta charset="utf-8" />
  <title>AngularJS Plunker</title>

  <link data-require="bootstrap@3.2.0" data-semver="3.2.0" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.css" />
  
  <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.2.0" data-semver="3.2.0" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.js"></script>
  <script data-require="angular.js@*" data-semver="1.4.0-beta.0" src="https://code.angularjs.org/1.4.0-beta.0/angular.js"></script>
  <script src="https://code.angularjs.org/1.4.0-beta.0/angular-mocks.js"></script>

  <script src="app.js"></script>
</head>

<body ng-controller="MainCtrl" class="container">
  <h1>Form Validation</h1>
  <ng-form name="contact" ng-model-options="{allowInvalid: true}" ng-model="model" contact-validator>
    <div class="row">
      <div class="col-md-12">
        <ng-messages for="contact.$invalid" ng-if="contact.$invalid">
          <div class="alert alert-danger">
            <ng-message when="contact">
              <div ng-repeat="error in contact.$errors">{{error}}</div>
            </ng-message>
          </div>
        </ng-messages>
      </div>
    </div>
    <div class="row">
      <div class="col-md-12">
        <div class="form-group">
          <label>First Name</label>
          <input type="text" class="form-control" ng-model-options="{updateOn:'blur'}" ng-model="model.firstname" />
        </div>
        <div class="form-group">
          <label>Last Name</label>
          <input type="text" class="form-control" ng-model="model.lastname" />
        </div>

        <button class="btn btn-success" ng-disabled="contact.$invalid">Update</button>
      </div>
    </div>
  </ng-form>
</body>

</html>