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:
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:
public onSaveClicked():void{
if(!this.model.validated()){
return;
}
}
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:
export class AddNewStaffModel extends BaseModel{
}
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:
<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:
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:
<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:
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's compile and refresh the page. Just leave the form empty and click "Save" button:
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:
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:
<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 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:
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 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:
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:
{
"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:
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
:
public class CreateStaffRequest: IBaseCommand
{
[Required("hrm.addNewStaff.firstNameWasRequired")]
public string FirstName { get; set; }
[Required("hrm.addNewStaff.lastNameWasRequired")]
public string LastName { get; set; }
}
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:
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
:
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:
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's 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'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:
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'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!
CodeProject