TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

[TinyERP: SPA for Enterprise Application]Handle error/ validation

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

Overview

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

Update addNewStaffModel.ts

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

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" (https://www.tranthanhtu.vn/post/2018/12/08/tinyerp-spa-for-enterprise-application-multiple-languages).

When will the validation be raised?

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

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

Ad #2 line, system will call to 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 raises "OnValidationFailed" event.

How does validated method work?

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

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

and BaseModel also implements 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 appropriated form input, let use validation directive as below:

<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 system to display "hrm.addNewStaff.firstNameWasRequired" validation error here.

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

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 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 change the html as:

<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 above validation directive in form-text-input for firstName and add error-message above form component.

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

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

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:

<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 compile and refresh the page. Just leave the form empty and click "Save" button:

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

We receive 2 error message 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 starting 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:

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

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

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 to 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 togheter (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  continue, there are some case, we did not have available decorator for our validation, we can add custom validation rule into addNewStaffModel class, as below:

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:

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

{
    "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 require we update the locale file also. this 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:

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 parameter into validator.add method. We can have more than 1 parameter. There is new const variable HrmConst.MAX_FULLNAME_LEGNTH and set value for this is 30. Just want to make sure the error was reloaded.

and also update message in hrm.en.json file:

{
    "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 build and run again, we have:

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

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

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 attribute was defined in TinyERP. They works 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 compile and check using REST client tool:

 

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

 

 And receive the fail validation for both firstName and lastName.

Some case, 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:

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:

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:

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 add more validation rule into Validate method:

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 definitely sure, 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 integrate with client side. Please remove all client validation, so it was easier for us to check server validation.

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

Create staff with fullName longer than 20 letters:

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

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

Summary

let review what did we learn in this article:

  • For the validation, we can do 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 override 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 series

 Thank you for reading,

 

 

 

Comments are closed