5

背景

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

尝试1:

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

尝试2:

这一次,我尝试利用 Angulars NgTemplateOutlet 将 HTML 动态添加到组件中,然后创建并绑定到响应式表单。起初这似乎是一个很好的解决方案,但最终为了让 html 呈现它需要使用[innerHTML]属性,再次使该方法无用(如我第一次尝试中所述)

尝试 3:

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

问题

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

尝试4:

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

通过使用此方法,显示动态组件 HTML,但出现新错误:

“错误错误:formGroup 需要一个 FormGroup 实例。请传入一个。”

StackBlitz 错误场景

4

2 回答 2

3

解决方案

使用解决方案使用 StackBlitz

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

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

完整代码如下

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);
  }
}
于 2018-02-13T03:10:36.160 回答
0

如果我没看错,您的模板 (HTML) 超出了您的组件初始化速度,特别是在FormGroup. 防止这种情况发生的最好方法是*ngIf在您绑定了FormGroup. FormGroup这样,在你被定义之前它不会渲染。

<form *ngIf="ackStringForm" [formGroup]="ackStringForm" novalidate>
于 2018-02-12T19:04:08.240 回答