Advertisement

#3 Angular Form Group Interop

When we are finished with this article we will have gain access to the controls that are contained with our form group component's template. We will also have added the input contained within our controls to the angular form group that is contained within our form group component.

References for our inputs

  • WebUi
    • Source
      • forms
        • xc-form-group.component.ts

Our first order of business is a little cleanup by way of renaming the _tGroup field to _group (a). Next in order to do anything with the inputs that are contained within our group we need have access to them. To do this we will make use of angular's content children decorator (a).

xc-form-group.component.ts (1)

import { ..., ContentChildren, ..., QueryList } from "@angular/core";
...
import { ..., XcFormControl } from "./forms.core";
...
export class XcFormGroupComponent ... {
    ...
    @ContentChildren(XcFormControl) private readonly _formControls: QueryList<XcFormControl> = null;
    
    private readonly _group: FormGroup = null; // Renamed from _tGroup

    private get tGroup() { return this._group; } // Fixed reference

    constructor() {
        this._group = new FormGroup({}); // Fixed reference
    }
    ...
}
(a) Renaming of the _tGroup field to _group and gaining access to our form controls using the content children decorator.

Once we make these changes and save we will see that we have an error in the console (b). This is the result of a previous decision that caused a circular reference and we will fix it next.

Error generated by a circular reference.
(b) Error generated by a circular reference.

Those circular dependencies will get you every time

  • WebUi
    • Source
      • forms
        • controls
          • input
            • xc-input.component.ts
          • xc-form-control.ts
        • forms.core.ts
        • forms.index.ts
        • xc-form-group.component.ts

In my defense I was aware of the potential for this error when we first set things up but since we did not run into it then I figured that with the update to angular this is was somehow fixed. You know what they say about assuming things though. Our problem stems from deciding to export a reference to a few of our classes in the forms.core.ts file (c). The fix is pretty simple though. We will just remove these reference and update the rest of our forms accordingly. Once removed we will add an export for the form group in our index file (d).

forms.core.ts (1)


export * from "./xc-form-group.component"; // Remove this
export * from "./controls/xc-control"; // Remove this
export * from "./controls/xc-form-control"; // Remove this
...
(c) Remove the references to our classes from the core file.

forms.index.ts (1)

...
export * from "./xc-form-group.component";
...
(d) Export the form group from our index file.

Next we need to update our forms control (e), input (f) and form group components (g).

xc-form-control.ts (1)

import { XcControl } from "../forms.core"; // Remove this
import { XcControl } from "./xc-control";
...
(e) We need to update our xc control reference.

xc-input.component.ts (1)

...
import { XcFormControl } from "../../forms.core"; // Remove this
import { XcFormControl } from "../xc-form-control";
...
(f) We need to update our xc form control reference.

xc-form-group.component.ts (2)

...
import { XcFormControl } from "./controls/xc-form-control";

import { throwMissingInputError } from "./forms.core";
...
export class XcFormGroupComponent ... {
    ...
}
(g) We need to update our xc form control reference.

ForwardRef to the rescue

  • WebUi
    • Source
      • forms
        • controls
          • input
            • xc-input.component.ts
        • xc-form-group.component.ts

If we check now to see if we have access to our inputs we will find that the query list does not contain any elements (h). The reason for this is that we are looking for XcFormControl and we have XcInput components within our template. Since the XcInput inherits from XcFormControl we can use the forwardRef function to solve this problem (i). In (j) we can see that our query list is empty before the change and in (k) we can see the list contains our input as expected.

xc-form-group.component.ts (3)

...
export class XcFormGroupComponent ... {
    ...
    public ngAfterViewInit(): void {
        ...
        console.log(this._formControls); // Remove after we see it is working
    }
    ...
}
(h) Console log statement that shows our query list is empty before using forewardRef, (j), and contains our input after using it (k).

xc-input.component.ts (2)

import { forwardRef, ... } from "@angular/core";

import { XcFormControl } from "../xc-form-control";

@Component({
    ...,
    providers: [{ provide: XcFormControl, useExisting: forwardRef(() => XcInputComponent) }]
})
export class XcInputComponent extends XcFormControl {
    ...
}
(i) Including the forwardRef statement fixes our inheritance problem.
Image showing our query list is empty before using the forwardRef function.
(j) Image showing our query list is empty before using the forwardRef function.
Image showing our query list contains a reference to our input after using the forwardRef function.
(k) Image showing our query list contains a reference to our input after using the forwardRef function.
Advertisement

