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:
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.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.<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 in the previous article? 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
Generate the component: I begin by generating a component named
image-form-control
within thesrc/app/shared/components
folder, using the Angular CLI:ng generate component image-form-control
.-
Migrate Existing Code: Next, I'll transfer the relevant code from the form template
user-profile.component.html
intoimage-form-control.component.html
. Similarly, I'll move the associated methods (getAvatarFullUrl
andonImageSelected
) from theuser-profile.component.ts
component intoimage-form-control.component.ts
. Remember to remove these methods from theUserProfileComponent
class. -
Integrate the component: Now, I'll update the form template in
user-profile.component.html
to use theImageFormControlComponent
:
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 this component to interact with the reactive form through the formControlName
directive. 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 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:
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 and suppose that the image source comes from an imgSrc
property:
imgSrc
property is set in the ImageFormControlComponent
class. Following the Angular custom form controls guide, I need to perform several steps:
- declare the
imgSrc
property in the component class - the component class should implement the
ControlValueAccessor
interface; this helps to communicate with the reactive forms system - implement the
writeValue
method of the interface (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 - register the component in the dependency Injection system; this tells Angular that this component should act like a form control
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. 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 the 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:
- Form Control Setup: When we create a
FormControl
in our component (either directly or throughFormBuilder
) and bind it to your custom form control usingformControlName
in the template, Angular establishes a connection between them. - Initial Value Setting: If we've provided an initial value to the
FormControl
(e.g., through thevalue
property orsetValue
method), Angular will call thewriteValue
method on our custom form control (theImageFormControlComponent
) to set that initial value. - Value Changes: Whenever the value of the
FormControl
changes (e.g., through user input,setValue
, orpatchValue
), Angular will again callwriteValue
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:
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: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 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:
- 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 provides a detailed explanation of this process.
Here is the updated image-form-control.component.ts
file:
<input>
element, which is used in the component as a ViewChild
: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: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:
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:
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:
writeValue
method in the component becomes simpler: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: 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: An in-depth article explaining the intricacies of the ControlValueAccessor interface and how to use it effectively.
- Custom Form Controls in Angular: A practical guide with examples on building various types of custom form controls.
File uploading
- Angular file upload: A detailed tutorial on file uploads in Angular, including how to create custom upload buttons.
- 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
Custom upload button
- How to create a custom file upload button using HTML, CSS, and JavaScript: A step-by-step guide with code examples on building a custom upload button.
- 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.
Top comments (0)