3

I have an angular 2 app that heavily uses forms throughout the application. Most of the forms are built using the reactive forms module in Angular, but the API I am working against also have lots of "dynamic fields".

For instance, the "back-end" allows users to create custom fields for certain posts/pages and I want to offer users the ability to use them in my Angular 2 app as well.

Example

The API gives me a JSON list that looks like this:

{
    "id": "the-custom-field-id",
    "label": "The input label",
    "min": 4,
    "max": 8,
    "value": "The current value of the custom field"
},
...

Right now, I fetch the list of custom fields in an observable and use ngfor to loop them and produce form elements for each entry, like this:

<div *ngFor="let cf of _customFields" class="form-group">

    <label>{{cf.label}}</label>

    <input id="custom-field-{{cf.id}}" type="text" class="form-control" value="{{cf.value}}">

</div>

Then on submit, I reach into the DOM (using jQuery) to get the values using the "IDs" of the custom fields.

This is ugly and goes against the idea of not mixing jQuery and Angular.

There must be a way of integrating these dynamic forms into Angular instead, so I can use them with control groups and validation rules?

Glenn Utter
  • 2,313
  • 7
  • 32
  • 44

3 Answers3

4

Yes, indeed there is. Check out Angular 2's Dynamic Forms. The basic jist of it is you create class's(questions) which defined the options for each type of form control you would like to be accessible. So for instance, as an end result, you could have something like:

private newInput;

constructor(){
    // typically you would get your questions from a service/back-end.

    this.newInput = new NumberQuestion({
        key: 'amount',
        label: 'Cash Back',
        value: 21,
        required: true,
        max: 1000,
        min: 10
    });
}

