Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

[TinyERP: SPA for Enterprise Application] Handle Error/ Validation

5.00/5 (1 vote)
21 Dec 2018Apache8 min read 5.2K  
[TinyERP: SPA for Enterprise Application] Handle Error/ Validation

Note: This article in part of a series of articles related to building enterprise applications. Please have a look at the previous article first, this will help you understand the context we were talking about.

Overview

In this article, let's see how to implement validation flow in TinyERP.

Update addNewStaffModel.ts

Open addNewStaffModel.ts file and add "required" decorator as below:

JavaScript
export class AddNewStaffModel extends BaseModel{
    @required("hrm.addNewStaff.firstNameWasRequired")
    public firstName:string;
    public lastName:string;
}

In this change, we add "required" over firstName. It means that firstName was required field. If we create new staff without value for firstName field, system will raise validationFail event with "hrm.addNewStaff.firstNameWasRequired" as error key.

Due to multi languages supported, we use "hrm.addNewStaff.firstNameWasRequired" path instead of "specified text". For more information about multi languages, see "Multiple languages".

When will the validation be raised?

In onSaveClicked in addNewStaff.ts, we have this code:

JavaScript
public onSaveClicked():void{
	if(!this.model.validated()){
		return;
	}
	// other logic
}

Ad #2 line, system will call the validated method. If the model passes all validation rules, we can continue with other business logic (create new staff in this case). Otherwise, system will raise "OnValidationFailed" event.

How does validated method work?

Look at addNewStaffModel.ts again, we see that this class inherits from BaseModel class:

JavaScript
export class AddNewStaffModel extends BaseModel{
    // body of AddNewStaffModel class
}

and BaseModel also implements the necessary logic for this method also. I mean that in addNewStaffModel, we do not need to implement this. Just add appropriated over class' property/ field.

Where will this error message be displayed?

It was up to you. In validation, we based on "hrm.addNewStaff.firstNameWasRequired" errorKey. There are some ways to display this error.

If you want to highlight the appropriated form input, let's use validation directive as below:

HTML
<horizontal-form>
	<form-text-input 
		[validation]="['hrm.addNewStaff.firstNameWasRequired']"
		[labelText]="'First Name'" 
		[(model)]=model.firstName></form-text-input>
	<form-text-input 
		[labelText]="'Last Name'" 
		[(model)]=model.lastName></form-text-input>
	<form-primary-button [label]="'Save'" 
	(onClick)="onSaveClicked($event)"></form-primary-button>
	<form-default-button [label]="'Cancel'" 
	(onClick)="onCancelClicked($event)"></form-default-button>
</horizontal-form>

At line 3, we tell the system to display "hrm.addNewStaff.firstNameWasRequired" validation error here.

Let's create new staff and click on "Save" without value for firstName, the validation error will be displayed on form as:

Image 1

The text input was highlighted, hover on this, "First name was required field" was displayed as tool tip. This provides more information about the error.

If you prefer to change css for this, please use inspect tool on browser and check yourself. There are some classes that need to be changed, such as: validation__invalid, ... We use BEM rule for css class.

For the tool-tip message, please update it in appropriated json files (multiple language files). It was "hrm.en.json" file located in "src/resources/locales" in this case. See picture above.

If you want to display all error messages above the form, let's change the HTML as:

HTML
<page>
    <page-header>Add new Staff</page-header>
    <page-content>
        <error-message 
            [messages]="['hrm.addNewStaff.firstNameWasRequired' ]"
        ></error-message>
        <horizontal-form>
            <form-text-input 
                [labelText]="'First Name'" [(model)]=model.firstName></form-text-input>
            <form-text-input 
                [labelText]="'Last Name'" [(model)]=model.lastName></form-text-input>
            <form-primary-button [label]="'Save'" (onClick)="onSaveClicked($event)">
            </form-primary-button>
            <form-default-button [label]="'Cancel'" (onClick)="onCancelClicked($event)">
            </form-default-button>
        </horizontal-form>
    </page-content>
</page>

Just remove the above validation directive in form-text-input for firstName and add error-message above form component.

"messages" input property contains the list of error keys we want to display. such as: 'hrm.addNewStaff.firstNameWasRequired' in this case.

Let's refresh the page and click on "Save" button again, this is the result:

Image 2

We can see "First name was required" was displayed above the form.

We can add more validation rule for lastName and more rule for firstName:

export class AddNewStaffModel extends BaseModel{
    @valueInRange(1,5,"hrm.addNewStaff.firstNameMax5Letter")
    @required("hrm.addNewStaff.firstNameWasRequired")
    public firstName:string;
    @required("hrm.addNewStaff.lastNameWasRequired")
    public lastName:string;
}

and update HTML as:

