55

I have an angular app that contains a save button taken from the examples:

<button ng-click="save" ng-disabled="form.$invalid">SAVE</button>

This works great for client side validation because form.$invalid becomes false as user fixes problems, but I have an email field which is set invalid if another user is registered with same email.

As soon as I set my email field invalid, I cannot submit the form, and the user has no way to fix that validation error. So now I can no longer use form.$invalid to disable my submit button.

There must be a better way

5 Answers 5

75

This is another case where a custom directive is your friend. You'll want to create a directive and inject $http or $resource into it to make a call back to the server while you're validating.

Some pseudo code for the custom directive:

app.directive('uniqueEmail', function($http) {
  var toId;
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, elem, attr, ctrl) { 
      //when the scope changes, check the email.
      scope.$watch(attr.ngModel, function(value) {
        // if there was a previous attempt, stop it.
        if(toId) clearTimeout(toId);

        // start a new attempt with a delay to keep it from
        // getting too "chatty".
        toId = setTimeout(function(){
          // call to some API that returns { isValid: true } or { isValid: false }
          $http.get('/Is/My/EmailValid?email=' + value).success(function(data) {

              //set the validity of the field
              ctrl.$setValidity('uniqueEmail', data.isValid);
          });
        }, 200);
      })
    }
  }
});

And here's how you'd use it in the mark up:

<input type="email" ng-model="userEmail" name="userEmail" required unique-email/>
<span ng-show="myFormName.userEmail.$error.uniqueEmail">Email is not unique.</span>

EDIT: a small explanation of what's happening above.

  1. When you update the value in the input, it updates the $scope.userEmail
  2. The directive has a $watch on $scope.userEmail it set up in it's linking function.
    • When the $watch is triggered it makes a call to the server via $http ajax call, passing the email
    • The server would check the email address and return a simple response like '{ isValid: true }
    • that response is used to $setValidity of the control.
  3. There is a in the markup with ng-show set to only show when the uniqueEmail validity state is false.

... to the user that means:

  1. Type the email.
  2. slight pause.
  3. "Email is not unique" message displays "real time" if the email isn't unique.

EDIT2: This is also allow you to use form.$invalid to disable your submit button.

Sign up to request clarification or add additional context in comments.

12 Comments

but this doesnt really solve my problem. they could still potentially submit a non-unique email and i must still validate it on the server side, at which point i must still show a clientside error and not via this directive?
but i digress, if they are able to submit an invalid email, the submit will return an error and mark form invalid, at which point they will be forced to use this directive to mark the email field valid before they can submit the form
This directive would send a request asynchronously to the server, which would check the email and say if it's unique or not on the server side... if it's not a validation message would show.
Other than that, regardless, you'll want to validate your email again when you go to save it. The nature of the web is stateless, and each request must be validated separately.
This post really helped me out. I had the exact same problem, but I had to make some mods to the code. One restrict had to be 'A' not 'E', and scope.apply() threw an exception saying "$digest was already in progress". So I inlined the ctrl.setValidity() call in the success callback, and it worked great.
|
30

I needed this in a few projects so I created a directive. Finally took a moment to put it up on GitHub for anyone who wants a drop-in solution.

https://github.com/webadvanced/ng-remote-validate

Features:

  • Drop in solution for Ajax validation of any text or password input

  • Works with Angulars build in validation and cab be accessed at formName.inputName.$error.ngRemoteValidate

  • Throttles server requests (default 400ms) and can be set with ng-remote-throttle="550"

  • Allows HTTP method definition (default POST) with ng-remote-method="GET"

Example usage for a change password form that requires the user to enter their current password as well as the new password.:

<h3>Change password</h3>
<form name="changePasswordForm">
    <label for="currentPassword">Current</label>
    <input type="password" 
           name="currentPassword" 
           placeholder="Current password" 
           ng-model="password.current" 
           ng-remote-validate="/customer/validpassword" 
           required>
    <span ng-show="changePasswordForm.currentPassword.$error.required && changePasswordForm.confirmPassword.$dirty">
        Required
    </span>
    <span ng-show="changePasswordForm.currentPassword.$error.ngRemoteValidate">
        Incorrect current password. Please enter your current account password.
    </span>

    <label for="newPassword">New</label>
    <input type="password"
           name="newPassword"
           placeholder="New password"
           ng-model="password.new"
           required>

    <label for="confirmPassword">Confirm</label>
    <input ng-disabled=""
           type="password"
           name="confirmPassword"
           placeholder="Confirm password"
           ng-model="password.confirm"
           ng-match="password.new"
           required>
    <span ng-show="changePasswordForm.confirmPassword.$error.match">
        New and confirm do not match
    </span>

    <div>
        <button type="submit" 
                ng-disabled="changePasswordForm.$invalid" 
                ng-click="changePassword(password.new, changePasswordForm);reset();">
            Change password
        </button>
    </div>
</form>

1 Comment

