The Truth About Custom Reactive Form Elements

When I first learned about reactive components in (then) Angular 2, I instantly fell in love. I never liked the idea of splitting up form logic between markup and controller (validators in markup, but not all of them, submitting logic in controller, but not all of it…) so being able to have a clear separation between markup and logic was a revolutionary thing.
And if you stay on the surface and have just a few simple forms with inputs here and maybe a textarea there, they definitely work great. Even attaching custom validators and using form groups and arrays still feels very nice and uncomplicated. But eventually, you might require something more complex than that, or you want to group some form elements together and make them reusable. So, why not make your own custom reactive element? Surely it can’t be that hard.

Well, in my experience that’s not entirely true. There’s a lot of misinformation out there about how you’re actually supposed to implement the required interfaces and not a lot of clear-cut snippets that show a minimal working example you could copy and learn from. As I’ve worked with them for a while now, I thought it would be a good idea to compile all of my experiences into a sort of reference on how custom reactive elements can be implemented. Here we go!

Reactive forms, how do they work?

First, a general overview of how reactive form components work. There’s basically 3 “layers” to reactive forms:

Whenever a reactive form gets built, Angular looks for the CVA first, and then tries to connect to the view and the model. The channels through which communication happens between view and model are set up as follows:

View to model updates

A change listener is registered for the actual DOM element. Whenever the value changes, onChange(value) is called by the CVA, which is a function it receives from the framework through registerOnChange(fn). Here you can see what function is passed when register is called. Angular then updates its model with the value passed for onChange. This is how the default value accessor does it.

Model to view updates

Whenever the model value changes, Angular calls writeValue(value) on the CVA, where value is the new form value that needs to be propagated to the view. A model change can happen either when the form element is created or whenever setValue or patchValue is called on it. This is where writeValue is called for the latter two.
The same goes for the disabled state of the form element: Angular notifies you when the user programatically disables the form field by calling setDisabledState() on your CVA. Since the model doesn’t have any notion of being disabled, this only happens in one direction.

Additionally, the touched state is propagated to the model when a blur event happens in the view - as you might have guessed, this happens similarly to view -> model updates; with the CVA listening to the event and calling onTouched() when it is fired by the view. (And by the way, onTouched has nothing to do with touch devices, which is what I read in another tutorial… Yeah, this is what I meant when I said there’s a lot of misinformation out there.)

NOTE: Sadly, the opposite direction for touched state propagation is missing. This means that when someone calls markAsTouched() or something similar on the form, there is no way for the CVA to know about this and update the form accordingly. In my opinion, this is a huge oversight and the only workaround I’ve found for this is to monkeypatch and hook the method on the control that owns the CVA, which is less that ideal. I’ll probably write an article on how I solved this for our use case and link to it here.

To visualize the whole thing, this is what the data flow for a reactive form element looks like:

Control Value Accessor Schema

Let’s make it custom

Armed with this knowledge, implementing a custom reactive form element is rather straightforward. We have 2 tasks here:

  1. Implement the view with the actual form element
  2. Implement the CVA that will propagate value changes from the model to our view and notify the model when its own value is changed

As an example, let’s build a simple custom form element that will allow a user to upload a profile picture by dropping it and then holds the URL of the uploaded image as a value. I’ll start with the stuff that’s non-specific to the CVA. I won’t go into too much detail on this since that’s not the focus of this post, and I’ll assume there’s an API for uploading Files and getting the URL of the uploaded File in place already.
We’re using the host listener annotation to listen to drag and drop-events on our custom element:

@Component({
    selector: 'profile-picture-uploader',
    template: `
        <img [src]="uploadedImageURL" *ngIf="uploadedImageURL">
        <span (click)="reset()" *ngIf="uploadedImageURL">
            reset
        </span>
    `,
    styles: [`
        :host {
            display: block;
            width: 150px;
            height: 150px;
            position: relative;
            overflow: hidden;
            text-align: center;
        }

        img {
            object-fit: cover;
        }

        span {
            position: absolute;
            z-index: 1;
            bottom: 10px;
        }
    `],
})
export class ProfilePictureUploaderComponent {
    // This is where we'll store the internal value
    // of the form element.
    uploadedImageUrl: string | null = null

    constructor(
        private fileUploadService: FileUploadService,
    ) {}

    reset(): void {
        this.uploadedImageUrl = null
    }

    @HostListener('dragover', ['$event'])
    @HostListener('dragenter', ['$event'])
    onDragOver(event: Event): void {
        // We have to prevent dragover events to be
        // able to intercept the drop event
        event.preventDefault()
        event.stopPropagation()
    }

    @HostListener('drop', ['$event'])
    onDrop(event: DragEvent): void {
        event.preventDefault()

        // Get the file from the transfer list, abort if
        // there is none
        const fileToUpload = event.dataTransfer.files[0]
        if (!fileToUpload) return

        // Upload using our imaginary service
        this.fileUploadService.upload(fileToUpload).subscribe(
            value => this.uploadedImageUrl = value,
        )
    }
}

Cool, we’re all set up. Now we can get to the juicy stuff!


Implementing the Control Value Accessor

First, we need to provide the register functions:

onChange = (value: string) => {}
onTouched = () => {}

// ...

registerOnChange(fn: any): void { this.onChange = fn }
registerOnTouched(fn: any): void { this.onTouched = fn }

The type of onChange here is (value: string) => void because the value our form will return is a string. You would need to replace this if your form element emits some other type of value.

Next is the writeValue and setDisabledState:

disabled = false

// ...

writeValue(value: string | null): void {
    this.uploadedImageURL = value
}

setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled
}

// We also modify our onDrop function to ignore
// dropped files when the form is disabled
@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
    if (this.disabled) return

    // ...
}

Now, the last thing is to notify Angular whenever we get a new value or whenever the form was touched in general:

reset(): void {
    this.uploadedImageURL = null
    this.onChange(null) // <--
    this.onTouched()    // <--
}

@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
    // ...

    this.fileUploadService.upload(fileToUpload).subscribe(
        value => {
            this.uploadedImageURL = value

            this.onChange(this.uploadedImageURL) // <--
            this.onTouched()                     // <--
        },
    )
}

And that’s it. We can now use the custom form element in any reactive form group or by itself, just by attaching the formControlName or formControl directives.


Conclusion

Custom Control Value Accessors can be a bit daunting at first, but if you know the mechanisms behind Angular’s reactive forms, a lot of the magic is dispelled and they can be a great tool to encapsulate form behaviour. They definitely shouldn’t be used for everything and everywhere, but in the right spot they are very powerful. Hit me up if I got anything wrong or if you have any questions!

Comments

comments powered by Disqus