Call async service in AngularJS custom validation directive

10,151

Solution 1

In order to get this to work, I needed to add "return value;" outside of the asynchronous call. Code below.

commonModule.directive("usernameVerify", [
    'userSvc', function(userSvc) {
        return {
            require: 'ngModel',
            scope: false,
            link: function(scope, element, attrs, ctrl) {
                ctrl.$parsers.unshift(checkForAvailability);
                ctrl.$formatters.unshift(checkForAvailability);

                function checkForAvailability(value) {
                    if (value.length < 5) {
                        return value;
                    }
                    userSvc.userExists(value)
                        .success(function(alreadyUsed) {
                            var valid = alreadyUsed === 'false';
                            if (valid) {
                                ctrl.$setValidity('usernameVerify', true);
                                return value;
                            }
                            ctrl.$setValidity('usernameVerify', false);
                            return undefined;
                        });
                    // Below is the added line of code.
                    return value;
                }
            }
        }
    }
]);

Solution 2

Angular has a dedicated array of $asyncValidators for precisely this situation:

see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;

// Lookup user by username
return $http.get({url:'/api/users/' + value}).
   then(function resolved() {
     //username exists, this means validation fails
     return $q.reject('exists');
   }, function rejected() {
     //username does not exist, therefore this validation passes
     return true;
   });
};
Share:
10,151
Ryan Langton
Author by

Ryan Langton

Updated on June 26, 2022

Comments

  • Ryan Langton
    Ryan Langton about 2 years

    I have a directive for custom validation (verify a username doesn't already exist). The validation uses the $http service to ask the server if the username exists, so the return is a promise object. This is working fantastic for validation. The form is invalid and contains the myform.$error.usernameVerify when the username is already taken. However, user.username is always undefined, so it's breaking my ng-model directive. I think this is probably because the function in .success is creating it's own scope and the return value isn't used on the controllers $scope. How do I fix this so the ng-model binding still works?

    commonModule.directive("usernameVerify", [
        'userSvc', function(userSvc) {
            return {
                require: 'ngModel',
                scope: false,
                link: function(scope, element, attrs, ctrl) {
                    ctrl.$parsers.unshift(checkForAvailability);
                    ctrl.$formatters.unshift(checkForAvailability);
    
                    function checkForAvailability(value) {
                        if (value.length < 5) {
                            return value;
                        }
                        // the userSvc.userExists function is just a call to a rest api using $http
                        userSvc.userExists(value)
                            .success(function(alreadyUsed) {
                                var valid = alreadyUsed === 'false';
                                if (valid) {
                                    ctrl.$setValidity('usernameVerify', true);
                                    return value;
                                } 
                                ctrl.$setValidity('usernameVerify', false);
                                return undefined;
                            });
                    }
                }
            }
        }
    ]);
    

    Here is my template:

    <div class="form-group" ng-class="{'has-error': accountForm.username.$dirty && accountForm.username.$invalid}">
        <label class=" col-md-3 control-label">Username:</label>
        <div class="col-md-9">
            <input name="username"
                   type="text"
                   class="form-control"
                   ng-model="user.username"
                   ng-disabled="user.id"
                   ng-minlength=5
                   username-verify
                   required />
            <span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.required">Username is required.</span>
            <span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.minlength">Username must be at least 5 characters.</span>
            <span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.usernameVerify">Username already taken.</span>
        </div>
    </div>
    
  • E-Madd
    E-Madd over 9 years
  • Forge_7
    Forge_7 over 9 years
    The problem with this approach is that the checkForAvailability function will return (with your value) immediately, since the userExists functions returns a promise which will be fulfilled later.
  • Michał Miszczyszyn
    Michał Miszczyszyn over 9 years
    Note: This is available since Angular 1.3.0