DEV Community

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

Posted on • Updated on

A case for extending Angular Forms - part 3

Intro

After deciding what approach to use in my previous article it's finally time to code. In this article I'll go through necessary changes to Angular AbstractControl in order to add visibility functionality.

The todos

There are a couple of things we need to do:

  1. Add the visible state
  2. Add the onChangeEventEmitter
  3. Add methods to control the state
  4. Enable adding visible as a constructor parameter
  5. Remove the validity check when the control is invisible

In order to achieve all of these, we will be altering some of the internal methods of the Angular framework. The best way to do this is to copy the code form their Github repo and apply the changes.

The fifth point form our todo list is not going to be implemented in this article since it is heavily interfering with the internals of the framework and as such, deserves more attention.

The implementation

In order to be able to use prototype we will be using module augmentation:

declare module "@angular/forms" {
    interface AbstractControl {
        visibilityChanges: EventEmitter<boolean>;
        readonly visible: boolean;
        show();
        hide();

        /**
         * This methods is marked as internal inside the AbstractControl.
         * Declaring it here allows us to easily override it
         * */
        _isBoxedValue(formState: any): boolean
    }
}
Enter fullscreen mode Exit fullscreen mode

This is pretty straightforward. We 're-declare' the @angular/forms module and it's AbstractControl class (this must be declared as interface here, abstract class does not work) and then we declare new members of the class and private method we want to override.

Adding new functionality is also rather simple using the good old prototype:

(AbstractControl.prototype as { visible: boolean }).visible = true;
AbstractControl.prototype.visibilityChanges = new EventEmitter<boolean>();
AbstractControl.prototype.hide = function () {
    if (this.visible) {
        (this as { visible: boolean }).visible = false;
        this.visibilityChanges.emit(this.visible);
        this.reset();
        this.updateValueAndValidity();
    }
};
AbstractControl.prototype.show = function () {
    if (!this.visible) {
        (this as { visible: boolean }).visible = true;
        this.visibilityChanges.emit(this.visible);
    }
};
Enter fullscreen mode Exit fullscreen mode

There are 2 things I want to point out:

  1. Casting control as { visible: boolean} - this one is from Angular framework itself, it allows us to modify the readonly value inside our class, while keeping it non-modifiable outside of it.
  2. Additional calls in the hide() - when hiding the control, we have to remember to clear it's content and update validity. Right now we do not have the full functionality to do this correctly - it will come in the following articles - so these two methods have to suffice.

So that's points 1-3 done, now let's take a look at number 4.

Angular allows us to pass either value or value and disable state into the FormControl constructor. Wouldn't it be nice to also be able to pass initial visibility ? In order to achieve this, we need to override two more methods:

  1. AbstractControl.prototype._isBoxedValue and
  2. FormControl.prototype._applyFormState

They check if the passed state is value or object, and assign the object values to form state respectively.

In the first one we just need to extend if statement a bit:

AbstractControl.prototype._isBoxedValue = function(formState: any): boolean {
    return typeof formState === 'object' && formState !== null &&
        Object.keys(formState).length >= 2 && 'value' in formState &&
        ('disabled' in formState || 'visible' in formState);
};
Enter fullscreen mode Exit fullscreen mode

But the second one is a bit more tricky. We need to remember that we do not require passing both disabled and visible params so if the visible was undefined it would be interpreted as a false in the if statement, hence hiding our control when we didn't want to. That's why we will specifically extend the second method to only allow true or false values and nothing else:

(FormControl.prototype as { _applyFormState: () => void })._applyFormState = function(formState: any) {
    if (this._isBoxedValue(formState)) {
        // ...
        if (formState.visible === true || formState.visible === false) {
            (this as {visible: any}).visible = formState.visible;
        }
    } else {
        // ...
    }
};
Enter fullscreen mode Exit fullscreen mode

Full code for extending the Angular Forms is available here.

Summary

That's all the functionality for this article, as mentioned before, handling the validity of hidden control will be the topic of the next one.

Full code along with the example is available in my Github repo.

Thank you for reading. Hope you enjoyed this article and will find it useful ! See you in the next article.

Top comments (0)