【问题标题】:Angular 2 nested forms with child components and validation带有子组件和验证的 Angular 2 嵌套表单
【发布时间】:2017-11-08 13:02:47
【问题描述】:

我正在尝试在 Angular 2 中实现一个带有验证的嵌套表单,我看过帖子并遵循了文档,但我真的很挣扎,希望你能指出我正确的方向。

我想要实现的是拥有一个包含多个子组件的经过验证的表单。这些子组件有点复杂,其中一些有更多的子组件,但是为了这个问题,我认为我们可以解决有父和子的问题。

我想要完成什么

有一个像这样工作的表单:

<div [formGroup]="userForm" novalidate>
    <div>
        <label>User Id</label>
        <input formControlName="userId">
    </div>
    <div>
        <label>Dummy</label>
        <input formControlName="dummyInput">
    </div>
</div>

这需要一个这样的类:

private userForm: FormGroup;
constructor(private fb: FormBuilder){
    this.createForm();
}
private createForm(): void{
    this.userForm = this.fb.group({
        userId: ["", Validators.required],
        dummyInput: "", Validators.required]
    });
}

这按预期工作,但现在我想解耦代码,并将“dummyInput”功能放在单独的不同组件中。这就是我迷路的地方。这是我尝试过的,我想我离答案不远了,但我真的没有想法,我对这个场景还很陌生:

parent.component.html

<div [formGroup]="userForm" novalidate>
    <div>
        <label>User Id</label>
        <input formControlName="userId">
    </div>
    <div>
        <dummy></dummy>
    </div>
</div>

parent.component.ts

