Working with forms in Angular often involves repeating the same steps:

  • Collecting form data​
  • Making an API call​
  • Managing loading states​
  • Handling success and error responses​
  • Disabling the submit button during processing​

If you find yourself duplicating this logic across multiple components, consider a more efficient approach.​

Typically, a form submission in Angular might look like this:​

onSubmit() {
  this.isSubmitting = true;
  const payload = this.form.value;

  this.api.saveUser(payload).pipe(
    finalize(() => this.isSubmitting = false)
  ).subscribe({
    next: res => console.log('Success', res),
    error: err => console.error('Error', err)
  });
}

While functional, this pattern can become repetitive and more challenging to maintain as your application grows.​

Introducing a Reusable Base Component
To streamline this process, we can create a base component that encapsulates the common submission logic:

// base-submit.component.ts
export abstract class BaseSubmitComponent {
  isSubmitting = false;

  // Method to create the payload
  abstract createPayload(): T;

  // Method to handle successful submission
  abstract onSubmitSuccess(response: any): void;

  protected submit(apiFn: (payload: T) => Observable) {
    this.isSubmitting = true;
    const payload = this.createPayload();

    apiFn(payload).pipe(
      finalize(() => this.isSubmitting = false)
    ).subscribe({
      next: res => this.onSubmitSuccess(res),
      error: err => this.handleError(err)
    });
  }

  protected handleError(err: any) {
    console.error('Submit error:', err);
    // Implement your error handling logic here
  }
}

With this base component, individual form components can focus on their specific logic:​

export class UserFormComponent extends BaseSubmitComponent {
  form = this.fb.group({
    name: [''],
    email: ['']
  });

  constructor(private fb: FormBuilder, private api: ApiService) {
    super();
  }

  createPayload(): UserPayload {
    return this.form.value;
  }

  onSubmitSuccess(response: any) {
    console.log('User saved successfully:', response);
    // Additional success handling
  }

  onSubmit() {
    this.submit(payload => this.api.saveUser(payload));
  }
}

Advantages of This Approach

  • Consistency: Ensures uniform handling of submissions across components.​
  • Maintainability: Centralizes common logic, making updates easier.​
  • Clarity: Keeps individual components focused on their unique responsibilities.​
  • Extensibility: Allows for easy addition of features like validation checks or pre-submission hooks.​

Enhancements to Consider

  • Shared UI Components: Create reusable components for buttons or form fields that integrate with the submission state.​
  • Centralized Error Handling: Implement a global service for displaying error messages or notifications.​
  • Form Validation: Incorporate validation logic to prevent invalid submissions.

Edge Cases to Handle

When implementing this form submission pattern, consider these important scenarios:

  • Custom Error Handling: Allow components to implement their error handling via an onSubmitError method.
  • Pre-submission Validation: Add validation checks before API calls to prevent invalid submissions.
  • Cancellable Requests: Implement a way to cancel ongoing submissions if users navigate away.
  • Form Reset Logic: Add standardized methods to reset forms after successful submissions.
  • Submission Throttling: Prevent duplicate submissions from rapid clicking.