57

I am trying to understand how to do the following things:

What is the accepted way of declaring a form. My understanding is you just declare the form in HTML, and add ng-model directives like so:

ng-model="item.name"

What to send to the server. I can just send the item object to the server as JSON, and interpret it. Then I can perform validation on object. If it fails, I throw a JSON error, and send back what exactly? Is there an accepted way of doing this? How do I push validation errors from the server to the client in a nice way?

I really need an example, but Angulars docs are pretty difficult to understand.

Edit: It seems I've phrased my question poorly.

I know how to validate client side, and how to handle error/success as promise callbacks. What I want to know, is the accepted way of bundling SERVER side error messages to the client. Say I have a username and password signup form. I don't want to poll the server for usernames and then use Angular to determine a duplicate exists. I want to send the username to the server, validate no other account exists with the same name, and then submit form. If an error occurs, how do I send it back?

What about pushing the data to the server as is (keys and values) with an error field appended like so:

{
  ...data...

  "errors": [
    {
      "context": null,
      "message": "A detailed error message.",
      "exceptionName": null
    }
  ]
}

Then binding to the DOM.

Dominic Bou-Samra
  • 14,799
  • 26
  • 100
  • 156
  • Check out the $resource module. It is exactly what you are probably looking for. http://stackoverflow.com/questions/13269882/angularjs-resource-restful-example – lucuma Apr 23 '13 at 14:08
  • Answered here http://stackoverflow.com/a/22971194/187350 – Paul Apr 24 '14 at 23:11

9 Answers9

64

I've also been playing around with this kind of thing recently and I've knocked up this demo. I think it does what you need.

Setup your form as per normal with any particular client side validations you want to use:

<div ng-controller="MyCtrl">
    <form name="myForm" onsubmit="return false;">
        <div>
            <input type="text" placeholder="First name" name="firstName" ng-model="firstName" required="true" />
            <span ng-show="myForm.firstName.$dirty && myForm.firstName.$error.required">You must enter a value here</span>
            <span ng-show="myForm.firstName.$error.serverMessage">{{myForm.firstName.$error.serverMessage}}</span>
        </div>
        <div>
            <input type="text" placeholder="Last name" name="lastName" ng-model="lastName"/>
            <span ng-show="myForm.lastName.$error.serverMessage">{{myForm.lastName.$error.serverMessage}}</span>
        </div>
        <button ng-click="submit()">Submit</button>
    </form>
</div>

Note also I have added a serverMessage for each field:

<span ng-show="myForm.firstName.$error.serverMessage">{{myForm.firstName.$error.serverMessage}}</span>

This is a customisable message that comes back from the server and it works the same way as any other error message (as far as I can tell).

Here is the controller:

function MyCtrl($scope, $parse) {
      var pretendThisIsOnTheServerAndCalledViaAjax = function(){
          var fieldState = {firstName: 'VALID', lastName: 'VALID'};
          var allowedNames = ['Bob', 'Jill', 'Murray', 'Sally'];
          
          if (allowedNames.indexOf($scope.firstName) == -1) fieldState.firstName = 'Allowed values are: ' + allowedNames.join(',');
          if ($scope.lastName == $scope.firstName) fieldState.lastName = 'Your last name must be different from your first name';
          
          return fieldState;
      };
      $scope.submit = function(){
          var serverResponse = pretendThisIsOnTheServerAndCalledViaAjax();

          for (var fieldName in serverResponse) {
              var message = serverResponse[fieldName];
              var serverMessage = $parse('myForm.'+fieldName+'.$error.serverMessage');
              
              if (message == 'VALID') {
                  $scope.myForm.$setValidity(fieldName, true, $scope.myForm);
                  serverMessage.assign($scope, undefined);
              }
              else {
                  $scope.myForm.$setValidity(fieldName, false, $scope.myForm);
                  serverMessage.assign($scope, serverResponse[fieldName]);
              }
          }
      };
}

