#3 Angular Form Group Interop
Friday, June 22, 2018
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.
Parts
- Part 14: Pipes to the Rescue
- Part 13: Rest of the Examples
- Part 12: Checkbox Example
- Part 11: Adding a Radio Button
- Part 10: Adding a Radio Group
- Part 9: Adding a Select
- Part 8: Adding a Checkbox
- Part 7: Adding a Textarea
- Part 6: Highlighting with Prismjs
- Part 5: Form Snippets Manager
- Part 4: Accessing Form Data
- Part 3: Angular Form Group Interop
- Part 2: The Form Group
- Part 1: Forms Project Creation
References for our inputs
-
WebUi
-
Source
-
forms
- xc-form-group.component.ts
-
forms
-
Source
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
}
...
}
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.
Those circular dependencies will get you every time
-
WebUi
-
Source
-
forms
-
controls
-
input
- xc-input.component.ts
- xc-form-control.ts
-
input
- forms.core.ts
- forms.index.ts
- xc-form-group.component.ts
-
controls
-
forms
-
Source
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
...
forms.index.ts (1)
...
export * from "./xc-form-group.component";
...
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";
...
xc-input.component.ts (1)
...
import { XcFormControl } from "../../forms.core"; // Remove this
import { XcFormControl } from "../xc-form-control";
...
xc-form-group.component.ts (2)
...
import { XcFormControl } from "./controls/xc-form-control";
import { throwMissingInputError } from "./forms.core";
...
export class XcFormGroupComponent ... {
...
}
ForwardRef to the rescue
-
WebUi
-
Source
-
forms
-
controls
-
input
- xc-input.component.ts
-
input
- xc-form-group.component.ts
-
controls
-
forms
-
Source
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
}
...
}
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 {
...
}
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
-
input
- xc-form-group.ts
-
controls
-
forms
-
Source
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;
}
xc-input.component.ts (3)
...
export class XcInputComponent ... {
...
protected get tagName() { return "xc-input"; }
}
basic-example.component.pug (1)
xc-form-group(...)
xc-input(..., name="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")
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).
Stupid ng-content in our way
-
WebUi
-
Source
-
forms
-
input
- xc-input.component.pug
- xc-form-control.ts
- xc-form-group.component.ts
-
input
-
forms
-
Source
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;
}
}
xc-input.component.pug (2)
div.xc-form-control-group(*ngIf="tGroup", [formGroup]="tGroup")
...
input(...)
xc-form-group.component.ts (4)
...
export class XcFormGroupComponent ... {
...
public ngAfterViewInit(): void {
...
this._formControls.forEach(x => {
x.group = this._group;
});
}
}
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.
And the hero is...settimeout
-
WebUi
-
Source
-
forms
- xc-form-group.component.ts
-
forms
-
Source
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
});
});
}
}