How to use reactive forms in a dynamic component

后端 未结 2 1725
难免孤独
难免孤独 2020-12-03 19:49

Background

I receive client generated data from the server that contains HTML that I then use to create a dynamic component that gets injected and displayed in our c
相关标签:
2条回答
  • 2020-12-03 20:04

    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);
      }
    }
    
    0 讨论(0)
  • 2020-12-03 20:08

    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>
    
    0 讨论(0)
提交回复
热议问题