private createForm(): void{
    this.userForm = this.fb.group({
    userId: ["", Validators.required],
    dummy: this.fb.group({
        dummyInput: ["", Validators.required]
    })
});

children.component.html

<div [formGroup]="dummyGroup">
    <label>Dummy Input: </label>
    <input formControlName="dummyInput">
</div>

children.component.ts

private dummyGroup: FormGroup;

我知道代码有问题,但我确实遇到了障碍。任何帮助将不胜感激。

谢谢。

【问题讨论】:

标签: forms angular validation components


【解决方案1】:

FormGroupDirective 的替代方法(如@blacksheep's answer 中所述)是使用ControlContainer,如下所示:

import { FormGroup, ControlContainer } from "@angular/forms";

export class ChildComponent implements OnInit {

  formGroup: FormGroup;

  constructor(private controlContainer: ControlContainer) {}

  ngOnInit() {
    this.formGroup = <FormGroup>this.controlContainer.control;
  }

formGroup 可以设置在直接父级或更高级别(例如,父级的父级)。这使得可以在各种嵌套组件上传递 from 组,而无需 @Input()s 链来传递 formGroup。在任何父级中设置formGroup 以使其通过子级中的ControlContainer 可用:

<... [formGroup]="myFormGroup">

【讨论】:

    【解决方案2】:

    要获得对父表单的引用,只需使用它(可能在 Angular 2 中不可用。我已经用 Angular 6 测试过):

    TS

    import {
       FormGroup,
       ControlContainer,
       FormGroupDirective,
    } from "@angular/forms";
    
    @Component({
      selector: "app-leveltwo",
      templateUrl: "./leveltwo.component.html",
      styleUrls: ["./leveltwo.component.sass"],
      viewProviders: [
        {
          provide: ControlContainer,
          useExisting: FormGroupDirective
        }
      ]
    })
    export class NestedLevelComponent implements OnInit {
      //form: FormGroup;
    
      constructor(private parent: FormGroupDirective) {
         //this.form = form;
      }
    }
    

    HTML

    <input type="text" formControlName="test" />
    

    【讨论】:

      【解决方案3】:

      主要思想是你必须将formGroup和formControls视为变量,主要是javascript对象和数组。

      所以我会输入一些代码来说明我的观点。下面的代码有点像你所拥有的。该表单是动态构建的,只是它被分成多个部分,每个部分包含其共享的字段和标签。

      HTML 由 typescript 类支持。那些不在这里,因为它们没有什么特别之处。只有 FormSchemaUI、FormSectionUI 和 FormFieldUI 很重要。

      将每段代码视为自己的文件。

      另外请注意,formSchema: FormSchema 是我从服务接收的 JSON 对象。您没有看到定义的 UI 类的任何属性都是从它们的基本 Data 类继承的。这些不在此处介绍。 层次结构是: FormSchema 包含多个部分。一个部分包含多个字段。

      <form (ngSubmit)="onSubmit()" #ciRegisterForm="ngForm" [formGroup]="formSchemaUI.MainFormGroup">
          <button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
          <br /><br />
          <app-ci-register-section *ngFor="let sectionUI of formSchemaUI.SectionsUI" [sectionUI]="sectionUI">
          </app-ci-register-section>
          <button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
      </form>
      

      ==============================================

      <div class="row" [formGroup]="sectionUI.MainFormGroup">
          <div class="col-md-12  col-lg-12" [formGroupName]="sectionUI.SectionDisplayId">
              <fieldset class="section-border">
                  <legend class="section-border">{{sectionUI.Title}}</legend>
                  <ng-container *ngFor='let fieldUI of sectionUI.FieldsUI; let i=index; let even = even;'>
                      <div class="row" *ngIf="even">
                          <ng-container>
                              <div class="col-md-6  col-lg-6" app-ci-field-label-tuple [fieldUI]="fieldUI">
      
                              </div>
                          </ng-container>
                          <ng-container *ngIf="sectionUI.Fields[i+1]">
                              <div class="col-md-6  col-lg-6" app-ci-field-label-tuple [fieldUI]="sectionUI.FieldsUI[i+1]">
      
                              </div>
                          </ng-container>
                      </div>
                  </ng-container>           
              </fieldset>
          </div>
      </div>
      

      ==============================================

      {{fieldUI.Label}}

      ==============================================

      <ng-container>
          <div class="row">
              <div class="col-md-4 col-lg-4 text-right">
                  <label for="{{fieldUI.FieldDisplayId}}"> {{fieldUI.Label}} </label>
              </div>
              <div class="col-md-8 col-lg-8">
                  <div app-ci-field-edit [fieldUI]="fieldUI" ></div>
              </div>
          </div>       
      </ng-container>
      

      ==============================================

      <ng-container [formGroup]="fieldUI.ParentSectionFormGroup">    
          <ng-container *ngIf="fieldUI.isEnabled">         
              <ng-container [ngSwitch]="fieldUI.ColumnType">            
                  <input *ngSwitchCase="'HIDDEN'" type="hidden" id="{{fieldUI.FieldDisplayId}}" [value]="fieldUI.Value" />
                  <ci-field-textbox *ngSwitchDefault
                                    [fieldUI]="fieldUI"
                                    (valueChange)="onValueChange($event)"
                                    class="fullWidth" style="width:100%">
                  </ci-field-textbox>
              </ng-container>       
          </ng-container>
      </ng-container>
      

      ==============================================

      export class FormSchemaUI extends FormSchema { 
      
          SectionsUI: Array<FormSectionUI>;
          MainFormGroup: FormGroup;
      
          static fromFormSchemaData(formSchema: FormSchema): FormSchemaUI {
              let formSchemaUI = new FormSchemaUI(formSchema);
              formSchemaUI.SectionsUI = new Array<FormSectionUI>();
              formSchemaUI.Sections.forEach(section => {
                  let formSectionUI = FormSectionUI.fromFormSectionData(section);
      
                  formSchemaUI.SectionsUI.push(formSectionUI);
              });
              formSchemaUI.MainFormGroup = FormSchemaUI.buildMainFormGroup(formSchemaUI);        
              return formSchemaUI;
          }
      
          static buildMainFormGroup(formSchemaUI: FormSchemaUI): FormGroup {
              let obj = {};
              formSchemaUI.SectionsUI.forEach(sectionUI => {
                  obj[sectionUI.SectionDisplayId] = sectionUI.SectionFormGroup;
              });
              let sectionFormGroup = new FormGroup(obj);
              return sectionFormGroup;
          }
      }
      

      ==============================================

      export class FormSectionUI extends FormSection {
      
          constructor(formSection: FormSection) {        
              this.SectionDisplayId = 'section' + this.SectionId.toString();
          }
      
          SectionDisplayId: string;
          FieldsUI: Array<FormFieldUI>;
          HiddenFieldsUI: Array<FormFieldUI>;
          SectionFormGroup: FormGroup;
          MainFormGroup: FormGroup;
          ParentFormSchemaUI: FormSchemaUI;
      
          static fromFormSectionData(formSection: FormSection): FormSectionUI {
              let formSectionUI = new FormSectionUI(formSection);
              formSectionUI.FieldsUI = new Array<FormFieldUI>();
              formSectionUI.HiddenFieldsUI = new Array<FormFieldUI>();
              formSectionUI.Fields.forEach(field => {
                  let fieldUI = FormFieldUI.fromFormFieldData(field);
                  if (fieldUI.ColumnType != 'HIDDEN')
                      formSectionUI.FieldsUI.push(fieldUI);
                  else formSectionUI.HiddenFieldsUI.push(fieldUI);
              });
              formSectionUI.SectionFormGroup = FormSectionUI.buildFormSectionFormGroup(formSectionUI);
              return formSectionUI;
          }
      
          static buildFormSectionFormGroup(formSectionUI: FormSectionUI): FormGroup {
              let obj = {};
              formSectionUI.FieldsUI.forEach(fieldUI => {
                  obj[fieldUI.FieldDisplayId] = fieldUI.FieldFormControl;
              });
              let sectionFormGroup = new FormGroup(obj);
              return sectionFormGroup;
          }
      }
      

      ==============================================

      export class FormFieldUI extends FormField {    
      
          constructor(formField: FormField) {
          super();
          this.FieldDisplayId = 'field' + this.FieldId.toString();       
      
          this.ListItems = new Array<SelectListItem>();        
         }
      
          public FieldDisplayId: string;
      
          public FieldFormControl: FormControl;
          public ParentSectionFormGroup: FormGroup;
          public MainFormGroup: FormGroup;
          public ParentFormSectionUI: FormSectionUI;  
      
          public ValueChange: EventEmitter<any> = new EventEmitter<any>();    
      
          static buildFormControl(formFieldUI:FormFieldUI): FormControl {
              let nullValidator = Validators.nullValidator;
      
              let fieldKey: string = formFieldUI.FieldDisplayId; 
              let fieldValue: any;
              switch (formFieldUI.ColumnType) {            
                  default:
                      fieldValue = formFieldUI.Value;
                      break;
              }
              let isDisabled = !formFieldUI.IsEnabled;
              let validatorsArray: ValidatorFn[] = new Array<ValidatorFn>();
              let asyncValidatorsArray: AsyncValidatorFn[] = new Array<AsyncValidatorFn>();
      
              let formControl = new FormControl({ value: fieldValue, disabled: isDisabled }, validatorsArray, asyncValidatorsArray);
              return formControl;
          }
      }
      

      【讨论】:

        【解决方案4】:
        import { Directive } from '@angular/core';
        import { ControlContainer, NgForm } from '../../../node_modules/@angular/forms';
        
        @Directive({
          selector: '[ParentProvider]',
          providers: [
            {
            provide: ControlContainer,
            useFactory: function (form: NgForm) {
              return form;
            },
            deps: [NgForm]
            }`enter code here`
          ]
        })
        export class ParentProviderDirective {
        
          constructor() { }
        
        }
        <div ParentProvider >
          for child
        </div>
        

        【讨论】:

          【解决方案5】:

          您可以在您的子组件中添加一个 Input 以将 FormGroup 传递给它。并使用​​ FormGroupName 来传递您的 FormGroup 的名称:)

          children.component.ts

          @Input('group');
          private dummyGroup: FormGroup;
          

          parent.component.html

          <div [formGroup]="userForm" novalidate>
              <div>
                  <label>User Id</label>
                  <input formControlName="userId">
              </div>
              <div formGroupName="dummy">
                  <dummy [group]="userForm.controls['dummy']"></dummy>
              </div>
          </div>
          

          【讨论】:

          • 这部分:[group]="userForm.controls['dummy']" 是解决方案所在。我花了一天时间试图找到解决这个问题的方法,传递了一个组到子组件。我有一个 ngFor。 Tee 最终代码如下所示:[group]="formGroup.controls['section'+section.SectionId]"
          • 嗨@dragos-durlut,如果你愿意解释它并分享一些代码,我很乐意接受答案。
          • @Mese 我已经添加并回答了代码支持。它应该是您问题的优雅解决方案。
          【解决方案6】:

          不会撒谎,不知道我怎么没早点找到这个帖子。

          Angular 2: Form containing child component

          解决方案是将子组件绑定到同一个formGroup,方法是将formGroup从父组件传递给子组件作为输入。

          如果有人分享一段代码以其他方式解决问题,我很乐意接受。

          【讨论】:

          • 我已添加并由代码支持答案。它应该是您问题的优雅解决方案。
          • 感谢您抽出宝贵的时间,我相信它会比我之前的回答更有帮助!
          • 欢迎。如果您需要更多信息,请评论答案。
          • 我还添加了更多代码。我忘记了section类。
          猜你喜欢
          • 1970-01-01
          • 2018-10-11
          • 1970-01-01
          • 2018-01-11
          • 2019-03-27
          • 2018-10-10
          • 2018-06-07
          • 2017-12-31
          • 2019-01-29
          相关资源
          最近更新 更多