ngOnInit(){
    let control = this.newInput.required ? 
        new FormControl(this.newInput.value, Validators.required)
        : new FormControl(this.newInput.value);

    this.form.addControl(this.newInput.key, control);
}
P. Moloney
  • 721
  • 8
  • 22
  • Awesome! Thank you very much. – Glenn Utter Oct 07 '16 at 07:17
  • But how to handle date picker? In my case, i need to display a date picker control based on the type 'DATE' received from server. – Pulak Kanti Bhattacharyya Mar 01 '17 at 12:21
  • You can use [angular2 DatePipe](http://stackoverflow.com/questions/35754586/how-to-format-date-as-dd-mm-yyyy-in-angular-2-using-pipes) and/or you could convert the Date to a string using [LoDash](https://lodash.com/) or some such method? – P. Moloney Mar 01 '17 at 12:28
1

To create a form that dynamically adds field you need to use FormArray inside the form and add your custom elements there during the runtime. Here's an example of how to dynamically add input fields to allow the user enter more than one email to the form by the click on the button Add Email: https://github.com/Farata/angular2typescript/blob/master/chapter7/form-samples/app/02_growable-items-form.ts

Yakov Fain
  • 11,972
  • 5
  • 33
  • 38
  • 1
    What if you wanted a FormArray of FormControls that each had their own name instead of iterating over them using an index? Instead of an array of email addresses, what if you wanted an array of custom fields retrieved from the API? – wolfhoundjesse Apr 03 '17 at 21:29
0

See also my example here. Is well commented so easy to understand hope. https://stackblitz.com/edit/angular-reactive-form-sobsoft

So this is what we need to maintan dynamic fields in app.component.ts

ngOnInit () {
  // expan our form, create form array this._fb.array
  this.exampleForm = this._fb.group({
      companyName: ['', [Validators.required,
                         Validators.maxLength(25)]],
      countryName: [''],
      city: [''],
      zipCode: [''],
      street: [''],
      units: this._fb.array([
         this.getUnit()
      ])
    });
 }

     /**
       * Create form unit
       */
      private getUnit() {
        const numberPatern = '^[0-9.,]+$';
        return this._fb.group({
          unitName: ['', Validators.required],
          qty: [1, [Validators.required, Validators.pattern(numberPatern)]],
          unitPrice: ['', [Validators.required, Validators.pattern(numberPatern)]],
          unitTotalPrice: [{value: '', disabled: true}]
        });
      }

      /**
       * Add new unit row into form
       */
      private addUnit() {
        const control = <FormArray>this.exampleForm.controls['units'];
        control.push(this.getUnit());
      }

      /**
       * Remove unit row from form on click delete button
       */
      private removeUnit(i: number) {
        const control = <FormArray>this.exampleForm.controls['units'];
        control.removeAt(i);
      }

Now in HTML:

<!-- Page form start -->
  <form [formGroup]="exampleForm" novalidate >

    <div fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="3.5%" fxLayoutAlign="left" >

      <!-- Comapny name input field -->
      <mat-form-field class="example-full-width" fxFlex="75%"> 
        <input matInput placeholder="Company name" formControlName="companyName" required>
        <!-- input field hint -->
        <mat-hint align="end">
          Can contain only characters. Maximum {{exampleForm.controls.companyName.value.length}}/25
        </mat-hint>
        <!-- input field error -->
        <mat-error *ngIf="exampleForm.controls.companyName.invalid">
          This field is required and maximmum alowed charactes are 25
        </mat-error>
      </mat-form-field>

      <!-- Country input field -->
      <mat-form-field class="example-full-width" > 
        <input matInput placeholder="Country" formControlName="countryName">
        <mat-hint align="end">Your IP country name loaded from freegeoip.net</mat-hint>
      </mat-form-field>

    </div>

    <div fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="3.5%" fxLayoutAlign="center" layout-margin>

      <!-- Street input field -->
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Street" fxFlex="75%" formControlName="street">
      </mat-form-field>

      <!-- City input field -->
      <mat-form-field class="example-full-width" > 
        <input matInput placeholder="City" formControlName="city">
        <mat-hint align="end">City name loaded from freegeoip.net</mat-hint>
      </mat-form-field>

      <!-- Zip code input field -->
      <mat-form-field class="example-full-width" fxFlex="20%"> 
        <input matInput placeholder="Zip" formControlName="zipCode">
        <mat-hint align="end">Zip loaded from freegeoip.net</mat-hint>
      </mat-form-field>

  </div>
  <br>

  <!-- Start form units array with first row must and dynamically add more -->
  <mat-card formArrayName="units">
    <mat-card-title>Units</mat-card-title>
    <mat-divider></mat-divider>

    <!-- loop throught units -->
    <div *ngFor="let unit of exampleForm.controls.units.controls; let i=index">

      <!-- row divider show for every nex row exclude if first row -->
      <mat-divider *ngIf="exampleForm.controls.units.controls.length > 1 && i > 0" ></mat-divider><br>

      <!-- group name in this case row index -->
      <div [formGroupName]="i">
        <div fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="3.5%" fxLayoutAlign="center">

          <!-- unit name input field -->
          <mat-form-field  fxFlex="30%"> 
            <input matInput placeholder="Unit name" formControlName="unitName" required>              
          </mat-form-field>

          <!-- unit quantity input field -->
          <mat-form-field  fxFlex="10%"> 
            <input matInput placeholder="Quantity" type="number" formControlName="qty" required>
          </mat-form-field>

          <!-- unit price input field -->
          <mat-form-field  fxFlex="20%"> 
            <input matInput placeholder="Unit price" type="number" formControlName="unitPrice" required>
          </mat-form-field>

          <!-- unit total price input field, calculated and not editable -->
          <mat-form-field > 
            <input matInput placeholder="Total sum" formControlName="unitTotalPrice">
          </mat-form-field>

          <!-- row delete button, hidden if there is just one row -->
          <button mat-mini-fab color="warn" 
                  *ngIf="exampleForm.controls.units.controls.length > 1" (click)="removeUnit(i)">
              <mat-icon>delete forever</mat-icon>
          </button>
        </div>
      </div>
    </div>

    <!-- New unit button -->
    <mat-divider></mat-divider>
    <mat-card-actions>
      <button mat-raised-button (click)="addUnit()">
        <mat-icon>add box</mat-icon>
        Add new unit
      </button>
    </mat-card-actions>
  </mat-card> <!-- End form units array -->    
  </form> <!-- Page form end -->
lubo08
  • 315
  • 4
  • 11