Let's say that my generated html from angular2 app looks like this:
<app>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)">
<panel-component>
<mid-component>
<inner-component-with-inputs>
<input/>
<inner-component-with-inputs>
<mid-component>
</panel-component>
<panel-component>
<mid-component>
<inner-component-with-inputs>
<input/>
<inner-component-with-inputs>
<mid-component>
</panel-component>
<!-- many many many fields -->
<button type="submit">Submit</button>
</form>
</app>
How can I set up my outer <form>
in such a way that I can validate all inner inputs on submit? Do I have to pass myForm
through @Input()
all the way down from panel-component
to inner-component-with-inputs
? Or is there other way?
In my application I have very big form with multiple panels, subpanels, tabs, modals etc. and I need to be able to validate it all at once on submit.
All the tutorials and resources on the internet talk only about forms spanning one component/template.
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');
}
}
There is easier way to pass formGroup and formControl into lower component - using @Inputs.
Plunker: https://plnkr.co/edit/pd30ru?p=preview
In FormComponent (MgForms) [main] we do:
in code:
this.form = this.formBuilder.group(formFields);
in template:
<form [formGroup]="form" novalidate>
<div class="mg-form-element" *ngFor="let element of fields">
<div class="form-group">
<label class="center-block">{{element.description?.label?.text}}:
<div [ngSwitch]="element.type">
<!--textfield component-->
<div *ngSwitchCase="'textfield'"class="form-control">
<mg-textfield
[group]="form"
[control]="form.controls[element.fieldId]"
[element]="element">
</mg-textfield>
</div>
<!--numberfield component-->
<div *ngSwitchCase="'numberfield'"class="form-control">
<mg-numberfield
[group]="form"
[control]="form.controls[element.fieldId]"
[element]="element">
</mg-numberfield>
</div>
</div>
</label>
</div>
</div>
</form>
In FieldComponent (MgNumberfield) [inner] we do:
in code:
@Input() group;
@Input() control;
@Input() element;
in template:
<div [formGroup]="group">
<input
type="text"
[placeholder]="element?.description?.placeholder?.text"
[value]="control?.value"
[formControl]="control">
</div>