2

I am new to angular 2 and I am struggling to get the values from Dynamic HTML. My requirement is I will have some Form Input and I need to inject Dynamic HTML in between which will contain some more inputs.

I have used the example from @Rene Hamburger and created the Dynamic Form Input.

If you look into the example It has 3 Inputs 2 in the Template (Name, LastName). I am injecting the address using addcomponent.

I am using Form Builder to build all the 3 controls, but when you click submit you could see the values Name & Last Name shows up, but could not get the values of address.

I am not now sure how to get the values. I am requesting the community gurus to help me out.

http://plnkr.co/edit/fcS1hdfLErjgChcFsRiX?p=preview

app/app.component.ts

import {AfterViewInit,OnInit, Compiler, Component, NgModule, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { FormGroup, FormControl, FormArray, FormBuilder, Validators } from '@angular/forms';
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";

@Component({
  selector: 'my-app',
  template: `
    <h1>Dynamic template:</h1>

    <form [formGroup]="myForm" (ngSubmit)="onSubmit()">
     <div  class="form-row">
      <label for="">Name</label>
      <input type="text" class="form-control" formControlName="name">
            <small [hidden]="myForm.controls.name.valid || (myForm.controls.name.pristine && !submitted)" class="text-danger">
            Name is required (minimum 5 characters).
          </small>
    </div>

        <div  class="form-row">
      <label for="">Last Name</label>
      <input type="text" class="form-control" formControlName="lastname">
            <small [hidden]="myForm.controls.name.valid || (myForm.controls.name.pristine && !submitted)" class="text-danger">
            Name is required (minimum 5 characters).
          </small>
    </div>

       <div #container></div>

      <div class="form-row">
      <button type="submit">Submit</button>
      </div>
       <div *ngIf="payLoad" class="form-row">
            <strong>Saved the following values</strong><br>{{payLoad}}
        </div>


    </form>
  `,
})
export class AppComponent implements OnInit , AfterViewInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
  public myForm: FormGroup; // our model driven form
  public payLoad: string;

    public onSubmit() {
        this.payLoad = JSON.stringify(this.myForm.value);
    }

  constructor(private compiler: Compiler,private formBuilder: FormBuilder,private sanitizer: DomSanitizer) {}

ngOnInit() {
      this.myForm = this.formBuilder.group({
            name: ['', [<any>Validators.required, <any>Validators.minLength(5)]],
            lastname: ['', [<any>Validators.required, <any>Validators.minLength(5)]],
            address: ['', [<any>Validators.required, <any>Validators.minLength(5)]]
            });
}
  ngAfterViewInit() {

    this.addComponent('<div  class="form-row"> <label for="">Address</label> <input type="text" class="form-control" formControlName="address">  </div>');
  }

  private addComponent(template: string) {
    @Component({template: template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
  }
}

The Plunker does not work, so I added the example in stackbliz.

https://stackblitz.com/edit/angular-t3mmg6

This example is dynamic Form controls is in add component (This is where you can get the Formcontrols from the server). If you see addcomponent method you can see the Forms Controls. In this example I am not using angular material,but It works (I am using @ work). This is target to angular 6, but works in all previous version.

Need to add JITComplierFactory for AngularVersion 5 and above.

Thanks

Vijay

2 Answers 2

1

The problem is that you add the group address to the formbuilder groups in the parent component but the html is added as a child component which cannot update your formgroup values.

Using the parent-child approach, you need to output the change of the value from the child component to the parent component when the value change and then set the value of your form group manually, take a look here for some different ways of communicating between parent-child components: https://angular.io/docs/ts/latest/cookbook/component-communication.html

To me, it looks easier if you could use ngFor or ngIf directives to control your dynamic form instead of adding child components. Take a look here for an example of how to do this: https://angular.io/docs/ts/latest/cookbook/dynamic-form.html

Sign up to request clarification or add additional context in comments.

13 Comments

@hanger thanks for the reply, but my situation is that I dont have the luxury of working here with dynamic forms. The html comes from the table which need to be manipulated to add inputs to be presented to the user to fill it in. I do understand i added the form group in the top, I am not sure how to handle it after the dynamic HTML is displayed. I have worked on dynamic forms, but dynamic forms is not going to work since the content of Forms Inputs are part of html from database and need to display it dynamically.
If you get html from your database you can bind the innerHtml property of a div to your html string to have the html displayed. But if this solution does not work then you need to use one of the ways to communicate between parent and child that is outlined in the angular cookbook in the original answer.
I tried using inner html to bind and it works fine and it shows the html and the input values, but the problem is I am not able to get the values back to parent. That is where i need help.
Ok, would you mind updating your post to show how you did when binding to inner html. To be clear - I talk about updating your template with a div like this: <div [innerHtml]="htmlString"></div>. There is no parent-child relationship with this approach because the html string and the values will all be on the parent.
Here you go. This is using div InnerHTML. I am using safeHTML so the values Input fields are not stripeed plnkr.co/edit/Pgd81dg5tBuraSXkDBe6?p=preview When you add some values and submit, you will not get the values for address. This is the problem I am having
|
1

My Colleague (Justin) helped me on how to access the Form Values from the Dynamic HTML. @Hagner (http://plnkr.co/edit/DeYGuZSOYvxT76YI8SRU?p=preview) answer was one way in which you can do. This involves Services. The method below does not involve service and is is more straight forward way to access the values. I thought I will post for those with these scenarios.

-- app/app.component.ts

    import {
  AfterContentInit, AfterViewInit, AfterViewChecked, OnInit, Compiler, Component, NgModule, ViewChild,
  ViewContainerRef, forwardRef, Injectable, ChangeDetectorRef
} from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { ReactiveFormsModule, FormGroup, FormControl, FormsModule, FormArray, FormBuilder, Validators } from '@angular/forms';
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";

@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: 'my-app',
  template: `
    <h1>Dynamic template:</h1>

<div class="container">
    <form [formGroup]="myForm" (ngSubmit)="onSubmit()" novalidate>



     <div  class="form-row">
      <label for="">First Name</label>
      <input type="text" class="form-control" formControlName="firstname" required>
              <div *ngIf="formErrors.firstname" class="alert alert-danger">
                {{ formErrors.firstname }}
              </div>

    </div>

        <div  class="form-row">
      <label for="">Last Name</label>
      <input type="text" class="form-control" formControlName="lastname"  required>

                            <div *ngIf="formErrors.lastname" class="alert alert-danger">
                {{ formErrors.lastname }}
              </div>
      </div>

       <div #container></div>
<!--
<div  class="form-row">

    <input type="radio" formControlName="Medical_Flu_Concent_Decline_medical_flu_concent_decline_rule1" value="1"> <b>Concent Template </b>
    <br>
    <input type="radio" formControlName="Medical_Flu_Concent_Decline_medical_flu_concent_decline_rule1" value="2"> <b>Decline  Template</b>
</div>
-->

       <br>
       <!--
            <button type="submit" class="btn btn-default"
             [disabled]="!myForm.valid">Submit</button>
       -->

                   <button type="submit" class="btn btn-default" >Submit</button>

       <div *ngIf="payLoad" class="form-row">
            <strong>Saved the following values</strong><br>{{payLoad}}
        </div>

        <div> Is Form Valid : {{myForm.valid}}</div>
    </form>

    </div>
  `
  ,
  styles: ['h1 {color: #369;font-family: Arial, Helvetica, sans-serif;font-size: 250%;} input[required]:valid {border-left: 5px solid #42A948; /* green */ } input[required]:invalid {border-left: 5px solid #a94442; /* red */ } .radioValidation input:invalid{outline: 2px solid  #a94442;} .radioValidation input:valid{outline: 2px solid #42A948;}'],

})
export class AppComponent implements AfterContentInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
  public myForm: FormGroup; // our model driven form
  public payLoad: string;
  public controlData: [string, boolean, number];
  public ctlClass: DynamicControlClass[];
  public formErrors: any = {};
  public group: any = {};
  public submitted: boolean = false;
  public setValidatorValue: boolean = false;

  public onSubmit() {

    this.submitted = true;
    this.setValidatorValue = false;
    this.onValueChanged();

    if (this.myForm.valid) {

      const form = this.myForm

      const control = form.get('Medical_Flu_Concent_Decline_medical_flu_concent_decline');

      if (control) {
        if (control.value === '1') {
          const controlreset = form.get('Medical_Flu_Decline_Details_medical_flu_decline_details');

          if ((controlreset) && (controlreset.value)) {
            this.myForm.patchValue({ Medical_Flu_Decline_Details_medical_flu_decline_details: null });
          }
        }
      }
      this.payLoad = JSON.stringify(this.myForm.value);
    }

  }

  constructor(private compiler: Compiler, private formBuilder: FormBuilder, private sanitizer: DomSanitizer) {

    this.ctlClass = [
      new DynamicControlClass('firstname', true, 5, 0, '', 'Please enter First Name', 'First Name must be Minimum of 5 Characters', '', 'textbox'),
      new DynamicControlClass('lastname', true, 5, 0, '', 'Please enter LastName', 'Last Name must be Minimum of 5 Characters', '', 'textbox'),
      new DynamicControlClass('address', true, 5, 0, 'Default Address', 'Please enter Address', 'Address must be Minimum of 5 Characters', '', 'textbox'),
      new DynamicControlClass('Medical_Flu_Concent_Decline_medical_flu_concent_decline', true, 0, 0, null, 'Please Select one of the Radio option', '', '', 'radio'),
      new DynamicControlClass('Medical_Flu_Decline_Details_medical_flu_decline_details', false, 0, 0, null, 'Please Select one of the Decline option', '', '', 'radio'),
      new DynamicControlClass('city', true, 5, 0, 'Enter City', 'Please enter City', 'City must be Minimum of 5 Characters', '', 'textbox')]
  };



  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 {

        if (dyclass.Key === 'Medical_Flu_Decline_Details_medical_flu_decline_details') {
          this.formErrors[dyclass.Key] = 'null';
          this.group[dyclass.Key] = new FormControl(dyclass.defaultValue);
        }
        else {
          this.group[dyclass.Key] = new FormControl(dyclass.defaultValue || '');
        }


      }



    });

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

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

    this.onValueChanged(); // (re)set validation messages now


    this.addComponent('<div [formGroup]="_parent.myForm" class="form-row">  <label for="">Address</label> <input type="text" class="form-control" formControlName="address"  required> <div *ngIf="_parent.formErrors.address" class="alert alert-danger">{{ _parent.formErrors.address }}</div><\div><div [formGroup]="_parent.myForm" class="form-row">   <label for="">City</label> <input type="text" class="form-control" formControlName="city"  required> <div *ngIf="_parent.formErrors.city" class="alert alert-danger">{{ _parent.formErrors.city }}</div><\div><div  [formGroup]="_parent.myForm" class="form-row radioValidation" > <input type="radio" formControlName="Medical_Flu_Concent_Decline_medical_flu_concent_decline"  id="Medical_Flu_Concent_Decline_medical_flu_concent_decline_1" name="Medical_Flu_Concent_Decline_medical_flu_concent_decline" value="1" required> <b>CONSENT.</b><br><br> Here is my Consent. <br><br><input type="radio"  formControlName="Medical_Flu_Concent_Decline_medical_flu_concent_decline" name="Medical_Flu_Concent_Decline_medical_flu_concent_decline" id="Medical_Flu_Concent_Decline_medical_flu_concent_decline_2" value="2" required> <b>DECLINE. </b><br/> I am  choosing to decline for the following reason: <br><br> <div *ngIf="_parent.formErrors.Medical_Flu_Concent_Decline_medical_flu_concent_decline" class="alert alert-danger">{{ _parent.formErrors.Medical_Flu_Concent_Decline_medical_flu_concent_decline }}</div></div><div [formGroup]="_parent.myForm" class="form-row"> <input type="radio" formControlName="Medical_Flu_Decline_Details_medical_flu_decline_details" id="Medical_Flu_Decline_Details_medical_flu_decline_details_1"   name="Medical_Flu_Decline_Details_medical_flu_decline_details"   value="1"  > I am not interested<br><br><input type="radio" formControlName="Medical_Flu_Decline_Details_medical_flu_decline_details" id="Medical_Flu_Decline_Details_medical_flu_decline_details_2"   name="Medical_Flu_Decline_Details_medical_flu_decline_details"   value="2"  > I have already received <br><br><input type="radio" formControlName="Medical_Flu_Decline_Details_medical_flu_decline_details" id="Medical_Flu_Decline_Details_medical_flu_decline_details_3"   name="Medical_Flu_Decline_Details_medical_flu_decline_details"   value="3"  > I am declining for other reasons<br><br><div *ngIf="_parent.formErrors.Medical_Flu_Decline_Details_medical_flu_decline_details" class="alert alert-danger">{{ _parent.formErrors.Medical_Flu_Decline_Details_medical_flu_decline_details }}</div></div>');




  }

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

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


      if (field === 'Medical_Flu_Decline_Details_medical_flu_decline_details') {
        if ((this.myForm.value['Medical_Flu_Concent_Decline_medical_flu_concent_decline']) && (this.myForm.value['Medical_Flu_Concent_Decline_medical_flu_concent_decline'] === "2")) {
          control.setValidators(Validators.required);
          control.updateValueAndValidity({ onlySelf: false, emitEvent: false })
        }
        else if ((this.myForm.value['Medical_Flu_Concent_Decline_medical_flu_concent_decline']) && (this.myForm.value['Medical_Flu_Concent_Decline_medical_flu_concent_decline'] === "1")) {
          control.setValidators(null);
          control.updateValueAndValidity({ onlySelf: false, emitEvent: false })

          const controlreset = form.get('Medical_Flu_Decline_Details_medical_flu_decline_details');

          if ((controlreset) && (controlreset.value)) {
            this.myForm.patchValue({ Medical_Flu_Decline_Details_medical_flu_decline_details: null });
          }          

        }
      }



      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 + ' ';
          }
        }



      }
    }

  }



  private addComponent(template: string) {
    @Component({
      template: template,
      styles: ['h1 {color: #369;font-family: Arial, Helvetica, sans-serif;font-size: 250%;} input[required]:valid {border-left: 5px solid #42A948; /* green */ } input[required]:invalid {border-left: 5px solid #a94442; /* red */ } .radioValidation input:invalid{outline: 2px solid  #a94442;} .radioValidation input:valid{outline: 2px solid #42A948;}'],


      // alternatively:  [{provide: TemplateContainer, useExisting: forwardRef(() => AppComponent)}]
    })
    class TemplateComponent {
      constructor(public _parent: AppComponent) {

        console.log("parent component", this._parent);

      }
    }
    @NgModule({ imports: [ReactiveFormsModule, FormsModule, BrowserModule], declarations: [TemplateComponent] })
    class TemplateModule { }

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
  }
}



