Angular 10, reactive forms with dynamic rows, validation and value changes stream.
This is a complex angular 10 reactive forms guide with the example on stackblitz.com.
In this example will be described these techniques:
- Create a reactive form
- Reactive form validation
- Reactive form values initialisation
- How to listen to form value changes
- How to create dynamic rows with form array
- How to use currency pipe in HTML and in component
1. Prepare module dependencies
Prepare modules, look at this app.module.ts in the example.
// This dependencies are basic and a must BrowserModule FormsModule // This dependencies you need: ReactiveFormsModule CurrencyPipe // This dependencies are optional used in example // and you not need it for your project Ng2Webstorage MaterialModule // see in example FlexLayoutModule HttpModule BrowserAnimationsModule
Prepare constructor in app.component.ts as an example.
// import components
import {Validators,
FormBuilder,
FormGroup,
FormControl,
FormArray} from '@angular/forms';
import {CurrencyPipe} from '@angular/common';
// put into component constructor FormBuilder an CurrencyPipe
constructor(private _fb: FormBuilder,
private currencyPipe: CurrencyPipe
) { }
2. Create a form with form builder.
Here I will describe app.component.ts code on how to initialise reactive form and create first input. See the final code here.
// insert your form group declaration
public exampleForm: FormGroup;
// now on init build form component
ngOnInit () {
// create form with validators // this._fb is form builder declared in constructor
this.exampleForm = this._fb.group({
companyName: ['']
});
}
Now we could create an HTML form, in this example is used angular material components bud fell free to use it with any another layout or components or even plain HTML. See in app.component.html.
Important is to tell form which group belongs to [formGroup]="exampleForm". Next important is pair component inside form like inputs, selects etc. within-group components formControlName="companyName".
// inser into content section in your html.
<form [formGroup]="exampleForm" novalidate ><!-- 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
</mat-error></mat-form-field>
</form>
now we should see our firs input inside a form.
3. Reactive form validation.
Validations with reactive forms are really simple. With the angular Validators component, we could cover almost all situations.
// just put validators as secon paramter of component, // could be one or more validators in array // required and max length validators companyName: ['', [Validators.required,Validators.maxLength(25)] ], // patern validator example const numberPatern = '^[0-9.,]+$'; unitPrice: ['', [Validators.required, Validators.pattern(numberPatern)]]
now in HTML based on our component, we could add an error message for a form field.
<mat-error *ngIf="exampleForm.controls.companyName.invalid"> This field is required </mat-error>
4. Initialise the value of form components.
With form builder is very simple to initialise the value of our inputs.
// could be part of init or based on any another action, in example
// is initialized after http request finished.
this.exampleForm.patchValue({companyName: 'example inc.'});
5. Listen to form value changes.
// we could simpli subcribe to our value changes right after form init
// initialize stream
const myFormValueChanges$ =
this.exampleForm.controls['units'].valueChanges;
// subscribe to the stream
myFormValueChanges$.subscribe(
units => this.updateTotalUnitPrice(units));
This way we could listen to the whole form or just to one control in the form.
6. Dynamic rows inside a form.
This is a case when we need to dynamically create rows inside a form. For example, we need to allow a user to create more addresses or accounts or invoice units. Of course, you could separate it to another screen or use modals or any other way but sometimes it is this way more comfortable.
// 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()
])
});
// now we create some service methods for create, add and remove
// row inside form
// create form row.
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 row
private addUnit() {
const control = <FormArray>this.exampleForm.controls['units'];
control.push(this.getUnit());
}
// remove row
private removeUnit(i: number) {
const control = <FormArray>this.exampleForm.controls['units'];
control.removeAt(i);
}
now we can modify our 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 --><br>
Of course, we need also to do some calculations for prices. So we subscribe to changes listener to units and now one important thing. When we are updating input that is subscribed we have to stop triggering this listener so we disable it with option parameter {emitEvent: false} otherwise you get infinite loop troubles.
control.at(+i).get('unitTotalPrice')
.setValue(totalUnitPriceFormatted,
{onlySelf: true, emitEvent: false});
7. Currency pipe.
There are at least two situations when you need to use pipes. One is inside HTML and one inside component.
// first one inside HTML
<!-- Total price calculation formated with angular currency pipe -->
<mat-card>
{{ totalSum | currency:'USD':'symbol-narrow':'1.2-2'}}
</mat-card>
// second one inside component.
let totalUnitPriceFormatted =
this.currencyPipe.transform(
totalUnitPrice, 'USD', 'symbol-narrow', '1.2-2');
Very detailed tutorial and for learning. I want to know did you do the edit/update the same form after submitted. It seems very critical to do that. May I know if you have done anything like this?
Great article help me a lot, but I have a question. When i add a new unit it trigger the validation??? anyone can help?
but when i submit same unit name show me validation error unit name is duplicate. please reply me
great article