Reputation: 2363
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?
Upvotes: 3
Views: 8298
Reputation: 315
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 -->
Upvotes: 0
Reputation: 721
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);
}
Upvotes: 4
Reputation: 12376
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
Upvotes: 1