HTML
<page>
    <page-header>Add new Staff</page-header>
    <page-content>
        <error-message 
            [messages]="[
                'hrm.addNewStaff.firstNameWasRequired',
                'hrm.addNewStaff.lastNameWasRequired',
                'hrm.addNewStaff.firstNameMax5Letter'
            ]"
        ></error-message>
        <horizontal-form>
            <form-text-input 
                [labelText]="'First Name'" [(model)]=model.firstName></form-text-input>
            <form-text-input 
                [labelText]="'Last Name'" [(model)]=model.lastName></form-text-input>
            <form-primary-button [label]="'Save'" (onClick)="onSaveClicked($event)">
            </form-primary-button>
            <form-default-button [label]="'Cancel'" (onClick)="onCancelClicked($event)">
            </form-default-button>
        </horizontal-form>
    </page-content>
</page>

Just add more validation error into error-message component.

Let's compile and refresh the page. Just leave the form empty and click "Save" button:

Image 3

There were 3 errors displayed on the form. Let's try to enter some text (more than 5 letters) into firstName textbox and click on "Save" again:

Image 4

We receive 2 error messages as above.

Ok, that was good, but the list of error messages may be long (it was 3 in this case), some complex form, it can be 20 errors we want to display. So, we can replace 3 error keys by "hrm.addNewStaff.", because all error validation always starts by "hrm.addNewStaff.", So we can use "hrm.addNewStaff." for short, it means that the error-message component will display all raised fail validation with error key starting with "hrm.addNewStaff.". So please use this carefully. the error-message component was changed as:

HTML
<error-message 
	[messages]="['hrm.addNewStaff.']"
></error-message>

Let refresh the page and click on "Save" button again, we receive the same:

Image 5

Ok, review a little.

  • In addNewStaffModel, we add a appropriated validation decorator, such as: required, valueInRange in this case.
  • In validation, we use error key which reference the specified text in locale file. See multi languages.
  • We can show error message in specified form-element (using validation directive) or show all error messages together (using error-message directive above form component).
  • In case of multiple error keys, we can use shorten form, such as: "hrm.addNewStaff." for all error keys starting with "hrm.addNewStaff.".

Update addNewStaffModel Class

Let's continue, there are some cases, we did not have available decorator for our validation, we can add custom validation rule into addNewStaffModel class, as below:

JavaScript
protected getValidationErrors(): ValidationException {
	let validator:ValidationException = new ValidationException();
	let fullName=String.format("{0} {1}", this.firstName, this.lastName);
	if(fullName.length>20){
		validator.add("hrm.addNewStaff.fullNameTooLong");
	}
	return validator;
}

In this method, just add some extra validation which was not supported by available validation decorator or hard to implement by validation decorator. For example, max length for full name as in this case.

We can add more errors as we need to return ValidationException object and the result:

Image 6

And the content for hrm.en.json file as below:

JavaScript
{
    "addNewStaff":{
        "firstNameWasRequired":"First name was required field",
        "lastNameWasRequired":"Last name was required",
        "firstNameMax5Letter":"First name should be in 1 (min) and 5(max) leters",
        "fullNameTooLong":"Full name should not exceed 20 letters."
    }
}

We see that the max-length for full-name can be changed, we can consider to extend to 30 letters. This requires that we update the locale file also. This may raise potential mistake for development team. So, we should consider to move number "20" to config file and replace the string in locale file to "Full name should not exceed {{MAX_FULLNAME_LENGTH}} letters.". Ideally, at run time, we will pass value for "MAX_FULLNAME_LENGTH".

Update the custom validation method:

JavaScript
protected getValidationErrors(): ValidationException {
	let validator:ValidationException = new ValidationException();
	let fullName=String.format("{0} {1}", this.firstName, this.lastName);
	if(fullName.length > HrmConst.MAX_FULLNAME_LEGNTH){
		validator.add(
			"hrm.addNewStaff.fullNameTooLong",[
				{key:"MAX_FULLNAME_LENGTH", value: HrmConst.MAX_FULLNAME_LEGNTH}
			]);
	}
	return validator;
}

Please note that we add more parameters into validator.add method. We can have more than 1 parameter. There is a new const variable HrmConst.MAX_FULLNAME_LENGTH and set value for this is 30. Just want to make sure the error was reloaded.

Also update message in hrm.en.json file:

JavaScript
{
    "addNewStaff":{
        "firstNameWasRequired":"First name was required field",
        "lastNameWasRequired":"Last name was required",
        "firstNameMax5Letter":"First name should be in 1 (min) and 5(max) leters",
        "fullNameTooLong":"Full name should not exceed {{MAX_FULLNAME_LENGTH}} letters."
    }
}

We also use that parameter in locale text. Let's build and run again, we have:

Image 7

From now, we only need to update new value for HrmConst.MAX_FULLNAME_LEGNTH const.

Ok this is for client side. What about server side? How can server side response fail validation back to client side and display on UI.

