To create a friendly interface, you need to ensure that all forms in your application behave consistently. Monotonous behavior is often achieved by repetitive code, albeit implicitly. Let me share a sketch of a pattern that I think simplifies development and standardizes form behavior.
If the code for submitting forms in your project is similar to this one, I advise you to look under cat.
onSubmit (): void
// login.component.ts
// bad practices
onSubmit(): void {
this.formSubmitted = true;
this.isUnhandledServerError = false;
if (!this.formGroup.valid) return;
this.isLoading = true;
const { username, password } = this.formGroup.value;
this.login(username, password)
.pipe(finalize(() => (this.isLoading = false)))
.subscribe({ error: error => this.handleError(error) });
}
For those who just love code:
Project on stackblitz before refactoring.
Stackblitz project after refactoring.
Description of the problem
Forms require many nuances to be taken into account. From a functional point of view, the form just sends the information entered by the user to the server. But to ensure a high-quality UX, in addition to everything, you have to do validation, display errors from the server, a loading indicator, etc. In practice, these details are often overlooked by developers, which either negatively affects the comfort of using the application, or results in code duplication and turns the development of forms into an intolerable routine.
Here's an example of a form submission handler that is good from a UX point of view, but bad from a development point of view. Stackblitz project before refactoring.
// login.component.ts
onSubmit(): void {
this.formSubmitted = true; //
this.isUnhandledServerError = false; //
if (!this.formGroup.valid) return; //
this.isLoading = true; //
const { username, password } = this.formGroup.value;
this.login(username, password) //
.pipe(finalize(() => (this.isLoading = false))) //
.subscribe({ error: error => this.handleError(error) });
}
As you can see, this handler takes into account a lot of the details that make up the UX. The only problem is that with this approach, these nuances will have to be written for each form in the application.
Decision
To simplify development and standardize the behavior of forms in your application, you need to move the form submission handler code into a separate class. Stackblitz project after refactoring. (I intentionally simplified the code for the example, in a real project you need to replace all boolean fields with Observable.)
class Form<T> {
submitted = false;
pending = false;
hasUnhandledServerError = false;
constructor(private formGroup: FormGroup, private action: (value: any) => Observable<T>) {}
submit(): Observable<T> {
if (this.pending) return EMPTY;
this.submitted = true;
this.hasUnhandledServerError = false;
if (this.formGroup.valid) {
this.pending = true;
return this.action(this.formGroup.value).pipe(
tap({ error: () => (this.hasUnhandledServerError = true) }),
finalize(() => (this.pending = false)),
);
}
return EMPTY;
}
}
Thus, we concentrate most of the UX features in one class and get rid of duplicate logic. Now writing a new form will take less time, and you can complete the behavior of forms throughout the application by changing only the Form class.
Why not put it in the library?
The UX requirements for each project are unique and more dependent on the designer. I have already had to redefine the behavior of standard Material elements at the request of the customer. Therefore, I do not see any way to standardize the behavior of forms in all applications using one library. Let the behavior of the interface remain at the mercy of the designer and developers. However, I think it's a good idea to separate UX-related logic into separate classes.
I hope the example was helpful and you will try to use the idea in your projects. While!