【问题标题】:Angular2 nested template driven formAngular2嵌套模板驱动表单
【发布时间】: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; 
  }
}

【问题讨论】:

标签: angular angular2-forms


【解决方案1】:

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

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

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

Stackblitz Example

另请阅读这篇解释其工作原理的文章。

更新

如果您正在寻找嵌套模型驱动表单,那么这里是类似的方法:

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

来自官方docsThis 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

    【讨论】:

      猜你喜欢
      • 2020-10-11
      • 2019-04-08
      • 2018-04-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-14
      • 2017-07-10
      • 2017-01-30
      相关资源
      最近更新 更多