DEV Community

Cover image for Angular Form Architecture: Achieving Separation of Concerns with a FormHandlerService
Cezar Pleșcan
Cezar Pleșcan

Posted on • Edited on

Angular Form Architecture: Achieving Separation of Concerns with a FormHandlerService

Introduction

In my previous articles, I've focused on managing the data flow in our Angular user profile form, separating concerns between components and services. We successfully extracted the data access logic into a UserService and created a HttpHelperService to handle form data preparation. While this was a great step towards a cleaner architecture, we can further refine our form handling to enhance maintainability and reusability.

In this article, I'll tackle the challenge of managing form related logic within our Angular component. I'll focus on decoupling this logic from the component core responsibility of UI interactions, ensuring that our code adheres to the Single Responsibility Principle (SRP) and is more adaptable to future changes.

What I'll cover here

I'll guide you through the following steps:

  • Identifying form handling logic - pinpoint specific methods and properties within the UserProfileComponent that relate to form management.
  • Creating the FormHandlerService - a new service to house the extracted form handling logic.
  • Implementing reusable methods - craft generic and reusable methods in the service to handle tasks like form updates, validation, and state management.
  • Integrating the service with the component - modify the UserProfileComponent to delegate form related operations to the FormHandlerService.
  • Considering alternatives and best practices - explore different approaches for organizing and sharing form handling logic, discussing their pros and cons.

By the end of this article, you'll have a deeper understanding of how to apply the Single Responsibility Principle to Angular forms, create reusable services for form logic, and build more maintainable and flexible frontend applications.

A quick note:

  • This article builds upon the concepts and code developed in previous articles in this series. If you're new here, I highly recommend catching up on the earlier articles to make the most of this one.
  • You can find the starting point for the code I'll be working with in the 17.error-interceptor branch of the repository.

Dealing with the form logic in the component

As I examine the user-profile.component.ts file, I see a lot of code related to form handling. This includes things like updating the form values, displaying error messages, or figuring out whether the form has been changed.

I want to clean things up and make my code more organized. So, I'm going to follow the separation of concerns principle and move some of this form handling logic out of the component and into a new service. This way, the component can focus on its main job: managing how the form looks and feels for the user.

Finding the right pieces to extract

I've spotted two methods that seem like good candidates for moving to a separate service: updateForm() and setFormErrors().

The updateForm() method updates the form values, and it doesn't really depend on anything else in the component. It's a pretty general function that could be used with any form.

The second one, setFormErrors(), handles setting error messages on the form based on a response from the server. It's also not specific to this component; it could be used with other forms too.

There are other methods that work with the form too, like restoreForm, isFormPristine, isSaveButtonDisabled, or isResetButtonDisabled, but I'll tackle those later. For now, I want to simplify the component and extract the logic from updateForm() and setFormErrors() methods into a new service.

Creating the FormHandlerService

I'll use the Angular CLI to create a new service in the src/app/services folder: ng generate service form-handler. Then, I'll move the updateForm() and setFormErrors() methods into this new service. Here's what the FormHandlerService looks like after moving the code:

At this moment the app is broken because the component doesn't have those methods anymore, and the service doesn't know which form to work on.

Fixing the service code

I'll update the two methods to accept the form instance as a parameter:

Additionally, I'll create a separate file src/app/shared/types/form.type.ts containing the definition of the FormModel interface:

I've also made the service more flexible by using generic types <FormType extends FormModel, DataType extends FormType> so that it can work with different kinds of forms and data structures.

Updating the UserProfileComponent

Now, let's update our component to use this new service:

  • remove the original updateForm() and setFormErrors() methods from the component.
  • inject the FormHandlerService into the component.
  • call the service methods where needed, passing in the form object as an argument.

Here is the updated component code:

The power of reusability

With these changes, we've successfully extracted some of the form handling logic into a separate service. This makes the component cleaner, easier to understand, and more focused on its job of managing the UI. The FormHandlerService is now reusable; we can use it with other forms in our application, too.

isFormPristine() method

Next I'll examine the logic in the isFormPristine() method. It checks if the form value matches the last saved data from the backend. This method relies on two things: the original user data and the current value of the form.

