A common pattern you will see throughout the Angular source code, when parent/child relationships are involved, is the parent type adding itself as a provider to itself. What this does is allow child component to inject the parent. And there will on only be one instance of the parent component all the way down the component tree because of hierarchical DI. Below is an example of what that might look like
export abstract class FormControlContainer {
abstract addControl(name: string, control: FormControl): void;
abstract removeControl(name: string): void;
}
export const formGroupContainerProvider: any = {
provide: FormControlContainer,
useExisting: forwardRef(() => NestedFormComponentsComponent)
};
@Component({
selector: 'nested-form-components',
template: `
...
`,
directives: [REACTIVE_FORM_DIRECTIVES, ChildComponent],
providers: [formGroupContainerProvider]
})
export class ParentComponent implements FormControlContainer {
form: FormGroup = new FormGroup({});
addControl(name: string, control: FormControl) {
this.form.addControl(name, control);
}
removeControl(name: string) {
this.form.removeControl(name);
}
}
Some notes:
We're using an interface/abstract parent (FormControlContainer
) for a couple reasons
- It decouples the
ParentComponent
from the ChildComponent
. The child doesn't need to know anything about the specific ParentComponent
. All it knows about is the FormControlContainer
and the contract that is has.
- We only expose methods on the
ParentComponent
that want, through the interface contract.
We only advertise ParentComponent
as FormControlContainer
, so the latter is what we will inject.
We create a provider in the form of the formControlContainerProvider
and then add that provider to the ParentComponent
. Because of hierarchical DI, now all the children have access to the parent.
If you are unfamiliar with forwardRef
, this is a great article
Now in the child(ren) you can just do
@Component({
selector: 'child-component',
template: `
...
`,
directives: [REACTIVE_FORM_DIRECTIVES]
})
export class ChildComponent implements OnDestroy {
firstName: FormControl;
lastName: FormControl;
constructor(private _parent: FormControlContainer) {
this.firstName = new FormControl('', Validators.required);
this.lastName = new FormControl('', Validators.required);
this._parent.addControl('firstName', this.firstName);
this._parent.addControl('lastName', this.lastName);
}
ngOnDestroy() {
this._parent.removeControl('firstName');
this._parent.removeControl('lastName');
}
}
IMO, this is a much better design than passing the FormGroup
through @Input
s. As stated earlier, this is a common design throughout the Angular source, so I think it's safe to say that it's an acceptable pattern.
If you wanted to make the child components more reusable, you could make the constructor parameter @Optional()
.
Below is the complete source I used to test the above examples
import {
Component, OnInit, ViewChildren, QueryList, OnDestroy, forwardRef, Injector
} from '@angular/core';
import {
FormControl,
FormGroup,
ControlContainer,
Validators,
FormGroupDirective,
REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';
export abstract class FormControlContainer {
abstract addControl(name: string, control: FormControl): void;
abstract removeControl(name: string): void;
}
export const formGroupContainerProvider: any = {
provide: FormControlContainer,
useExisting: forwardRef(() => NestedFormComponentsComponent)
};
@Component({
selector: 'nested-form-components',
template: `
<form [formGroup]="form">
<child-component></child-component>
<div>
<button type="button" (click)="onSubmit()">Submit</button>
</div>
</form>
`,
directives: [REACTIVE_FORM_DIRECTIVES, forwardRef(() => ChildComponent)],
providers: [formGroupContainerProvider]
})
export class NestedFormComponentsComponent implements FormControlContainer {
form = new FormGroup({});
onSubmit(e) {
if (!this.form.valid) {
console.log('form is INVALID!')
if (this.form.hasError('required', ['firstName'])) {
console.log('First name is required.');
}
if (this.form.hasError('required', ['lastName'])) {
console.log('Last name is required.');
}
} else {
console.log('form is VALID!');
}
}
addControl(name: string, control: FormControl): void {
this.form.addControl(name, control);
}
removeControl(name: string): void {
this.form.removeControl(name);
}
}
@Component({
selector: 'child-component',
template: `
<div>
<label for="firstName">First name:</label>
<input id="firstName" [formControl]="firstName" type="text"/>
</div>
<div>
<label for="lastName">Last name:</label>
<input id="lastName" [formControl]="lastName" type="text"/>
</div>
`,
directives: [REACTIVE_FORM_DIRECTIVES]
})
export class ChildComponent implements OnDestroy {
firstName: FormControl;
lastName: FormControl;
constructor(private _parent: FormControlContainer) {
this.firstName = new FormControl('', Validators.required);
this.lastName = new FormControl('', Validators.required);
this._parent.addControl('firstName', this.firstName);
this._parent.addControl('lastName', this.lastName);
}
ngOnDestroy() {
this._parent.removeControl('firstName');
this._parent.removeControl('lastName');
}
}