问题
I am aware of creating custom controls as components, but I can't figure out how to create custom groups.
The same we can do this by implementing ControlValueAccessor and using a custom component like <my-cmp formControlName="foo"></my-cmp>
, how can we achieve this effect for a group?
<my-cmp formGroupName="aGroup"></my-cmp>
Two very common use-cases would be (a) separting a long form into steps, each step in a separate component and (b) encapsulating a group of fields which appear across multiple forms, such as address (group of country, state, city, address, building number) or date of birth (year, month, date).
Example usage (not actual working code)
Parent has the following form built with FormBuilder:
// parent model
form = this.fb.group({
username: '',
fullName: '',
password: '',
address: this.fb.group({
country: '',
state: '',
city: '',
street: '',
building: '',
})
})
Parent template (inaccessible and non-semantic for brevity):
<!-- parent template -->
<form [groupName]="form">
<input formControlName="username">
<input formControlName="fullName">
<input formControlName="password">
<address-form-group formGroup="address"></address-form-group>
</form>
Now this AddressFormGroupComponent
knows how to handle a group which has these specific controls inside of it.
<!-- child template -->
<input formControlName="country">
<input formControlName="state">
<input formControlName="city">
<input formControlName="street">
<input formControlName="building">
回答1:
The piece I was missing was mentioned in rusev's answer, and that is injecting the ControlContainer.
Turns out that if you place formGroupName
on a component, and if that component injects ControlContainer
, you get a reference to the container which contains that form. It's easy from here.
We create a sub-form component.
@Component({
selector: 'sub-form',
template: `
<ng-container [formGroup]="controlContainer.control">
<input type=text formControlName=foo>
<input type=text formControlName=bar>
</ng-container>
`,
})
export class SubFormComponent {
constructor(public controlContainer: ControlContainer) {
}
}
Notice how we need a wrapper for the inputs. We don't want a form because this will already be inside a form. So we use an ng-container
. This will be striped away from the final DOM so there's no unnecessary element.
Now we can just use this component.
@Component({
selector: 'my-app',
template: `
<form [formGroup]=form>
<sub-form formGroupName=group></sub-form>
<input type=text formControlName=baz>
</form>
`,
})
export class AppComponent {
form = this.fb.group({
group: this.fb.group({
foo: 'foo',
bar: 'bar',
}),
baz: 'baz',
})
constructor(private fb: FormBuilder) {}
}
You can see a live demo on StackBlitz.
This is an improvement over rusev's answer in a few aspects:
- no custom
groupName
input; instead we use theformGroupName
provided by Angular - no need for
@SkipSelf
decorator, since we're not injecting the parent control, but the one we need - no awkward
group.control.get(groupName)
which is going to the parent in order to grab itself.
回答2:
Angular forms doesn't have concept for group name as for form control name. However you can quite easily workaround this by wrapping the child template in a form group.
Here is an example similar to the markup you've posted - https://plnkr.co/edit/2AZ3Cq9oWYzXeubij91I?p=preview
@Component({
selector: 'address-form-group',
template: `
<!-- child template -->
<ng-container [formGroup]="group.control.get(groupName)">
<input formControlName="country">
<input formControlName="state">
<input formControlName="city">
<input formControlName="street">
<input formControlName="building">
</ng-container>
`
})
export class AddressFormGroupComponent {
@Input() public groupName: string;
constructor(@SkipSelf() public group: ControlContainer) { }
}
@Component({
selector: 'my-app',
template: `
<!-- parent template -->
<div [formGroup]="form">
<input formControlName="username">
<input formControlName="fullName">
<input formControlName="password">
<address-form-group groupName="address"></address-form-group>
</div>
{{form?.value | json}}
`
})
export class AppComponent {
public form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
username: '',
fullName: '',
password: '',
address: this.fb.group({
country: '',
state: '',
city: '',
street: '',
building: '',
})
});
}
}
回答3:
I have found a dynamic way to do this with Reactive Forms.
Researching on the issues and posts about this topic, I found material to build a directive that bounds a parent form, with dynamic child components. The neat thing is that you don't need to define all the form in the parent component, but each FormGroup is independent and bounded vía the directive.
I've named it BindFormDirective
, and it receives a parent
and a child
Components who implements the BindForm
interface (they have a public form: FormGroup
member to manipulate), and they provide themselves with the BINDFORM_TOKEN
.
The directive receives a value as the name of the child group, and its code is as follows:
import { ChangeDetectorRef, Directive, Inject, InjectionToken, Input, OnDestroy, OnInit, Self, SkipSelf } from '@angular/core';
import { FormGroup } from '@angular/forms';
export interface BindForm {
form: FormGroup;
}
export const BINDFORM_TOKEN = new InjectionToken<BindForm>('BindFormToken');
@Directive({
selector: '[bindForm]'
})
export class BindFormDirective implements OnInit, OnDestroy {
private controlName = null;
@Input()
set binForm(value) {
if (this.controlName) {
throw new Error('Cannot change the bindName on runtime!');
}
this.controlName = value;
}
constructor(
private cdr: ChangeDetectorRef,
@Inject(BINDFORM_TOKEN) @SkipSelf() private parent: BindForm,
@Inject(BINDFORM_TOKEN) @Self() private child: BindForm
) {}
ngOnInit() {
if (!this.controlName) {
throw new Error('BindForm directive requires a value to be used as the subgroup name!');
}
if (this.parent.form.get(this.controlName)) {
throw new Error(`That name (${this.controlName}) already exists on the parent form!`);
}
// add a child control under the unique name
this.parent.form.addControl(this.controlName, this.child.form);
this.cdr.detectChanges();
}
ngOnDestroy() {
// remove the component from the parent
this.parent.form.removeControl(this.controlName);
}
}
In the other hand, the involved components should provide the BINDFORM_TOKEN
in their @Component
definition, to be inject-able by the directive, and implement the BindForm
interface like:
@Component({
...
providers: [
{
provide: BINDFORM_TOKEN,
useExisting: forwardRef(() => MyFormComponent)
}
]
})
export class MyFormComponent implements BindForm, OnInit {
form: FormGroup;
...
So, you implement your many form components independently, and to bind FormGroups each other, you just use the directive on your parent form component:
<form [formGroup]="form" ...>
<my-step1 bindForm="step1"></my-step1>
</form>
If the remaining code is needed for a complete illustration, I would dedicate some additional time to update my answer later. Enjoy!
来源:https://stackoverflow.com/questions/45696761/create-a-reusable-formgroup