Many frontend applications require the extensive use of forms to allow users entering information. Angular supports various mechanisms to handle forms, but I’ve struggled to figure out how to handle validations of data on different pages/routes. Below is a pattern I’ve used based on Redux which doesn’t leverage much of the Angular forms functionality but works very nicely for me.
Initially I thought that my requirements would be pretty straight forward and common for single-page applications. However there were two things which I didn’t manage to address with core Angular forms functionality.
- Display validation errors for multiple forms on different pages (pages from an user experience perspective; technically routes) including for forms that are not currently visible. In my case I had a sidebar component with validation errors which was displayed on every page.
- Display validation errors for forms that have not been loaded yet. This was necessary since I read an initial set of data which was used to pre-populate forms.
Angular supports two types of forms handling – template driven forms and reactive forms. Since the reactive forms provide more flexibility I started to use them. My hope was to somehow utilize the router to load all pages without showing them to trigger the out of the box form validations. Unfortunately I didn’t get this to work without a lot of flickering and weird behavior in the user interface.
A separate issue I had was refreshing of components. Read my older blog entry My Advice: Don’t use Angular 2+ without Redux for details. Because of the refresh issues and the form validation issues I decided to do a bigger refactoring of my code to use Redux.
I have open sourced an application which demonstrates how Redux helps to validate data for forms before forms have been loaded. The following screenshot shows an validation error of the password field and in the right column the Redux Chrome extension which displays state information.
Here is an overview of how this has been implemented. Let’s start with the HTML for the password field.
In the component’s Typescript file observables are used for the value of the password field and the validation errors. With the “xxx$ | async” notation above the values are displayed in the user interface when they change. |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@select(['tool', 'watsonPassword']) watsonPassword$: Observable<boolean>;
validationErrorsPassword$: Observable<ValidationError[]>;
ngOnInit(): void {
this.skillOverviewForm = this.formBuilder.group({});
...
this.validationErrorsPassword$ = this.ngRedux.select((state) => {
let errors: ValidationError[] = [];
state.tool.validationErrors.forEach((validationError) => {
if (validationError.itemName == 'watsonPassword') errors.push(validationError);
});
return errors;
});
...
}
onKeyUpWatsonPassword(event) {
let value: string;
let element: any;
element = document.getElementById('watsonPassword');
if (element) {
value = element.value;
this.ngRedux.dispatch({ type: ToolAction.SET_WATSON_USER_PASSWORD, payload: value });
}
}
When users change the password the Redux reducer is invoked which sets the new value in the Redux store and triggers the validation logic.
1
2
3
4
5
6
7
8
9
10
11
12
export function toolReducer(state: Tool = INITIAL_TOOL_STATE, action: any) {
...
switch (action.type) {
case ToolAction.SET_WATSON_USER_PASSWORD:
newTool.watsonPassword = action.payload;
Validations.validate(newTool);
return newTool;
...
default:
return state;
}
}
In the Validations class all errors are initially deleted and (re-)created if necessary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export class Validations {
static validate(tool: Tool) {
tool.validationErrors = [];
Validations.watsonPassword(tool);
...
}
static watsonPassword(tool: Tool): void {
let itemName: string = 'watsonPassword';
let error: ValidationError;
let value: string = "";
let validatorOutput: boolean;
let type: string;
if (tool.watsonPassword) {
value = tool.watsonPassword;
}
validatorOutput = Validators.required(value);
type = Validators.errorTypeRequired;
if (validatorOutput) {
error = new ValidationError(itemName, 'The Watson Conversation password is required.');
tool.validationErrors.push(error);
}
...
}
}
Check out the full sample application Conversation Inspector for IBM Watson for details.