Angular Form Architecture: Achieving Separation of Concerns with a FormHandlerService
Tags: angular, tutorial, forms, service
Published: 2024-06-28 13:28:45

## 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](https://github.com/cezar-plescan/user-profile-editor/tree/17.error-interceptor)_.

## Dealing with the form logic in the component

As I examine the [user-profile.component.ts](https://github.com/cezar-plescan/user-profile-editor/blob/17.error-interceptor/src/app/user-profile/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.{% embed https://gist.github.com/cezar-plescan/25f89223023b9e86a513fe04af3dcd5b %}

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. {% embed https://gist.github.com/cezar-plescan/6e94cb5839ebb9841ea2c0e5f0ac5ebe %}

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: {% embed https://gist.github.com/cezar-plescan/94d2f7c49521243e9cbd1ce0f3edd8ba %}

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: {% embed https://gist.github.com/cezar-plescan/592bd92e76cf764c5cc6142789f17c0e %}

Additionally, I'll create a separate file `src/app/shared/types/form.type.ts` containing the definition of the `FormModel` interface: {% embed https://gist.github.com/cezar-plescan/02c0fdcf439c7c51a5ca9882832059e1 %}

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:{% embed https://gist.github.com/cezar-plescan/38c33d2be6607f88731b4eb2a87dca7b %}

### 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: {% embed https://gist.github.com/cezar-plescan/0d8d57c70e75ac153cf3c1e34e57ff5d %}

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:{% embed https://gist.github.com/cezar-plescan/5872673e093b310dda2c41e48184ce24 %}

### 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:{% embed https://gist.github.com/cezar-plescan/0475f5c0387569a4b1e9152717dbe689 %}

And now update the `UserProfileComponent`, by simply delegating the calls to these new service methods:{% embed https://gist.github.com/cezar-plescan/6fd5753dc796025ca9c55589627d18bf %}

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:{% embed https://gist.github.com/cezar-plescan/b508247d97d4735e53d7d769b484329d %}{% embed https://gist.github.com/cezar-plescan/a3330389eb1e745e552bc6cf32abb30b %}

## 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:{% embed https://gist.github.com/cezar-plescan/15ed68892c7324f59d2120e42bad3f51 %}

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`:{% embed https://gist.github.com/cezar-plescan/034d332b3093ff4b1514e1d8432ddbef %}

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](https://github.com/cezar-plescan/user-profile-editor/tree/18.form-service).

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!_