How to use reactive forms in a dynamic component

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

Leave a Comment