Angular2嵌套模板驱动表单

这简直是疯了,看起来没有办法让一个表单的其中一个输入是在子组件中。

我读了所有的博客,教程和一切,没有办法解决这个问题。

问题在于,当子组件具有任何形式的指令(ngModel、 ngModelGroup 或其他形式的指令)时。.)没用的。

这只是模板驱动的表单中的一个问题

这是 混蛋:

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;
}
}
26800 次浏览

来自官方 医生: This directive can only be used as a child of NgForm.

因此,我认为您可以尝试将子组件包装在不同的 ngForm中,并期望在父组件中得到子组件的 @Output结果。如果你需要进一步澄清,请告诉我。

更新: 这里是 笨蛋与一些变化,我转换子表单到模型驱动,因为没有办法监听表单驱动的形式更新之前,它将被提交。

通过阅读一系列相关的 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作为属性公开,并将自身导出为 模板引用变量:

@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>

注意,提交按钮显式地检查 ngFormAddressaddressFields表单的有效状态。这样,我至少可以合理地构成复杂的形式,即使它有一些样板。

另一个可能的解决办法:

@Directive({
selector: '[provide-parent-form]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}
]
})
export class ProvideParentForm {}

只需将此指令放在节点层次结构顶部的某个子组件中(在 ngModel 之前)。

如何使用@Host ()进行 NgModel 符合条件父窗体的依赖项查找。因此,对于子组件中的 NgModel 来说,来自父组件的表单是不可见的。但是我们可以使用上面演示的代码在子组件中注入并提供它。

我使用指令和服务创建了一个解决方案。一旦将这些代码添加到模块中,您需要做的唯一其他代码更改就是在模板的表单级别上进行。这适用于动态添加的表单字段和 AOT。它还支持页面上多个不相关的表单。这里是普拉克: 混蛋

它使用以下指令:

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;
      

}

一个简单的解决方案是在子组件的 viewProviders数组中提供 ControlContainer,比如:

import { ControlContainer, NgForm } from '@angular/forms';


@Component({
...,
viewProviders: [ { provide: ControlContainer, useExisting: NgForm } ]
})
export class ChildComponent {}

Stackblitz 示例

也请阅读这篇文章,解释为什么它的工作原因。

更新

如果你正在寻找 嵌套式模型驱动形式,那么这里有一个类似的方法:

@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))
}
}

五-运行示例

更新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 {}

五-运行示例

对于大约100个动态形式的控件,隐式包含控件可能会使您成为模板驱动的巨人。下面将在所有地方应用 Yurzui 的奇迹

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 为组件提供 control ContainerProvider。

StackBlitz 示例

窗体需要控件在默认情况下设置 name 属性。使用以下指令删除此要求,并且仅在设置 name 属性时包含控件。

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 示例