On server, we must re-validate all validation rule on UI. As API can be called from multiple parties or user can use some tool and pass the client validation.

Update validation attribute for CreateStaffRequest.cs class:

Client send create staff request to server, appropriated information was stored in CreateStaffClass and send this request to StaffCommandHandler.cs class. So let's add some validation rules for CreateStaffRequest:

C#
public class CreateStaffRequest: IBaseCommand
{
	[Required("hrm.addNewStaff.firstNameWasRequired")]
	public string FirstName { get; set; }
	[Required("hrm.addNewStaff.lastNameWasRequired")]
	public string LastName { get; set; }
	/*other */
}

There are some available validation attributes that were defined in TinyERP. They work in the same way as validation decorator on client side. Just receive the error key as parameter.

We need to use the error key the same as client side as each key mapped to locale text. So it was not reasonable to use 2 difference texts for the same failure of validation, such as: hrm.addNewStaff.firstNameWasRequired in this case.

Ok, the API can be called by rest client. So let's compile and check using REST client tool:

Image 8

In this case, I send request to create new staff with empty first name and receive result with error as photo. Let's present request with empty in both firstName and lastName:

Image 9

And receive the fail validation for both firstName and lastName.

In some cases, we want to perform custom validation, for example: full-name should not be exceed 20 letters. For this, we need to add custom validation rule into StaffCommandHandler.cs, open Validate(CreateStaffRequest command) method and add new custom rules:

C#
private void Validate(CreateStaffRequest command)
{
	IValidationException validator = ValidationHelper.Validate(command);
	if (string.Format("{0} {1}", command.FirstName, command.LastName).Length > 
        HRMConst.MAX_FULLNAME_LEGNTH) {
		validator.Add(new ValidationError("hrm.addNewStaff.fullNameTooLong"));
	}
	validator.ThrowIfError();
}

We can see that, on the server side, we mostly do in the same way as client side. We can also pass in the parameters for locale text as below:

C#
private void Validate(CreateStaffRequest command)
{
	IValidationException validator = ValidationHelper.Validate(command);
	if (string.Format("{0} {1}", command.FirstName, command.LastName).Length > 
              HRMConst.MAX_FULLNAME_LEGNTH) {
		validator.Add(new ValidationError(
			"hrm.addNewStaff.fullNameTooLong",
			"MAX_FULLNAME_LENGTH",
			HRMConst.MAX_FULLNAME_LEGNTH.ToString()
			));
	}
	validator.ThrowIfError();
}

Let compile the API and try again:

Image 10

It was not enough, we may want to do more validation rule, for example, staff was not allowed to have the same firstName and lastName (just assumption). Let's add more validation rule into Validate method:

C#
private void Validate(CreateStaffRequest command)
{
	IValidationException validator = ValidationHelper.Validate(command);
	if (string.Format("{0} {1}", command.FirstName, command.LastName).Length > 
        HRMConst.MAX_FULLNAME_LEGNTH) {
		validator.Add(new ValidationError(
			"hrm.addNewStaff.fullNameTooLong",
			"MAX_FULLNAME_LENGTH",
			HRMConst.MAX_FULLNAME_LEGNTH.ToString()
			));
	}

	IStaffRepository repo = IoC.Container.Resolve<IStaffRepository>();
	if (repo.Exists(command.FirstName, command.LastName)) {
		validator.Add(new ValidationError(
				"hrm.addNewStaff.fullNameWasExisted",
				"FIRST_NAME", command.FirstName,
				"LAST_NAME", command.LastName
			));
	}
	validator.ThrowIfError();
}

Just check against database if staff which has the same both first name and last name. I suggest that you should use const for "FIRST_NAME", "LAST_NAME",...

I'm definitely sure that the validation was still returned to client in the same way as "hrm.addNewStaff.fullNameTooLong" validation rule.

Please add "hrm.addNewStaff.fullNameWasExisted" into your locale file also.

Let's integrate with client side. Please remove all client validation, so it will be easier for us to check server validation.

Send request with both first name and last name were empty:

Image 11

Create staff with fullName longer than 20 letters:

Image 12

And create staff with existing first-name and last-name:

Image 13

Ok, we can see validation on both client and server can integrate together really well.

Summary

Let's review what we learned in this article:

  • For the validation, we can do it on both client and server side.
  • In validation, we use error key instead of specified text. The UI will resolve to specified text based on current language. See multiple languages for more information.
  • On client side, we can add existing validation decorator directly over properties of model. For example, addNewStaffModel in this article.
  • Or we can add custom validation rule, by overriding getValidationErrors function declared in BaseModel class.
  • On server side, we can do the same by using available validation attribute in "TinyERP.Common.Validation.Attribute" namespace or add custom validation rule.

For more information about source-code for this part, please have a look at https://github.com/tranthanhtu0vn/TinyERP (in feature/handle_error).

Other Articles in the Series

Thank you for reading!

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0