Enhancing Angular Reactive Forms with a Custom Image Uploader
Tags: angular, tutorial, fileupload, refactoring
Published: 2024-06-10 17:14:18

## Introduction

In this article, I'll guide you through building a special component that makes **uploading images** in forms much easier. This component won't just make the form templates look cleaner, it'll also work seamlessly with the existing reactive forms setup, letting you treat image uploads just like any other form field.

Throughout this article, I'll walk you through the following steps:
- **Creating the component**: Generate the new component and migrate the existing code.
- **Displaying the saved image**: Implement the logic to display the currently saved image.
- **Selecting a new image**: Add functionality to allow users to choose and preview a new image before uploading it.
- **Form integration**: Integrate the custom component with Angular reactive forms system.
- **Removing hardcoded paths**: Refactor the code to avoid hardcoding image URLs and instead fetch them from the server.
- **Custom upload button**: Create a custom upload button for a consistent look across browsers.

_**A quick note**: Before we begin, I'd like to remind you that this article builds on concepts and code introduced in previous articles of this series. If you're new here, I highly recommend that you check out those articles first to get up to speed. You can find the starting point for the code I'll be working with in the `13.validation-error-directive` branch of the https://github.com/cezar-plescan/user-profile-editor/tree/13.validation-error-directive repository._

## Identifying the current issues

