Angular2嵌套模板驱动表单
Posted
技术标签:
【中文标题】Angular2嵌套模板驱动表单【英文标题】:Angular2 nested template driven form 【发布时间】:2017-01-07 14:35:25 【问题描述】:这简直是疯了,看起来没有办法让其中一个输入位于子组件中的表单。
我已经阅读了所有的博客和教程以及所有内容,没有办法解决这个问题。
问题是当子组件将有任何类型的表单指令(ngModel、ngModelGroup 或其他......)时,它不会工作。
这只是模板驱动表单中的问题
这是plunker:
import Component from '@angular/core';
@Component(
selector: 'child-form-component',
template: `
<fieldset ngModelGroup="address">
<div>
<label>Street:</label>
<input type="text" name="street" ngModel>
</div>
<div>
<label>Zip:</label>
<input type="text" name="zip" ngModel>
</div>
<div>
<label>City:</label>
<input type="text" name="city" ngModel>
</div>
</fieldset>`
)
export class childFormComponent
@Component(
selector: 'form-component',
directives:[childFormComponent],
template: `
<form #form="ngForm" (ngSubmit)="submit(form.value)">
<fieldset ngModelGroup="name">
<div>
<label>Firstname:</label>
<input type="text" name="firstname" ngModel>
</div>
<div>
<label>Lastname:</label>
<input type="text" name="lastname" ngModel>
</div>
</fieldset>
<child-form-component></child-form-component>
<button type="submit">Submit</button>
</form>
<pre>
form.value | json
</pre>
<h4>Submitted</h4>
<pre>
value | json
</pre>
`
)
export class FormComponent
value: any;
submit(form)
this.value = form;
【问题讨论】:
另见medium.com/@a.yurich.zuev/… 【参考方案1】:一个简单的解决方案是在您的子组件的viewProviders
数组中提供ControlContainer
,例如:
import ControlContainer, NgForm from '@angular/forms';
@Component(
...,
viewProviders: [ provide: ControlContainer, useExisting: NgForm ]
)
export class ChildComponent
Stackblitz Example
另请阅读这篇解释其工作原理的文章。
Angular: Nested template driven form更新
如果您正在寻找嵌套模型驱动表单,那么这里是类似的方法:
@Component(
selector: 'my-form-child',
template: `<input formControlName="age">`,
viewProviders: [
provide: ControlContainer,
useExisting: FormGroupDirective
]
)
export class ChildComponent
constructor(private parent: FormGroupDirective)
ngOnInit()
this.parent.form.addControl('age', new FormControl('', Validators.required))
Ng-run Example
更新 2
如果您不确切知道哪种类型的 ControlContainer
包装了您的自定义组件(例如您的控件在 FormArray 指令中),那么只需使用通用版本:
import SkipSelf from '@angular/core';
import ControlContainer from '@angular/forms';
@Component(
...,
viewProviders: [
provide: ControlContainer,
useFactory: (container: ControlContainer) => container,
deps: [[new SkipSelf(), ControlContainer]],
]
)
export class ChildComponent
Ng-run Example
【讨论】:
天哪。我一直在等待这个功能!感谢您的帖子。 这对我不起作用 - 我得到一个“NgForm 没有提供者!”。我怀疑这是因为我们的自定义表单控件有一个this.parent.form
在我的上一个示例中指的是父 FormGroup
这么简单,只有一行...viewProviders: [provide: ControlContainer, useExisting: NgForm]
:)
@ManishJain 您需要在 zip 组件 stackblitz.com/edit/angular-qse4xu?file=app/zip.component.ts 中为 viewProviders 使用 NgModelGroup,或者通过提供我在 Update2 stackblitz.com/edit/angular-rbkufw?file=app/zip.component.ts 中描述的 ControlContainer 以更抽象的方式使用。此外,如果您不想拥有像 zip: zip: ''
这样的附加属性,请从您的 zip 组件 stackblitz.com/edit/angular-ixlr45?file=app/zip.component.ts 中删除 <fieldset ngModelGroup="zip">
包装器@【参考方案2】:
阅读了一堆相关的github问题[1][2],我还没有找到一种直接的方法来使角度添加子Component
的控件到父ngForm
(有些人也称它们为嵌套表单、嵌套输入或复杂控件)。
所以我要在这里展示的是一个对我有用的解决方法,对父母和孩子使用单独的ngForm
指令。它并不完美,但它让我足够接近以至于我停在那里。
我用ngForm
指令声明我的childFormComponent
(即它不是 html 表单标签,只有指令):
<fieldset ngForm="addressFieldsForm" #addressFieldsForm="ngForm">
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" [(ngModel)]="model.email" name="email" #email="ngModel" required placeholder="Email">
</div>
...
然后组件将addressFieldsForm
公开为属性,并将自身导出为template reference variable:
@Component(
selector: 'mst-address-fields',
templateUrl: './address-fields.component.html',
styleUrls: ['./address-fields.component.scss'],
exportAs: 'mstAddressFields'
)
export class AddressFieldsComponent implements OnInit
@ViewChild('addressFieldsForm') public form: NgForm;
....
然后父表单可以像这样使用子表单组件:
<form (ngSubmit)="saveAddress()" #ngFormAddress="ngForm" action="#">
<fieldset>
<mst-address-fields [model]="model" #addressFields="mstAddressFields"></mst-address-fields>
<div class="form-group form-buttons">
<button class="btn btn-primary" type="submit" [disabled]="!ngFormAddress.valid || !addressFields.form.valid">Save</button>
</div>
</fieldset>
</form>
请注意,提交按钮明确检查ngFormAddress
和addressFields
表单上的有效状态。这样我至少可以明智地组合复杂的表格,即使它有一些样板。
【讨论】:
约翰内斯·鲁道夫竞选总裁。你拯救了我的一天 :) 谢谢! 根据您的回答,我找到了一种方法,使子组件可以将自己自动连接到共享表单服务中:gist.github.com/jehugaleahsa/c40fb64d8613cfad8f1e1faa4c2a7e33 @TravisParks 您应该将您的完整解决方案添加为这篇文章的新答案,它工作得很好!谢谢!!【参考方案3】:另一种可能的解决方法:
@Directive(
selector: '[provide-parent-form]',
providers: [
provide: ControlContainer,
useFactory: function (form: NgForm)
return form;
,
deps: [NgForm]
]
)
export class ProvideParentForm
只需将此指令放在节点层次结构顶部的某个子组件中(在任何 ngModel 之前)。
它是如何工作的:NgModel qualifies 父表单的依赖查找与@Host()。因此,来自父组件的表单对子组件中的 NgModel 不可见。但是我们可以使用上面演示的代码在子组件中注入和提供它。
【讨论】:
优秀的解决方案!你应该写一篇关于这个的博客文章。 我很想写一篇博文来更深入地了解这个 @artem 它在我的示例 plunker 中不起作用:plnkr.co/edit/VSUzQtrtEmXSIEPzAjXb 你知道为什么吗? @MartinoBordin,好的,那是因为您对所有子输入具有相同的名称。你需要有类似name="childInput-rowNumber"
而不是name="childInput"
@ArtemAndreev 我尝试了你的建议,它几乎可以工作:plnkr.co/edit/WzFsoFbWRgIAu8Nv3Lst 如果你删除例如第一个项目('Apple)然后你添加另一个孩子,第二个绑定将是错误的
【参考方案4】:
来自官方docs:
This directive can only be used as a child of NgForm.
所以我认为您可以尝试将子组件包装在不同的ngForm
中,并期望子组件的父组件结果@Output
。如果您需要更多说明,请告诉我。
更新: 这里是Plunker 有一些变化,我将子表单转换为模型驱动,因为在提交之前无法监听表单驱动的表单以进行更新。
【讨论】:
组件在 ng2 中是隔离的,所以应该回答第一句。附加 plunker 您正在使用响应式定义控件的方式,因为整个问题是关于模板驱动的表单。 我已经回答了你的问题,为什么它不起作用 - 因为你的子组件没有 ngForm。 您的答案与反应式表单有关,这里是关于模板驱动的。有什么解决办法吗?【参考方案5】:我使用指令和服务创建了一个解决方案。将这些添加到模块后,您需要进行的唯一其他代码更改是模板中的表单级别。这适用于动态添加的表单字段和 AOT。它还支持一个页面上的多个不相关的表单。这是 plunker:plunker.
它使用这个指令:
import Directive, Input from '@angular/core';
import NgForm from '@angular/forms';
import NestedFormService from './nested-form.service';
@Directive(
selector: '[nestedForm]',
exportAs: 'nestedForm'
)
export class NestedFormDirective
@Input('nestedForm') ngForm: NgForm;
@Input() nestedGroup: string;
public get valid()
return this.formService.isValid(this.nestedGroup);
public get dirty()
return this.formService.isDirty(this.nestedGroup);
public get touched()
return this.formService.isTouched(this.nestedGroup);
constructor(
private formService: NestedFormService
)
ngOnInit()
this.formService.register(this.ngForm, this.nestedGroup);
ngOnDestroy()
this.formService.unregister(this.ngForm, this.nestedGroup);
reset()
this.formService.reset(this.nestedGroup);
还有这项服务:
import Injectable from '@angular/core';
import NgForm from '@angular/forms';
@Injectable()
export class NestedFormService
_groups: [key: string] : NgForm[] = ;
register(form: NgForm, group: string = null)
if (form)
group = this._getGroupName(group);
let forms = this._getGroup(group);
if (forms.indexOf(form) === -1)
forms.push(form);
this._groups[group] = forms;
unregister(form: NgForm, group: string = null)
if (form)
group = this._getGroupName(group);
let forms = this._getGroup(group);
let i = forms.indexOf(form);
if (i > -1)
forms.splice(i, 1);
this._groups[group] = forms;
isValid(group: string = null) : boolean
group = this._getGroupName(group);
let forms = this._getGroup(group);
for(let i = 0; i < forms.length; i++)
if (forms[i].invalid)
return false;
return true;
isDirty(group: string = null) : boolean
group = this._getGroupName(group);
let forms = this._getGroup(group);
for(let i = 0; i < forms.length; i++)
if (forms[i].dirty)
return true;
return false;
isTouched(group: string = null) : boolean
group = this._getGroupName(group);
let forms = this._getGroup(group);
for(let i = 0; i < forms.length; i++)
if (forms[i].touched)
return true;
return false;
reset(group: string = null)
group = this._getGroupName(group);
let forms = this._getGroup(group);
for(let i = 0; i < forms.length; i++)
forms[i].onReset();
_getGroupName(name: string) : string
return name || '_default';
_getGroup(name: string) : NgForm[]
return this._groups[name] || [];
在带有表单的父组件中使用该指令:
import Component, Input from '@angular/core';
import Person from './person.model';
@Component(
selector: 'parent-form',
template: `
<div class="parent-box">
<!--
ngForm Declare Angular Form directive
#theForm="ngForm" Assign the Angular form to a variable that can be used in the template
[nestedForm]="theForm" Declare the NestedForm directive and pass in the Angular form variable as an argument
#myForm="nestedForm" Assign the NestedForm directive to a variable that can be used in the template
[nestedGroup]="model.group" Pass a group name to the NestedForm directive so you can have multiple forms on the same page (optional).
-->
<form
ngForm
#theForm="ngForm"
[nestedForm]="theForm"
#myForm="nestedForm"
[nestedGroup]="model.group">
<h3>Parent Component</h3>
<div class="pad-bottom">
<span *ngIf="myForm.valid" class="label label-success">Valid</span>
<span *ngIf="!myForm.valid" class="label label-danger">Not Valid</span>
<span *ngIf="myForm.dirty" class="label label-warning">Dirty</span>
<span *ngIf="myForm.touched" class="label label-info">Touched</span>
</div>
<div class="form-group" [class.hasError]="firstName.invalid">
<label>First Name</label>
<input type="text" id="firstName" name="firstName" [(ngModel)]="model.firstName" #firstName="ngModel" class="form-control" required />
</div>
<child-form [model]="model"></child-form>
<div>
<button type="button" class="btn btn-default" (click)="myForm.reset()">Reset</button>
</div>
</form>
</div>
`
)
export class ParentForm
model = new Person();
然后在一个子组件中:
import Component, Input from '@angular/core';
import Person from './person.model';
@Component(
selector: 'child-form',
template: `
<div ngForm #theForm="ngForm" [nestedForm]="theForm" [nestedGroup]="model.group" class="child-box">
<h3>Child Component</h3>
<div class="form-group" [class.hasError]="lastName.invalid">
<label>Last Name</label>
<input type="text" id="lastName" name="lastName" [(ngModel)]="model.lastName" #lastName="ngModel" class="form-control" required />
</div>
</div>
`
)
export class ChildForm
@Input() model: Person;
【讨论】:
【参考方案6】:大约 100 个动态表单中的控件,隐式包含控件可能使您成为模板驱动的主宰。以下将适用于任何地方yurzui's miracle。
export const containerFactory = (container: ControlContainer) => container;
export const controlContainerProvider = [
provide: ControlContainer,
deps: [[new Optional(), new SkipSelf(), ControlContainer]],
useFactory: containerFactory
]
@Directive(
selector: '[ngModel]',
providers: [controlContainerProvider]
)
export class ControlContainerDirective
为带有 NgModelGroup 的组件提供 controlContainerProvider。
StackBlitz Example
默认情况下,表单要求控件设置名称属性。使用以下指令删除此要求,并仅在设置名称属性时包含控件。
import Directive, ElementRef, HostBinding, OnInit from '@angular/core';
import ControlContainer, NgModel from '@angular/forms';
@Directive(
selector: '[ngModel]:not([name]):not([ngModelOptions])',
providers: [
provide: ControlContainer,
useValue: null
]
)
export class StandaloneDirective implements OnInit
StackBlitz Example
【讨论】:
以上是关于Angular2嵌套模板驱动表单的主要内容,如果未能解决你的问题,请参考以下文章