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 没有提供者!”。我怀疑这是因为我们的自定义表单控件有一个 ,然后在其中插入了各种控件。知道这可能是什么解决方法吗? @Nimishgoel 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 中删除 &lt;fieldset ngModelGroup="zip"&gt; 包装器@【参考方案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>

请注意,提交按钮明确检查ngFormAddressaddressFields 表单上的有效状态。这样我至少可以明智地组合复杂的表格,即使它有一些样板。

【讨论】:

约翰内斯·鲁道夫竞选总裁。你拯救了我的一天 :) 谢谢! 根据您的回答,我找到了一种方法,使子组件可以将自己自动连接到共享表单服务中: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嵌套模板驱动表单的主要内容,如果未能解决你的问题,请参考以下文章

带有 ngFor 输入的 Angular 2 模板驱动表单

Angular 2中的模板驱动形式和反应形式有啥区别

Angular2 模板驱动的异步验证器

Angular 2 - 自定义表单控件 - 禁用

markdown Angular2 Snippets - 表格(模板驱动)

以 angular2 模型驱动形式重用组件