Let's take a look at our form in the `user-profile.component.ts` file, which includes controls for name, email, address and avatar:{% embed https://gist.github.com/cezar-plescan/fae1e74f125eb1977d4dea190b642fed %}When I examine the template in `user-profile.component.html` I can easily identify the `input` elements associated with the first 3 controls, thanks to the `formControlName` directive provided by Angular's Reactive Forms Module.{% embed https://gist.github.com/cezar-plescan/f8295980e88b84f78a517949356d7eff %}These `input` elements provide both the display and modification of the form control value. However, when it comes to images, my current approach separates these functionalities.{% embed https://gist.github.com/cezar-plescan/628de75a97870355c0b0f407eebf1bc3 %}I rely on an `<img>` tag to display the existing image, but for uploading a new image, I use a separate `<input type="file"/>` element. This fragmented approach makes the code less maintainable and creates an unintuitive user experience. From my point of view, users should have a clear and consistent way to interact with an image in a form, whether they're viewing the current image or selecting a new one for upload.

To address this, I'll create a dedicated Angular component to manage both image display and upload. I want this component to be able to receive the `formControlName` directive, just like any other form control.

But why do I create a component instead of a directive, like I did for the validation errors? While directives are excellent for manipulating existing elements, a component provides both a view (for displaying the image and selecting a new one for uploading) and its associated logic.

By extracting this functionality into a reusable component, we'll achieve some advantages:
- **Simplified templates**: our form templates become cleaner and more focused on the overall structure.
- **Single Responsibility Principle**: the component adheres to SRP, as it encapsulates all the logic and behavior related to image handling, keeping the parent component concerns separate.
- **Reusability**: we can easily use this component in any form throughout our application. 

Let's explore the steps involved in creating this dedicated image form control component.

## Implementing the new component

### Create the new component files

1. Generate the component: I begin by generating a component named **`image-form-control`** within the `src/app/shared/components` folder, using the Angular CLI: `ng generate component image-form-control`.

2. Migrate Existing Code: Next, I'll transfer the relevant code from the form template `user-profile.component.html` into `image-form-control.component.html`. Similarly, I'll move the associated methods (`getAvatarFullUrl` and `onImageSelected`) from the `user-profile.component.ts` component into `image-form-control.component.ts`. Remember to remove these methods from the `UserProfileComponent` class.{% embed https://gist.github.com/cezar-plescan/0fd0ae9addfc7f196960339042a348f4 %}{% embed https://gist.github.com/cezar-plescan/9c6f6db642b5670665f36280036eaa7b %}

3. Integrate the component: Now, I'll update the form template in `user-profile.component.html` to use the `ImageFormControlComponent`:{% embed https://gist.github.com/cezar-plescan/f18fe573fd43460e7997a2d0209ff3a7 %}

At this stage the application is broken. We still have some work to do to make the new component fully functional. 

The first visible error is that the `form` property doesn't exist in the `ImageFormControlComponent`. Additionally, we need to instruct the component to interact with the reactive form. In the following sections I'll describe how to resolve these challenges and make the component a fully functional form control.

I'll break down the implementation into two main parts:
- displaying the saved image
- selecting a new image to upload

_**Note**: This new component is essentially a **custom form control**. If you're new to this topic, I highly recommend you to refer to this [comprehensive guide](https://blog.angular-university.io/angular-custom-form-controls/) from the Angular University blog. My implementation will simply follow the steps from this guide._

### Displaying the image

Let's take a closer look at how our component is used in the form template:{% embed https://gist.github.com/cezar-plescan/f18fe573fd43460e7997a2d0209ff3a7 %}We notice that it only has one input property, `formControlName="avatar"`, without any direct reference to the form or the avatar form control itself. Stay with me to explore how to connect our component to the Reactive Forms setup.

For now, I'll just use the `<img>` tag in our template:{% embed https://gist.github.com/cezar-plescan/f623f614a7fbf63a0a3df3d04518f5ff %}Now I need to define how the `imgSrc` property is set in `ImageFormControlComponent` class. Following the [Angular custom form controls guide](https://blog.angular-university.io/angular-custom-form-controls/), I need to perform several steps:
1. declare the `imgSrc` property in the component class
2. the component class should implement the `ControlValueAccessor` interface; this helps to communicate with the reactive forms system
3. implement the `writeValue` method (I'll ignore the other methods for now); this method will be called by Angular to set the initial image source when the form loads
4. register the component in the dependency Injection system; this tells Angular that this component should act like a form control{% embed https://gist.github.com/cezar-plescan/261fd279afdecf8582a592700dae01af %}Now, our component can read the avatar value from the form and display the image.

**Note**: The `writeValue` method contains a hardcoded string for the images path. This is not ideal and I'll address it in a later section.

To understand how our custom form control integrates with Angular reactive forms, let's dive a bit deeper into the internals.

#### NG_VALUE_ACCESSOR injection token

Let's see why we need this token and how it is used internally. I'll jump straight into the `formControlName` directive [source code](https://github.com/angular/angular/blob/17.3.3/packages/forms/src/directives/reactive_directives/form_control_name.ts#L133). The relevant line is in the constructor definition:
`@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]`

What does this actually mean?

**`NG_VALUE_ACCESSOR`** is an injection token provided by Angular. Its purpose is to act as a lookup key for finding the appropriate `ControlValueAccessor` implementation for a given form control. In the component `providers` array, we're essentially saying, "Hey Angular, when you encounter a form control that needs a `ControlValueAccessor`, use my `ImageFormControlComponent` as the implementation." When Angular processes the `formControlName` directive, it looks for a `NG_VALUE_ACCESSOR` provider in the injector hierarchy. Since we've registered our component as the provider, Angular knows how to use the component methods (writeValue, registerOnChange, etc.) to interact with the form control.

The `formControlName` directive, when applied to an element (in our case, the `app-image-form-control` component), uses Angular dependency injection system to look for providers of the `NG_VALUE_ACCESSOR` token on that specific element itself. It's not looking for a global provider or one in a parent component. This is indicated by the `@Self()` decorator on the injection site in the directive's constructor.

When the `formControlName` directive is applied to our component, it finds this provider and uses it to get an instance of the `ControlValueAccessor`. Since we've provided our component itself, the directive gets a direct reference to it.

#### Executing the `writeValue` method

Here is a simplified process of how the method is called internally:
1. Form Control Setup: When we create a `FormControl` in our component (either directly or through `FormBuilder`) and bind it to your custom form control using `formControlName` in the template, Angular establishes a connection between them.
2. Initial Value Setting: If we've provided an initial value to the `FormControl` (e.g., through the `value` property or `setValue` method), Angular will call the `writeValue` method on our custom form control (the `ImageFormControlComponent`) to set that initial value.
3. Value Changes: Whenever the value of the `FormControl` changes (e.g., through user input, `setValue`, or `patchValue`), Angular will again call `writeValue` on our component to update it with the new value. This ensures that the UI element stays in sync with the form control's value.

### Selecting an image to upload

The current template contains only the `<img>` tag. For selecting an image file to upload I have to add the `<input type="file"/>` element back into the template:{% embed https://gist.github.com/cezar-plescan/7fb0bfc28907e6707958c525a6070aea %}I've also included the handler `onImageSelected` for the `change` event, that will be triggered when the user selects a file. This is necessary to display the selected image in place of the original one, before uploading it to the server. Here is its implementation:{% embed https://gist.github.com/cezar-plescan/2191632bcf5fcaaa2518fdbfa5df8aa9 %}I want to upload the image to the server only when the user explicitly submits the form. Why do I do this? Because the user could change their mind after selecting an image and I don't want to immediately send the image to the server, but only when the user is ready to save the form.

#### Serving files locally

The selected image file doesn't exist on the server yet and you might wonder where it is served from. The answer lies in the `URL.createObjectURL(file)` method of the browser's API. This method takes a `File` object (or a `Blob`) and generates a URL string. This URL doesn't point to a physical file on our server; instead, it references the file data directly in the browser's memory.

The generated URL is a special type called a blob URL (e.g., `blob:http://localhost:4200/d9856eeb-2405-4388-8894-064e56c254a8`). We can use this URL as the `src` attribute of an `<img>` tag to display the selected image in the browser without needing to upload it to a server first. You can verify this by inspecting the <img> element in DevTools after selecting an image file; you'll notice the `src` attribute value has a format similar to the example above.

#### Revoking blob URLs

Blob URLs are temporary. They only exist as long as the document in which they were created remains open. Once the document is closed or navigated away from, the blob URL is automatically revoked by the browser. It's a good practice to release blob URLs when they are no longer needed using `URL.revokeObjectURL()`. This helps avoid potential memory leaks, especially if you're dealing with large image files. Let's see how to incorporate this method in our component:{% embed https://gist.github.com/cezar-plescan/7c217c43c1bc8e56e5db74f2d3205f31 %}To verify that the blob URLs are indeed being discarded, follow these steps:
- select an image file
- go to DevTools and open the image source URL in a new tab
- select another image from the form
- go to the previously opened tab and refresh it
- you should no longer be able to view the previous image, confirming that the blob URL has been revoked

#### Notify the form about the new image selection

At this stage, you might have noticed that the Save and Reset buttons don't become active after selecting a new image. This isn't what we expect. When typing into the text input fields, the buttons become active, and we expect the same behavior when selecting a new image. This is because the form doesn't yet know that we've changed the image. 

Let's see how to address this. The reactive forms system provides a way to notify the form control that its value has changed by calling a function that Angular registers with our custom form control. The [Angular custom form controls guide](https://blog.angular-university.io/angular-custom-form-controls/) provides a detailed explanation of this process. 

Here is the updated `image-form-control.component.ts` file:{% embed https://gist.github.com/cezar-plescan/883aae40f4dbf8a01262a665169d745f %}I've also added a template reference variable for the `<input>` element, which is used in the component as a `ViewChild`:{% embed https://gist.github.com/cezar-plescan/ae83b1b7d5530cb8668c3b919c8a6e78 %}In the `user-profile.component.ts` file I've removed all references to `fileInput` property, as it's now managed entirely by the `ImageFormControlComponent`; two methods were affected:{% embed https://gist.github.com/cezar-plescan/09648e8210f43f98231e8bb08b0a0c86 %}

Now I'll explain the key changes step by step:

##### `registerOnChange` method

Angular calls the `registerOnChange` method, part of the `ControlValueAccessor` interface, internally. The only action required here is to store a reference to the internal function that will be used to notify the form control when its value changes:{% embed https://gist.github.com/cezar-plescan/08f4632817db8027af1b134b9da369b5 %}Now, we need to call this stored function whenever the user selects a new image. In the `onImageSelected` method I've added `this.onChange?.(file)` which will internally notify the form control about the change. This results in the form buttons becoming enabled after selecting an image.

##### Clear the image filename after upload

There's one refinement to make here. Currently, after the form is submitted with a new image, the filename remains displayed in the file input element. To fix this, we need to clear the file input value after the form is submitted and the image is uploaded. To achieve this, we have to modify the `writeValue` method to also clear the file input. 

First, we need access to the file input element in the template. I've attached a template reference variable to the element `<input **#fileInput** ... />`. Then, to access it in the component I've used the `@ViewChild` decorator `@ViewChild('fileInput', {static: true}) protected fileInput!: ElementRef<HTMLInputElement>`.

Finally, to clear the filename, I've simply set its value to an empty string within the `writeValue` method: `this.fileInput.nativeElement.value = ''`.

With these changes, the image upload component is now integrated with the reactive form.

## Improvements

In this section I'll explore some enhancements we can make to our image form control component, taking its functionality to a new level.

### Custom upload button

The current implementation of selecting an image to upload is rendered differently across browsers. There's no way of consistently style a plain input of type file. A solution is to hide the default input element and create a custom button that triggers it behind the scenes.

Here is the updated template:{% embed https://gist.github.com/cezar-plescan/4d58915892e796e2aa31d82a6542b001 %}I've added some styling to the component elements:{% embed https://gist.github.com/cezar-plescan/98bb011bd94febe0f0cb7965286caf89 %}Don't forget to import the necessary Angular Material modules `MatButtonModule` and `MatIconModule` into the component.

The trick is that the custom button delegates the click event to the hidden file input. With this approach we can fully customize the appearance of the upload button. If necessary, the filename of the selected image can be extracted and displayed, using additional logic in the template and the component, but I've omitted that for simplicity.

### Remove hardcoded path for image URLs

In the previous implementation, I've hardcoded the path for image URLs within the component. However, it's not ideal to have the client-side responsible for constructing URLs. Ideally, the server should provide the complete URL for each image.

There are several advantages of this approach:
- **Environment Flexibility**: By dynamically generating URLs, our application becomes more adaptable to different environments (development, staging, production) without requiring manual changes to hardcoded paths.
- **Improved Maintainability**: Centralizing URL generation on the server makes it easier to manage and update image paths if needed.

To address this, I've updated the `server.ts` file. When the server sends a response containing the user data, it will include the full URL for the avatar image, while keeping only the filename stored in the `db.json` file:{% embed https://gist.github.com/cezar-plescan/fedb25330cc9340706eaa116c0c43861 %}With this change, the `writeValue` method in the component becomes simpler:{% embed https://gist.github.com/cezar-plescan/c26f580ebf32af30258701f12184e4a7 %}Remember to restart the server to see these changes.

## Additional Resources

Here are some additional resources that will help you dive deeper into the concepts covered in this article.

#### Custom form control

- [Angular Custom Form Controls - Complete Guide](https://blog.angular-university.io/angular-custom-form-controls/): A comprehensive guide to creating custom form controls in Angular, covering the ControlValueAccessor interface, validation, and more.
- [Never Again Be Confused When Implementing ControlValueAccessor in Angular Forms](https://angularindepth.com/posts/1055/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms): An in-depth article explaining the intricacies of the ControlValueAccessor interface and how to use it effectively.
- [Custom Form Controls in Angular](https://medium.com/@oluwaetosin/custom-form-controls-in-angular-d5a63d1a1d60): A practical guide with examples on building various types of custom form controls.

#### File uploading

- [Angular file upload](https://blog.angular-university.io/angular-file-upload/): A detailed tutorial on file uploads in Angular, including how to create custom upload buttons.
- [How to upload files in HTML?](https://imagekit.io/blog/how-to-upload-files-in-html/): A comprehensive guide to file uploads in HTML, covering the basics of file input elements, JavaScript interactions, and best practices.
- [A comprehensive overview of the File API and its capabilities for file selection and reading](https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications)

#### Custom upload button

- [How to create a custom file upload button using HTML, CSS, and JavaScript](https://dev.to/faddalibrahim/how-to-create-a-custom-file-upload-button-using-html-css-and-javascript-1c03): A step-by-step guide with code examples on building a custom upload button.
- [Styling an input type="file" button](https://stackoverflow.com/questions/572768/styling-an-input-type-file-button): A Stack Overflow discussion on different ways to style the file input button.

## Conclusion: Building powerful forms with custom controls

In this article, we've taken a significant step towards mastering Angular forms by creating a reusable `ImageFormControlComponent`. We've seen how to:
- Transform a basic image upload element into a full-fledged form control.
- Integrate it seamlessly with Angular reactive forms system.
- Handle image display, selection, and preview.
- Ensure efficient memory management with blob URLs.
- Address common challenges like cross-browser styling inconsistencies.

This custom component not only improves our code but also enhances the user experience by providing a clear and consistent way to manage image uploads within your forms.

I encourage you to experiment with the code from this article and explore the possibilities for further enhancements. You can find the code for this project at: https://github.com/cezar-plescan/user-profile-editor/tree/14.image-form-control.

If you have any questions, suggestions, or experiences you'd like to share, please leave a comment below! Let's continue the conversation and learn together.