The logic inside isFormPristine() isn't specific to our component, so it's a good candidate for moving to the FormHandlerService. This way, we can reuse it with other forms throughout the application. To make this happen, I'll pass both the form instance and the last saved data as parameters to the method:

Then, I'll update the UserProfileComponent: remove the isFormPristine() method and replace it with this.formHandlerService.isFormPristine() in the two methods where it's used:

Form button logic

But wait, there's more! The logic in the isSaveButtonDisabled() and isResetButtonDisabled() methods also looks pretty reusable. These methods determine when to disable the "Save" and "Reset" buttons, depending on whether the form is valid, pristine, or if a save operation is in progress.

Let's refactor these too and move them into our service:

And now update the UserProfileComponent, by simply delegating the calls to these new service methods:

You might be thinking, "Whoa, this is a lot of arguments to pass to the service!" And you're right, it does look like more code at first. But trust me, this will really pay off when you reuse the FormHandlerService in other components later on.

restoreForm() method

Taking a closer look at the restoreForm() method, I see that it's responsible for resetting the form to its initial or pristine state. This logic is directly related to the form and its values, making it another candidate for extraction into our FormHandlerService.

By moving this method to the service, we:

  • consolidate the form logic, keeping all form related operations centralized within the FormHandlerService, adhering to the Single Responsibility Principle (SRP).
  • enhance reusability, making the restoreForm logic available to other components that might need it.
  • simplify the component, removing unnecessary code from the UserProfileComponent, allowing it to focus more on its core UI responsibilities.

Let's see the refactored code of both the service and the component:

Separate service instance for each component

You might have noticed that all of the service methods currently require the form object as an argument. This can lead to a bit of extra work when calling these methods repeatedly. What if we could pass the form only once when the service is first injected into the component?

This would involve storing the form object within the service instance itself. To achieve this, we'll need to have multiple instances of the FormHandlerService, one for each component that injects it. Fortunately, Angular's dependency injection system allows us to easily provide services at the component level, ensuring each component gets its own dedicated instance.

Modifying the FormHandlerService

Here's the updated service class:

The key changes:

  • I've removed {providedIn: 'root'}. This ensures the service is no longer a singleton shared across the entire application.
  • The service now contains the form property to store the form instance.
  • The initForm method allows the component to initialize the service with its specific form instance.
  • I've removed the form parameter from all the other methods and instead used the local reference this.form.

Providing the service at the component level

Here's how to provide the service in the UserProfileComponent:

Remember to remove the this.form argument from all the service methods within the component.

Benefits and considerations

By creating a separate service instance for each component, we achieve the following:

  • reduced overhead - avoid passing the form object to every method call.
  • clearer intent - the component interaction with the service is more explicit, as it initializes the service with the form upon creation.
  • isolated forms - each component manages its own form independently, preventing conflicts between different forms.

There's a tradeoff

Having multiple instances of a service can consume more memory, especially if we have many components with forms.

Balancing flexibility and performance

While this approach offers isolation and convenience, we should consider the potential memory impact in larger applications with numerous forms.

The best approach will depend on the specific requirements and scale of our application. In many cases, the benefits of isolation and cleaner code outweigh the minor performance overhead of multiple service instances.

Wrapping up

In this article, I've focused on improving the UserProfileComponent by moving its form related logic to a dedicated FormHandlerService. We've accomplished the following:

  • recognized that the component was violating the Single Responsibility Principle by handling too many unrelated concerns.
  • extracted form related methods like updateForm, setFormErrors, isFormPristine, and button state logic into this new service.
  • used TypeScript generics to make the service reusable with different form types and data structures.
  • provided the service at the component level to ensure each form has its own isolated instance.

By separating the form handling logic into a dedicated service, we've made our UserProfileComponent cleaner, more focused, and easier to maintain.

Where to find the code

The complete code for this refactoring can be found in the 18.form-service branch of the GitHub repository.

Feel free to explore, experiment, and adapt this approach to your own Angular forms. This is just another step in the ongoing journey of refactoring and improving our application architecture.


If you have any questions, suggestions, or experiences you'd like to share, please leave a comment below! Let's continue learning and building better Angular applications together.

Thanks for reading!

Top comments (0)