-- app/app.module.ts


import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { COMPILER_PROVIDERS } from '@angular/compiler';

import { AppComponent }   from './app.component';


@NgModule({
  imports:      [BrowserModule, ReactiveFormsModule],
  declarations:  [AppComponent],
  bootstrap:    [ AppComponent ]
})

export class AppModule { }


-- app/main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

const platform = platformBrowserDynamic();

platform.bootstrapModule(AppModule);


-- config.js

System.config({
  //use typescript for compilation
  transpiler: 'typescript',
  //typescript compiler options
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  paths: {
    'npm:': 'https://unpkg.com/'
  },
  //map tells the System loader where to look for things
  map: {

    'app': 'app',

      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',

      // angular testing umd bundles
      '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
      '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
      '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
      '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
      '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
      '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
      '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
      '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',

      // other libraries
      'rxjs':                       'npm:rxjs',
      'lodash':                       'npm:lodash/lodash.min.js',
      'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
      'ts':                         'npm:plugin-typescript/lib/plugin.js',
      'typescript':                 'npm:typescript/lib/typescript.js',
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    },
    rxjs: {
      defaultExtension: 'js'
    }
  }
});

-- index.html

<!DOCTYPE html>
<html>

  <head>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css">

<script src="https://unpkg.com/[email protected]/dist/zone.js"></script>
<script src="https://unpkg.com/[email protected]/Reflect.js"></script>
<script src="https://unpkg.com/[email protected]/dist/system.js"></script>
<script src="https://unpkg.com/[email protected]/lib/typescript.js"></script>
<script src="config.js"></script>

    <script src="config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>     
  </head>

  <body>
    <my-app>Loading...</my-app>
  </body>

</html>

http://plnkr.co/edit/rELaWPJ2cDJyCB55deTF?p=preview

Full Credit to Justin for helping me out.

1 Comment

If you want to work with AOT here is what you need to do import { JitCompilerFactory } from '@angular/compiler'; private compiler: Compiler = new JitCompilerFactory([{ useDebug: false, useJit: true }]).createCompiler(); Create complier @ where you need. Remove the complier from constructor. This will work with AOT. Updated the plnkr to work with AOT Compilation plnkr.co/edit/2qAWGh?p=preview

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.