Server-side Validations with Angular 2

Server-side data validation is necessarily still a thing with Single Page Applications. While rendering JSON validation errors in the backend is ridiculously simple with Rails, I’ll show you how you can handle these responses on the client-side and integrate them in your Angular 2 forms.

Rendering server-side validation errors

In my previous article about Rails 5 and its new API mode, I’ve briefly mentioned rendering of server-side validation errors in a JSON format. All it takes is the rendering of the model’s error details, preferably with a HTTP status code 422 (Unprocessable Entity):

def create
  category = Category.new(category_params)

  if category.save
    render json: category
  else
    render json: category.errors.details,
           status: :unprocessable_entity
  end
end

Rails then returns a JSON response with the invalid attributes as keys and an array of errors and additional parameters as values:

{"name":[{"error":"taken","value":"fruits"}]}

For validation errors not concerning a specific attribute, Rails uses the key base.

You can obviously render validation errors with any other backend technology in a similar fashion.

An example

Let’s assume we have a web application with a form to create categories. A category has an attribute „name“ with the following constraints:

  • required
  • maximum length of 10 characters
  • unique

While the required and maximum length constraints can (and should) already be validated on the client (in addition to the server), the unique validation can only be performed on the server. In case of a unique error, it is ideally presented to the users in the same way the other client-side errors are. With our example we will eventually achieve this behavior:

Client- & server-side validation with Angular 2

The complete source code of the example is available on Github. Checkout the README file for the instructions on how to setup the frontend and backend.

Building a reactive form with Angular 2

First, you need to have a working Angular 2 form that allows to do client-side validations. In this example we are using the reactive forms approach.

Form template

Here is how the template may look like:

Notice these things:

  • the

    tag binds to the formGroup directive

  • the formControlName on the form fields correlates them with the Angular form controls

Futhermore a component is used to render generic errors (i.e. with key base) and components are used to render field-specific errors. Typically these two kinds of errors are styled differently.

Form class

In the form component class, the FormGroup instance is created using the FormBuilder and the client-side validations are defined:

form: FormGroup;

constructor(fb: FormBuilder) {
  this.form = fb.group({
    name: ['', Validators.compose([
      Validators.required,
      Validators.maxLength(10)])
    ],
    description: ['']
  });
}

In the onSubmit() function, the form’s values are only submitted if it is valid:

onSubmit() {
  this.submitted = true;

  if (this.form.valid) {
    this.restService.create(this.form.value)
      .subscribe(
        entry => this.handleSubmitSuccess(entry),
        error => this.handleSubmitError(error)
      );
  }
}

There are also two functions used in the template to fetch the generic or field-specific errors:

formErrors(): FormError[] {
  if (this.submitted && this.form.errors) {
    return this.getErrors(this.form);
  }
}

fieldErrors(name: string): FormError[] {
  let control = this.findFieldControl(name);
  if (control && (control.touched || this.submitted) &&
      control.errors) {
    return this.getErrors(control);
  } else {
    return undefined;
  }
}

In case of client-side validation errors, the findFieldControl() function basically does a this.form.get(field) to get the form control in question.

Integrating server-side validation errors

To integrate server-side validation errors, .setErrors() is called on the corresponding form control to use the same infrastructure for the handling and rendering of the form errors.

Since the validation error responses all have status code 422, they can be handled in the this.handleSubmitError(error) function that is used as the Observable’s onError callback in onSubmit().

protected handleSubmitError(error: any) {
  if (error.status === 422) {
    const data = error.json();
    const fields = Object.keys(data || {});
    fields.forEach((field) => {
      const control = this.findFieldControl(field);
      const errors = this.fetchFieldErrors(data, field);
      control.setErrors(errors);
    });
  }
}

With the fetchFieldErrors() helper, the validation error from the server response in the format [{ error: , : , ... }] is converted to the format for the form control: { : {: , ... } }

The other helper, findFieldControl(), returns the form control with the given name, but also falls back to the form group instance if it is a base error or returns the from controls of nested form groups.

You may study the implementation of these functions for more details.

Error presentation and i18n

Earlier, I’ve mentioned the and components that are used to render the errors. The component translates and outputs all errors of a certain form control:


  {{ 'errors.messages.' + e.error | translate:e.params }}

In case of Rails, you may get the possible error keys and their translations from the rails-i18n project’s locale files and add them to the ng2-translate locale file. Translations for the Angular validations also have to be added (be aware that the same keys are used for both kinds of errors, so appropriate translations must be chosen).

Conclusion

From a user’s perspective the presentation of server-side validation errors in the same way as client-side ones is a real benefit. Angular 2 gives us good instruments to realize this, although there is still quite some work required.

The whole validation logic can easily be put in a generic form component from which concrete model form components may inherit using component inheritance.

Image credit: „Trespass Guard“ by Mathis Hofer, 2008, CC BY-SA 3.0

Kommentare sind geschlossen.