How to Extend the FormControl Functionality in Angular

Written by amirankou | Published 2024/01/29
Tech Story Tags: angular | forms | reactive-forms | template-driven-forms | custom-form-control | form-field-dependencies | formcontrol-functionality | extend-formcontrol

TLDRThis article reveals the way to create a custom form control based on FormControl to extend its features and bring more flexibility to the development.via the TL;DR App

Form manipulation is a typical task for a Front-End engineer. They are used everywhere - from simple feedback forms to a complex ones for creating something big. While you are using any of the UI frameworks or libraries, they offer us a convenient way to create forms of any complexity easily and quickly.

With Angular, we have two options for form building: template-driven and reactive forms. Both of them can be applied in different scenarios. For example, if we need to create some simple form with primitive (or even without) validation, we can consider a template-driven form as a good candidate. Another case is when we need to build something more complex than a login form, with tons of validation, field dependencies from another field, etc. In this way, reactive forms come on the stage. But even with this powerful tool, we sometimes need to build some functionality that is not available from the box. In this article, I will show one of the possible cases, and you can apply this approach to your project.

Let's cut to the chase and move to the problem: we need to update a form field based on a value from another form field. Moreover, to enhance the application experience and not confuse a user, we want to highlight (put some message and color the field) the updated field. The fields can be any kind of element (input, dropdown, date picker, etc.), and any combination of field values can lead to the updation of another field. How can we solve this problem? There are some possible ways:

  1. We can write a directive for it. It will be used in any control you want despite your choice of form type. But it's quite a complicated task to support this directive in the future, and possibly, it will go for an awkward legacy code that nobody would like to extend or fix some issue related to this;
  2. Use EventEmitter. We can observe the expected condition for updating a target field and emit a value. When we do it - update the target field with the emitted value (or you can use this like a trigger and set any value you want). This option is better than the previous one, but it still requires a lot of manual work, and it won't be easy to reuse it somewhere - we will have to implement this mechanism for each case by hand;
  3. Extend the FormControl class. In functionality, which is available from the box, we won't find any methods or fields (or even a combination of them) to check that the form control was updated by another one. Let's create it!

First things first, we need to create a class for our new control, which extends the FormControl class:

import { FormControl } from '@angular/forms';

export class AutoUpdateFormControl<TValue> extends FormControl {
  constructor() {
    super();
  }
}

Then, to add a new functionality, we need to write our method for the control updating and mark the inner state as auto-updated:

import { FormControl } from '@angular/forms';

export class AutoUpdateFormControl<TValue> extends FormControl {
    public isAutoUpdated: boolean = false;

    constructor() {
        super();
    }

    public autoUpdateValue(value: any, options?: { emitEvent: boolean }): void {
        super.setValue(value, options);
        this.markAsAutoUpdated();
    }

    public setValue(
        value: TValue,
        options?: {
            onlySelf?: boolean;
            emitEvent?: boolean;
            emitModelToViewChange?: boolean;
            emitViewToModelChange?: boolean;
        },
    ): void {
        super.setValue(value, options);
        this.markAsNotAutoUpdated();
    }

    public markAsAutoUpdated(): void {
        this.isAutoUpdated = true;
    }

    public markAsNotAutoUpdated(): void {
        this.isAutoUpdated = false;
    }
}

An example of usage:

  1. Create a form:
interface IForm {
  sourceField: AbstractControl<string>;
  targetField: AutoUpdateFormControl<string>;
}

const form = new FormGroup<IForm>({
    sourceField: new FormControl(''),
    targetField: new AutoUpdateFormControl(),
});

  1. Subscribe to the sourceField control and check the required value. If matches - call the autoUpdateValue method. It sets the control value and updates the control field:
form.controls.sourceField.valueChanges.subscribe((value) => {
    if (value === 'test') {
        this.form.controls.testField.autoUpdateValue('updated field');
    }
});

  1. We can add mark-up for handling field highlighting and showing the text depending on the control field:
<span *ngIf="form.controls.targetField.isAutoUpdated">Auto-updated</span>

If you use some custom components or the wrappers around the third-party ones, you can directly add this kind of template into the component to not repeat it again and again.

I found this solution perfect for my needs and decided to share it. Perhaps, it will help someone. But if you want to go further, remember:

  • Your custom form control should remain compatible with Angular's forms API. Avoid altering the existing behavior of FormControl unless necessary;
  • Test your custom form control thoroughly, especially if you're also overriding any of the existing methods;
  • Document your custom methods and any changes to standard behavior to help other developers in your team understand your custom control.

With these bits of advice, you can create your custom controls safely and with pleasure.


Written by amirankou | Passionate about Front-End development
Published by HackerNoon on 2024/01/29