如何在动态组件中使用响应式表单

How to use reactive forms in a dynamic component

背景

我从包含 HTML 的服务器接收客户端生成的数据,然后我用它来创建一个动态组件,该组件被注入并显示在我们的客户端中。 我收到的 HTML 可以包含我需要通过 Angular 响应式表单 绑定的一个或多个输入。

尝试 1:

我试图通过简单地使用 [innerHTML] 属性 并创建动态响应式表单来绑定到输入来解决这个要求。但是,由于使用 innerHTML 属性 的技术限制,该方法失败了。 一旦 HTML 在浏览器中呈现,所有属性都将强制为小写文本,因此任何 Angular 指令或属性都会失败 。例如 *ngIf, *ngFor, [formGroup], formControlName, 等等... Angular 对几乎所有内容都使用驼峰式大小写,因此一旦它被强制为小写文本,它就会被忽略,这种方法不再是一个可行的解决方案。

尝试 2:

这一次我试图利用 Angulars NgTemplateOutlet 将 HTML 动态添加到组件,然后创建并绑定到反应形式。 起初这似乎是一个很好的解决方案,但最终为了让 html 渲染它需要使用 [innerHTML] 属性,再次渲染此方法没用(正如我第一次尝试时所描述的).

尝试 3:

最后我发现了动态组件,这个解决方案部分有效。我现在可以成功地创建一个格式正确的 Angular HTML 模板,该模板可以在浏览器中正确呈现。然而,这只解决了我一半的要求。 此时 HTML 按预期显示,但我无法创建响应式表单并绑定到输入

问题

我现在有一个生成 HTML 的动态组件,其中包含我需要通过创建响应式表单绑定到的输入。

尝试 4:

这次尝试我将创建响应式表单的所有逻辑放在创建的动态组件中。

通过使用此方法显示动态组件 HTML,但我得到一个新错误:

"ERROR Error: formGroup expects a FormGroup instance. Please pass one in."

StackBlitz with error scenario

如果我没看错的话,您的模板 (HTML) 会超出您的组件初始化,特别是在 FormGroup 上。防止这种情况发生的最佳方法是将 *ngIf 语句附加到您绑定 FormGroup 的表单中。这样它就不会呈现,直到您的 FormGroup 被定义。

<form *ngIf="ackStringForm" [formGroup]="ackStringForm" novalidate>

解决方案

Working StackBlitz with solution

解决方法是在父组件中创建Reactive Form。然后使用Angulars依赖注入,将父组件注入动态组件。

通过将父组件注入动态组件,您将可以访问所有父组件 public 属性,包括反应形式。 该解决方案演示了能够创建并使用响应式表单绑定到动态生成的组件中的输入。

完整代码如下

import {
  Component, ViewChild, OnDestroy,
  AfterContentInit, ComponentFactoryResolver,
  Input, Compiler, ViewContainerRef, NgModule,
  NgModuleRef, Injector, Injectable
} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
  ReactiveFormsModule, FormBuilder,
  FormGroup, FormControl, Validators
} from '@angular/forms';


@Injectable()
export class DynamicControlClass {
  constructor(public Key: string,
    public Validator: boolean,
    public minLength: number,
    public maxLength: number,
    public defaultValue: string,
    public requiredErrorString: string,
    public minLengthString: string,
    public maxLengthString: string,
    public ControlType: string
  ) { }
}

@Component({
  selector: 'app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterContentInit, OnDestroy {
  @ViewChild('dynamicComponent', { read: ViewContainerRef }) _container: ViewContainerRef;
  public ackStringForm: FormGroup;
  public ctlClass: DynamicControlClass[];
  public formErrors: any = {};
  public group: any = {};
  public submitted: boolean = false;

  private cmpRef;

  constructor(
    private fb: FormBuilder,
    private componentFactoryResolver: ComponentFactoryResolver,
    private compiler: Compiler,
    private _injector: Injector,
    private _m: NgModuleRef<any>) {
    this.ctlClass = [
      new DynamicControlClass('formTextField', true, 5, 0, '', 'Please enter a value', 'Must be Minimum of 5 Characters', '', 'textbox')]
  }

  ngOnDestroy() {
    //Always destroy the dynamic component
    //when the parent component gets destroyed
    if (this.cmpRef) {
      this.cmpRef.destroy();
    }
  }

  ngAfterContentInit() {
    this.ctlClass.forEach(dyclass => {
      let minValue: number = dyclass.minLength;
      let maxValue: number = dyclass.maxLength;

      if (dyclass.Validator) {
        this.formErrors[dyclass.Key] = '';

        if ((dyclass.ControlType === 'radio') || (dyclass.ControlType === 'checkbox')) {
          this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || null, [Validators.required]);
        }
        else {
          if ((minValue > 0) && (maxValue > 0)) {
            this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '', [Validators.required, <any>Validators.minLength(minValue), <any>Validators.maxLength(maxValue)]);
          }
          else if ((minValue > 0) && (maxValue === 0)) {
            this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '', [Validators.required, <any>Validators.minLength(minValue)]);
          }
          else if ((minValue === 0) && (maxValue > 0)) {
            this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '', [Validators.required, <any>Validators.maxLength(maxValue)]);
          }
          else if ((minValue === 0) && (maxValue === 0)) {
            this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '', [Validators.required]);
          }
        }
      }
      else {
        this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '');
      }
    });

    this.ackStringForm = new FormGroup(this.group);

    this.ackStringForm.valueChanges.subscribe(data => this.onValueChanged(data));

    this.onValueChanged();

    this.addComponent();
  }

  private addComponent() {
    let template = `  <div style="border: solid; border-color:green;">
                      <p>This is a dynamic component with an input using a reactive form </p>
                      <form [formGroup]="_parent.ackStringForm" class="form-row">
                      <input type="text" formControlName="formTextField"  required> 
                      <div *ngIf="_parent.formErrors.formTextField" class="alert alert-danger">
                      {{ _parent.formErrors.formTextField }}</div>
                      </form><br>
                      <button (click)="_parent.submitForm()"> Submit</button>
                      <br>
                      </div>
                      <br>
                      `;
    @Component({
      template: template,
      styleUrls: ['./dynamic.component.css']
    })
    class DynamicComponent {
      constructor(public _parent: AppComponent) {}
    }
    @NgModule({ 
      imports: [
        ReactiveFormsModule,
        BrowserModule
        ], 
        declarations: [DynamicComponent] 
    })
    class DynamicComponentModule { }

    const mod = this.compiler.compileModuleAndAllComponentsSync(DynamicComponentModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === DynamicComponent
    );
    const component = this._container.createComponent(factory);
  }

  private onValueChanged(data?: any) {
    if (!this.ackStringForm) { return; }
    const form = this.ackStringForm;

    for (const field in this.formErrors) {
      // clear previous error message (if any)
      this.formErrors[field] = '';
      const control = form.get(field);

      if ((control && control.dirty && !control.valid) || (this.submitted)) {

        let objClass: any;

        this.ctlClass.forEach(dyclass => {
          if (dyclass.Key === field) {
            objClass = dyclass;
          }
        });

        for (const key in control.errors) {
          if (key === 'required') {
            this.formErrors[field] += objClass.requiredErrorString + ' ';
          }
          else if (key === 'minlength') {
            this.formErrors[field] += objClass.minLengthString + ' ';
          }
          else if (key === 'maxLengthString') {
            this.formErrors[field] += objClass.minLengthString + ' ';
          }
        }
      }
    }
  }

  public submitForm(){
    let value = this.ackStringForm.value.formTextField;
    alert(value);
  }
}