I am pretending to call the server in pretendThisIsOnTheServerAndCalledViaAjax you can replace it with an ajax call, the point is it just returns the validation state for each field. In this simple case I am using the value VALID to indicate that the field is valid, any other value is treated as an error message. You may want something more sophisticated!

Once you have the validation state from the server you just need to update the state in your form.

You can access the form from scope, in this case the form is called myForm so $scope.myForm gets you the form. (Source for the form controller is here if you want to read up on how it works).

You then want to tell the form whether the field is valid/invalid:

$scope.myForm.$setValidity(fieldName, true, $scope.myForm);

or

$scope.myForm.$setValidity(fieldName, false, $scope.myForm);

We also need to set the error message. First of all get the accessor for the field using $parse. Then assign the value from the server.

var serverMessage = $parse('myForm.'+fieldName+'.$error.serverMessage');
serverMessage.assign($scope, serverResponse[fieldName]);
starball
  • 20,030
  • 7
  • 43
  • 238
Derek Ekins
  • 11,215
  • 6
  • 61
  • 71
  • 2
    You can use ng-submit instead of ng-click and onsubmit workaround. – Kugel Jul 30 '13 at 01:29
  • Nice solution. But two remarks: 1) it would be great to automatize the process, so I don't have to put those "span ng-show" under every field for both client and server errors; 2) the server might actually send more than one error for the same field and I'd like to show all of them, so the user doesn't have to curse me if every attempt to fill the field results in a new complain from server; 3) not sure in which cases $dirty is needed - maybe we should clear also server errors if user changes the field value? In short - a configurable solution (Angular plugin) would be great. – JustAMartin Aug 06 '15 at 10:16
  • 2
    JustAMartin, this is just some example code not a full implementation. You will need to implement anything further yourself. Feel free to post your as an additional answer. – Derek Ekins Aug 07 '15 at 12:28
  • 2
    Using `$scope.myForm.$setValidity(fieldName, false/true, $scope.myForm)` won't set `$scope.myForm.fieldName.$invalid` on `true/false` state (only the form will be). To solve this you must set the field as well `$scope.myForm[fieldName].$setValidity(fieldName, true/false);`. – adricadar Mar 30 '16 at 08:26
17

I've got similar solution as Derek, described on codetunes blog. TL;DR:

  • display an error in similar way as in Derek's solution:

    <span ng-show="myForm.fieldName.$error.server">{{errors.fieldName}}</span>
    

  • add directive which would clean up an error when user change the input:

    <input type="text" name="fieldName" ng-model="model.fieldName" server-error />
    
    angular.module('app').directive 'serverError', ->
      {
        restrict: 'A'
        require: '?ngModel'
        link: (scope, element, attrs, ctrl) ->
          element.on 'change', ->
            scope.$apply ->
              ctrl.$setValidity('server', true)
      }
    
  • Handle an error by passing the error message to the scope and telling that form has an error:

    errorCallback = (result) ->
      # server will return something like:
      # { errors: { name: ["Must be unique"] } }
      angular.forEach result.data.errors, (errors, field) ->
        # tell the form that field is invalid
        $scope.form[field].$setValidity('server', false)
        # keep the error messages from the server
        $scope.errors[field] = errors.join(', ') 
    

Hope it would be useful :)