This works great! I am using version 0.6.1 I see there is an open issue using inputnameSetArgs not being called. And a fix to add $parent to the scope. I changed this manually and then SetArgs is called with extra parameters send to the server.
17

I have created plunker with solution that works perfect for me. It uses custom directive but on entire form and not on single field.

http://plnkr.co/edit/HnF90JOYaz47r8zaH5JY

I wouldn't recommend disabling submit button for server validation.

3 Comments

That's extremely helpful! I had to make one change: newest versions don't have angular.forEach, so I updated it to use $.each: plnkr.co/edit/1UNljzvr70F6O6Tac1R8
I am having troubles to run this plunker update you (jphoward) created. What version of angular doesn't support forEach? I see you included "1.1.1".
This is good - except it's unfortunate that you need to duplicate the code for actually displaying the error for every control.
5

Ok. In case if someone needs working version, it is here:

From doc:

 $apply() is used to enter Angular execution context from JavaScript

 (Keep in mind that in most places (controllers, services) 
 $apply has already been called for you by the directive which is handling the event.)

This made me think that we do not need: $scope.$apply(function(s) { otherwise it will complain about $digest

app.directive('uniqueName', function($http) {
    var toId;
    return {
        require: 'ngModel',
        link: function(scope, elem, attr, ctrl) {
            //when the scope changes, check the name.
            scope.$watch(attr.ngModel, function(value) {
                // if there was a previous attempt, stop it.
                if(toId) clearTimeout(toId);

                // start a new attempt with a delay to keep it from
                // getting too "chatty".
                toId = setTimeout(function(){
                    // call to some API that returns { isValid: true } or { isValid: false }
                    $http.get('/rest/isUerExist/' + value).success(function(data) {

                        //set the validity of the field
                        if (data == "true") {
                            ctrl.$setValidity('uniqueName', false);
                        } else if (data == "false") {
                            ctrl.$setValidity('uniqueName', true);
                        }
                    }).error(function(data, status, headers, config) {
                        console.log("something wrong")
                    });
                }, 200);
            })
        }
    }
});

HTML:

<div ng-controller="UniqueFormController">

        <form name="uniqueNameForm" novalidate ng-submit="submitForm()">

            <label name="name"></label>
            <input type="text" ng-model="name" name="name" unique-name>   <!-- 'unique-name' because of the name-convention -->

            <span ng-show="uniqueNameForm.name.$error.uniqueName">Name is not unique.</span>

            <input type="submit">
        </form>
    </div>

Controller might look like this:

app.controller("UniqueFormController", function($scope) {
    $scope.name = "Bob"
})

2 Comments

I would change this to use the angular $timeout instead of setTimeout
@ses: It works, fine! Just want to call it on onblur, or onchange. Is this possible? Is this a right way?
3

Thanks to the answers from this page learned about https://github.com/webadvanced/ng-remote-validate

Option directives, which is slightly less than I do not really liked, as each field to write the directive. Module is the same - a universal solution.

But in the modules I was missing something - check the field for several rules.
Then I just modified the module https://github.com/borodatych/ngRemoteValidate
Apologies for the Russian README, eventually will alter.
I hasten to share suddenly have someone with the same problem.
Yes, and we have gathered here for this...

Load:

<script type="text/javascript" src="../your/path/remoteValidate.js"></script>

Include:

var app = angular.module( 'myApp', [ 'remoteValidate' ] );

HTML

<input type="text" name="login" 
ng-model="user.login" 
remote-validate="( '/ajax/validation/login', ['not_empty',['min_length',2],['max_length',32],'domain','unique'] )" 
required
/>
<br/>
<div class="form-input-valid" ng-show="form.login.$pristine || (form.login.$dirty && rv.login.$valid)">
    From 2 to 16 characters (numbers, letters and hyphens)
</div>
<span class="form-input-valid error" ng-show="form.login.$error.remoteValidate">
    <span ng:bind="form.login.$message"></span>
</span>

BackEnd [Kohana]

public function action_validation(){

    $field = $this->request->param('field');
    $value = Arr::get($_POST,'value');
    $rules = Arr::get($_POST,'rules',[]);

    $aValid[$field] = $value;
    $validation = Validation::factory($aValid);
    foreach( $rules AS $rule ){
        if( in_array($rule,['unique']) ){
            /// Clients - Users Models
            $validation = $validation->rule($field,$rule,[':field',':value','Clients']);
        }
        elseif( is_array($rule) ){ /// min_length, max_length
            $validation = $validation->rule($field,$rule[0],[':value',$rule[1]]);
        }
        else{
            $validation = $validation->rule($field,$rule);
        }
    }

    $c = false;
    try{
        $c = $validation->check();
    }
    catch( Exception $e ){
        $err = $e->getMessage();
        Response::jEcho($err);
    }

    if( $c ){
        $response = [
            'isValid' => TRUE,
            'message' => 'GOOD'
        ];
    }
    else{
        $e = $validation->errors('validation');
        $response = [
            'isValid' => FALSE,
            'message' => $e[$field]
        ];
    }
    Response::jEcho($response);
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.