How to use reactive forms in a dynamic component

对着背影说爱祢 提交于 2019-11-27 15:19:01

The Solution

Working StackBlitz with solution

The solution is to create the Reactive Form in the parent component. Then use Angulars dependency injection and inject the parent component into the Dynamic Component.

By injecting the parent component into the dynamic component you will have access to all of the parents components public properties including the reactive form. This solution demonstrates being able to create and use a Reactive Form to bind to the input in a dynamically generated component.

Full code below

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);
  }
}

If I am reading this correctly, your Template (HTML) is outrunning your component initialization, specifically on the FormGroup. The best way to prevent this from happening is to attach an *ngIf statement to your form on which you have bound your FormGroup. That way it won't render until your FormGroup has been defined.

<form *ngIf="ackStringForm" [formGroup]="ackStringForm" novalidate>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!