Jan Dudulski
  • 843
  • 1
  • 8
  • 14
  • 2
    Does this work for angular-1.3/1.4 ? The value of $scope.form seems to be undefined even when the form has been named and when I try to access either with form or form name, I see that it is undefined. – Divick Mar 13 '15 at 16:35
  • @DivKis01 did you add name="form" to the form? From the [docs](https://docs.angularjs.org/api/ng/directive/ngForm): "Name of the form. If specified, the form controller will be published into related scope, under this name." – Jan Dudulski Mar 17 '15 at 09:00
  • Yeah I did add the name="form" – Divick Mar 17 '15 at 09:33
  • It only resets the validity when a user leaves the field. – x-yuri Nov 18 '21 at 14:47
5

Well, the Answer Derek Ekins gave is very nice to work on. But: If you disable the submit button with ng-disabled="myForm.$invalid" - the button will not automatically go back to enabled as the server-based error state doesn't seem to be changed. Not even if you edit ALL fields in a form again to comply with valid inputs (based on client side validation).

Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Yosh
  • 382
  • 3
  • 8
  • 3
    If you want to be able to disable the submit button then you will need to fire the validation when your input changes, otherwise there is not a way of knowing whether or not it is valid. I can make an example of this if you really want. – Derek Ekins Jul 02 '13 at 12:31
  • Yes, I want validation on input for uniqueness of barcodes. I like your implementation, BTW, it does help a lot. – Naomi Aug 24 '17 at 22:20
3

By default, the form is submitted normally. If you don't provide a name property for each field in the form then it won't submit the correct data. What you can do is capture the form before it submitted and submit that data yourself via ajax.

<form ng-submit="onSubmit(); return false">

And then in your $scope.onSubmit() function:

$scope.onSubmit = function() {
  var data = {
    'name' : $scope.item.name
  };
  $http.post(url, data)
    .success(function() {
    })
    .failure(function() {

    });
};

You can also validate the data by setting up required attributes.

matsko
  • 21,895
  • 21
  • 102
  • 144
  • @mastko. I saw your tutorial about Taming forms in AngularJs 1.3 I really liked it but I have a few questions in regards to my implementation. I want to discuss them by email, if possible. – Naomi Sep 06 '17 at 22:29
  • @Naomi please contact me through my website. – matsko Sep 14 '17 at 01:58
  • I think I tried through twitter, didn't see other ways of communication. I was able to solve my problems, though, so everything works smoothly now. I combined several ideas I saw on the web and eventually I was able to only run validators when something was changing in the form, not on load. – Naomi Sep 14 '17 at 11:43
  • Just in case this can be helpful, I wrote an article about my solution. Here is the link https://social.technet.microsoft.com/wiki/contents/articles/42394.implementing-server-side-validations-in-angularjs.aspx – Naomi Jan 30 '18 at 19:43
3

If you choose ngResource, it would look like this

var Item = $resource('/items/');
$scope.item = new Item();
$scope.submit = function(){
  $scope.item.$save(
    function(data) {
        //Yahooooo :)
    }, function(response) {
        //oh noooo :(
        //I'm not sure, but your custom json Response should be stick in response.data, just inspect the response object 
    }
  );
};

The most important thing is, that your HTTP-Response code have to be a 4xx to enter the failure callback.

kfis
  • 4,739
  • 22
  • 19
3

As of July 2014, AngularJS 1.3 has added new form validation features. This includes ngMessages and asyncValidators so you can now fire server side validation per field prior to submitting the form.

Angular 1.3 Form validation tutorial :

References:

Tony O'Hagan
  • 21,638
  • 3
  • 67
  • 78
  • Thanks, Tony. Going to study that to see if I can implement prevention of duplicates that requires a server-side test on input. – Naomi Aug 24 '17 at 22:21
  • This seems to be an excellent tutorial, I hope to be able to follow but I already have a quick question. How can I communicate with the author of this blog with my question? My question is - I want to return an error message from the server. How exactly I should implement that? – Naomi Sep 05 '17 at 22:31
  • You most likely want to use an [asyn validator](https://www.yearofmoo.com/2014/09/taming-forms-in-angularjs-1-3.html#asynchronous-validation-via-asyncvalidators). – Tony O'Hagan Sep 08 '17 at 06:04
  • You'll also want to have a look at the [latest docs for Angular validation](https://angular.io/guide/form-validation) as quite a lot has changed since Angular 1.x – Tony O'Hagan Sep 08 '17 at 06:08
  • 1
    Heres a new tutorial for [Angular 2 Form Validation](https://scotch.io/tutorials/angular-2-form-validation) – Tony O'Hagan Sep 08 '17 at 06:09
  • Thanks, Tony. We're on AngularJs 1.6.04 right now. I found several extra good tutorials and I think at the end I did achieve my goal (few more tests will be required to make sure the problem I was getting earlier is not happening). My yesterday midday problem is https://stackoverflow.com/questions/46101820/do-asyncvalidators-fire-all-the-time. I also thought few moments ago that I forgot an extra scenario of entering new information. In my case I need to also make sure I am not entering same barcode and UPC. Will have to think of a best way to handle this extra scenario – Naomi Sep 08 '17 at 12:12
  • This [discussion](https://stackoverflow.com/questions/31736496/why-does-adding-additional-angularjs-validation-directives-cause-asyncvalidator) might also be helpful. – Tony O'Hagan Sep 11 '17 at 00:15
  • Yes, I saw that discussion (and even left a comment). I was able to prevent firing server side code in page load by saving original value in the scope variable. It did took me many iterations before I solved it (I hope). – Naomi Sep 11 '17 at 01:13
2

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.:

    Change password

    Current Required Incorrect current password. Please enter your current account password.
    <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>
    
Paul
  • 12,392
  • 4
  • 48
  • 58
1

As variant

// ES6 form controller class

class FormCtrl {
 constructor($scope, SomeApiService) {
   this.$scope = $scope;
   this.someApiService = SomeApiService;
   this.formData = {};
 }

 submit(form) {
   if (form.$valid) {
     this.someApiService
         .save(this.formData)
         .then(() => {
           // handle success

           // reset form
           form.$setPristine();
           form.$setUntouched();

           // clear data
           this.formData = {};
         })
         .catch((result) => {
           // handle error
           if (result.status === 400) {
             this.handleServerValidationErrors(form, result.data && result.data.errors)
           } else {// TODO: handle other errors}
         })
   }
 }

 handleServerValidationErrors(form, errors) {
  // form field to model map
  // add fields with input name different from name in model
  // example: <input type="text" name="bCategory" ng-model="user.categoryId"/>
  var map = {
    categoryId: 'bCategory',
    // other
  };

  if (errors && errors.length) {
    // handle form fields errors separately
    angular.forEach(errors, (error) => {
      let formFieldName = map[error.field] || error.field;
      let formField = form[formFieldName];
      let formFieldWatcher;

      if (formField) {
        // tell the form that field is invalid
        formField.$setValidity('server', false);

        // waits for any changes on the input
        // and when they happen it invalidates the server error.
        formFieldWatcher = this.$scope.$watch(() => formField.$viewValue, (newValue, oldValue) => {
          if (newValue === oldValue) {
            return;
          }

          // clean up the server error
          formField.$setValidity('server', true);

          // clean up form field watcher
          if (formFieldWatcher) {
            formFieldWatcher();
            formFieldWatcher = null;
          }
        });
      }
    });

  } else {
    // TODO: handle form validation
    alert('Invalid form data');
  }
}
Bohdan Lyzanets
  • 1,652
  • 22
  • 25
0

As I understand the question is about passing errors from the server to the client. I'm not sure if there are well-established practices. So I'm going to describe a possible approach:

<form name="someForm" ng-submit="submit()" ng-controller="c1" novalidate>
    <input name="someField" type="text" ng-model="data.someField" required>
    <div ng-show="someForm.$submitted || someForm.someField.$touched">
        <div ng-show="someForm.someField.$error.required" class="error">required</div>
        <div ng-show="someForm.someField.$error.someError" class="error">some error</div>
    </div>
    <input type="submit">
</form>

Let's say a server returns an object of the following kind:

{errors: {
    someField: ['someError'],
}}

Then you can pass the errors to the UI this way:

Object.keys(resp.errors).forEach(i => {
    resp.errors[i].forEach(c => {
        $scope.someForm[i].$setValidity(c, false);
        $scope.someForm[i].$validators.someErrorResetter
            = () => $scope.someForm[i].$setValidity(c, true);
    });
});

I make each field invalid and add a validator (which is not really a validator). Since validators are called after every change, this let's us reset the error status.

You can experiment with it here. You might also want to check out ngMessages. And a couple of related articles.

x-yuri
  • 16,722
  • 15
  • 114
  • 161