DEV Community

Cover image for A case for extending Angular Forms - part 4
DigitalCrafting
DigitalCrafting

Posted on • Updated on

A case for extending Angular Forms - part 4

Intro

Previously we added visibility to AbstractControl, but, we are missing one crucial part - treating hidden control as VALID. After that we will add another missing functionality: defaultValue and clear() method.

As always, full code examples are on my github.

What I also forgotten before is that in order for our application to apply all this changes, we must import our file in app.component file as such:

import "../../../core/forms/dc-forms";
Enter fullscreen mode Exit fullscreen mode

Handling hidden control

As it turns out, this part is the easiest one by far. We simply need to if statement in updateValueAndValidity method:

AbstractControl.prototype.updateValueAndValidity = function (opts: { onlySelf?: boolean, emitEvent?: boolean } = {}): void {
    ...
    if (this.visible === false) {
        (this as {status: string}).status = 'VALID';
    } else if (this.enabled) {
        ...
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Note that we add the if (this.enabled) as an else to our if because there is no need to run any validators on a hidden control.

Default value

When we first create FormControl we can pass options to contructor, either as a initial value or value and other parameters. So why isn't this value treated as default when we reset the control? This only makes sense in the FormControl so we will not do it FormGroup and FormArray (but of course you can).

First, we declare another interface in our augmented module:

declare module "@angular/forms" {
    ...
    interface FormControl extends AbstractControl {
        readonly defaultValue: any;
    }
}
Enter fullscreen mode Exit fullscreen mode

And then, override the _applyValue method, adding another line just below the visibility handling from previous article:

(this as {defaultValue}).defaultValue = formState.value;
Enter fullscreen mode Exit fullscreen mode

And that's it for storing the defaultValue. Now we can enhance reset() and clear() methods.

clear() and reset()

For compatibility, clear and reset will be declared both in AbstractControl and in FormControl interfaces:

interface AbstractControl {
    ...
    clear(value?: any, options?: Object);
    // @ts-ignore
    reset(value?: any, options?: Object);
}

interface FormControl extends AbstractControl {
    readonly defaultValue: any;
    clear(options?: Object);
    // @ts-ignore
    reset(options?: Object);
}
Enter fullscreen mode Exit fullscreen mode

The reason for different signature is that, as mentioned before, defaultValue is mainly useful in FormControl. For compatibility's sake the AbstractControl.clear() method will be declared as follows:

AbstractControl.prototype.clear = function (value?: any, options?: Object) {
    this.reset(value, options);
}
Enter fullscreen mode Exit fullscreen mode

Now, for the actual functionality, we implement the FormControl methods:

FormControl.prototype._applyValue = function(value: any, options?: Object) {
    this._applyFormState(value);
    this.markAsPristine(options);
    this.markAsUntouched(options);
    this.setValue(this.value, options);
    this._pendingChange = false;
};

FormControl.prototype.reset = function (options?: Object) {
    this._applyValue(this.defaultValue, options);
};

FormControl.prototype.clear = function (options?: Object) {
    this._applyValue(null, options);
};
Enter fullscreen mode Exit fullscreen mode

The _applyValue() method is basically what reset() looked liked before, so in terms of Vanilla Angular:

  • our new reset() method sets the FormControl value to defaultValue
  • our clear() method sets the FormControl value to null

What else ?

Now we can enhance our visibility functionality by:

  • calling clear() when we hide our control
  • calling reset() when we show our control
declare module "@angular/forms" {
    interface FormGroup extends AbstractControl {
        reset(value: any, options: {onlySelf?: boolean, emitEvent?: boolean}): void;
    }
}
...
AbstractControl.prototype.hide = function () {
    if (this.visible) {
        (this as { visible: boolean }).visible = false;
        this.visibilityChanges.emit(this.visible);
        this.clear();
        this.updateValueAndValidity();
    }
};
AbstractControl.prototype.show = function () {
    if (!this.visible) {
        (this as { visible: boolean }).visible = true;
        this.visibilityChanges.emit(this.visible);
        this.reset();
        this.updateValueAndValidity();
    }
};
Enter fullscreen mode Exit fullscreen mode

One other thing we can do is to override the FormGroup.reset() method to account for different signature of reset() and clear():

FormGroup.prototype.reset = function (value: any = {}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
    this._forEachChild((control: AbstractControl, name: string) => {
        if ('defaultValue' in control) {
            (control as FormControl).reset(options);
        } else {
            control.reset(value[name], {onlySelf: true, emitEvent: options.emitEvent});
        }
    });
    ...
};
Enter fullscreen mode Exit fullscreen mode

Summary

Doesn't this look more natural in terms of usability ? We clear control when hiding it, and restore default when we show it again. Also, the hidden control is treated as VALID, hence not iterfering in the FormGroup as a whole.

In the next article, we will tackle the last shortcoming of Angular Forms: lack of differentiation between user and programmer input.

Hope you found this article usefull :)

Top comments (0)