Time to use the form control name directive

  • WebUi
    • Source
      • forms
        • controls
          • input
            • xc-input.component.pug
            • xc-input.component.ts
          • xc-form-control.ts
        • xc-form-group.ts

In order to add a reference for our input to the form group we need to make use of the formControlName directive. To use this directive we need to add a name to our input. To do this we add an input to our XcFormControl class (l). Since we need this input to be defined we will once again through an error if it is not. To make it possible for us to throw the error in our base class we will force any class that inherits from it to define a getter for it's tag name. With that done we need to add the getter to our input class (m). And in our example template we need to specify the name for our input (n).

xc-form-control.ts (2)

import { Input } from "@angular/core";

import { throwMissingInputError } from "../forms.core";
...
export abstract class XcFormControl extends XcControl {
    @Input("name") private readonly _name: string = null;

    private get name() {
        throwMissingInputError(this._name, this.tagName, "You must specify the name.", "name='name'");
        return this._name;
    }
    private get tName() { return this.name; }
    protected abstract get tagName(): string;
}
(o) In order to add our control to the angular form group we need to have a name for it. We will require the name to set by throwing an error if it is not.

xc-input.component.ts (3)

...
export class XcInputComponent ... {
    ...
    protected get tagName() { return "xc-input"; }
}
(m) We need to specify a tag name so that an error can be thrown if needed.

basic-example.component.pug (1)

xc-form-group(...)
    xc-input(..., name="input")
(n) Our example now needs to define a name for the input.

The moment of truth has arrived and it is now time to add the form control name directive to our component's template (p).

xc-input.component.pug (1)

div.xc-form-control-group
    ...
    input(..., [formControlName]="tName")
(p) By adding in the form control name directive we should be good to go.

Remembering that our form group contains a reference to an angular form group in its template we should be good to go now. Of course since I am writing this you know that we are not. Once everything is saved and we look in the browser console window we should see an error (q).

Error showing angular can not find the form group.
(q) We have a form group directive higher up the component tree from the form control name directive so what's the problem?

Stupid ng-content in our way

  • WebUi
    • Source
      • forms
        • input
          • xc-input.component.pug
        • xc-form-control.ts
        • xc-form-group.component.ts

Since the form group directive is contained in a parent component that is on the other side of ng-content our form control name directive can not find it. The solution is to make sure a reference to the group is also within the template of our control. We start by defining a getter and setter for the form group within our base form control class (r). With that done we need to add the form group directive to our control template (s). Lastly we need to set our control form group property within the form group component (t).

xc-form-control.ts (3)

...
import { FormControl, FormGroup } from "@angular/forms";
...
export abstract class XcFormControl extends XcControl {
    ...
    private _group: FormGroup = null;
    protected formControlInternal = new FormControl();
    ...
    private get tGroup() { return this._group; }
    ...
    public set group(value: FormGroup) {
        value.addControl(this.name, this.formControlInternal);
        this._group = value;
    }
}
(r) Adding a getter and setter for the form group that will be passed in by our form group component.

xc-input.component.pug (2)

div.xc-form-control-group(*ngIf="tGroup", [formGroup]="tGroup")
    ...
    input(...)
(s) Updating our template to make use of the new form group getter.

xc-form-group.component.ts (4)

...
export class XcFormGroupComponent ... {
    ...
    public ngAfterViewInit(): void {
        ...
        this._formControls.forEach(x => {
            x.group = this._group;
        });
    }
}
(t) We need to set the form group property of our controls.

For sure everything must be working correctly now, right? Tell that to the error that we are receiving now (u). As we can see this is the result of updating a property after the change detector has already run.

Image showing the change detection error that we are receiving now.
(u) Image showing the change detection error that we are receiving now.

And the hero is...settimeout

  • WebUi
    • Source
      • forms
        • xc-form-group.component.ts

To eliminate the change detection error we just need to wrap our for each method with a set timeout call (v). Since we know that the after view init callback will be executed only once we can get away with this here. I would not do this in a method that gets called more than once within our component's lifecycle.

xc-form-group.component.ts (5)

...
export class XcFormGroupComponent ... {
    ...
    public ngAfterViewInit(): void {
        ...
        setTimeout(() => {
            this._formControls.forEach(x => {
                x.group = this._group;
                console.log(x); // Remove after we see it is working
            });
        });
    }
}
(v) Wrapping the for each call in a set timeout callback will remove the change detection error.
Console showing our component is wired up correctly.
(w) Just to make sure that everything thing is wired up we can take a look at our component within the console.
Exciton Interactive LLC
Advertisement