Angular Form
Useful Basic FormControl Members
Name | Description |
---|---|
value | return the current value of the form control, defined using the any type. |
setValue(value) | sets the value of the form control. |
valueChanges | returns an Observable<any>, through which changes can be observed. |
enabled | returns true if the form control is enabled. |
disabled | returns true if the form control is disabled. |
enable() | enables the form control. |
disable() | disables the form control. |
reset(value) | resets the form control, with an optional value. The form control will be reset to its default state if the value argument is omitted. |
Managing Control State
Name | Description |
---|---|
untouched | returns true if the HTML element is untouched, meaning that the element has not been selected. |
touched | This property returns true if the HTML element has been touched, meaning that the element has been selected. |
markAsTouched() | marks the element as touched. |
markAsUntouched() | marks the element as untouched. |
pristine | returns true if the element contents have not been edited by the user. |
dirty | returns true if the element contents have been edited by the user |
markAsPristine() | marks the element as pristine. |
markAsDirty() | marks the element as dirty |
FormControl Members for Managing Validators
Name | Description |
---|---|
validator | returns a function that combines all of the configured validators so that the form control can be validated with a single function call |
hasValidator(v) | returns true if the form control has been configured with the specified validator. |
setValidators(v) | sets the form control validators. The argument can be a single validator or an array of validators |
addValidators(v) | adds one or more validators to the form control |
removeValidators(v) | removes one or more validators from the control |
clearValidators() | removes all of the validators from the form control |
FormControl Members for Managing Validation State
- status: VALID, INVALID, PENDING, or DISABLED
- statusChanges: returns an
Observable<FormControlStatus>
, which will emit a FormControlStatus value when the state of the form control changes - valid
- invalid
- pending: returns true if the form control’s value is being validated asynchronously
- errors: returns a ValidationErrors object that contains the errors generated by the form control’s validators, or null if there are no errors.
- getError(v): returns the error message, if there is one, for the specified validator. This method accepts an optional path for use with nested form controls
- hasError(v): returns true if the specified validator has generated an error message. This method accepts an optional path for use with nested form controls
- setErrors(errs): This method is used to add errors to the form control’s validation status, which is useful when performing manual validation in the component. This method accepts an optional path for use with nested form controls,
Simple Form - FormControl
export class FormComponent {
nameField = new FormControl('', {
validators: [Validators.required, Validators.minLength(3), Validators.pattern('^[A-Za-z ]+$')],
updateOn: 'change', // change(default), blur, submit
nonNullable: true,
});
ngOnInit() {
this.nameField.statusChanges.subscribe((newStatus) => {
if (newStatus == 'INVALID' && this.nameField.errors != null) {
const errs = Object.keys(this.nameField.errors).join(', ');
this.messageService.reportMessage(new Message(`INVALID: ${errs}`));
} else {
this.messageService.reportMessage(new Message(newStatus));
}
});
// this.nameField.valueChanges.subscribe((newValue) => {
// this.messageService.reportMessage(new Message(newValue || '(Empty)'));
// });
}
}
app/shared/validation/validationHelper.pipe.ts:
import {Pipe} from '@angular/core';
import {FormControl, ValidationErrors} from '@angular/forms';
@Pipe({name: 'validationFormat'})
export class ValidationHelper {
transform(source: any, name: any): string[] {
if (source instanceof FormControl) {
return this.formatMessages((source as FormControl).errors, name);
}
return this.formatMessages(source as ValidationErrors, name);
}
formatMessages(errors: ValidationErrors | null, name: string): string[] {
let messages: string[] = [];
for (let errorName in errors) {
switch (errorName) {
case 'required':
messages.push(`You must enter a ${name}`);
break;
case 'minlength':
messages.push(`A ${name} must be at least ${errors['minlength'].requiredLength} characters`);
break;
case 'pattern':
messages.push(`The ${name} contains illegal characters`);
break;
}
}
return messages;
}
}
app/shared/shared.module.ts:
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {ValidationHelper} from './validation/validationHelper.pipe';
@NgModule({
declarations: [ValidationHelper],
imports: [CommonModule],
exports: [ValidationHelper],
})
export class SharedModule {}
form.component.html:
<div class="form-group">
<label>Name</label>
<input class="form-control" name="name" [formControl]="nameField" #name="ngForm" />
<ul class="text-danger list-unstyled mt-1" *ngIf="name.dirty && name.invalid">
+ <li *ngFor="let err of name.errors | validationFormat:'name'">{{ err }}</li>
</ul>
</div>
FormGroup Members for Adding and Removing Controls
Name | Description |
---|---|
addControl(name, ctrl) | adds a control to the FormGroup with the specified name. No action is taken if there is already a control with this name |
setControl(name, ctrl) | adds a control to the FormGroup with the specified name, replacing any existing control with this name |
removeControl(name) | removes the control with the specified name |
controls | returns a map containing the controls in the group, using their names as keys |
get(name) | returns the control with the specified name |
FormGroup Methods for Managing Control Values
Name | Description |
---|---|
value | returns an object containing the values of the form controls in the group, using the names given to each control as the names of the properties. |
setValue(val) | sets the contents of the form controls using an object, whose property names correspond to the names given to each control. The specified value object must define properties for all the form controls in the group. |
patchValue(val) | sets the contents of the form controls using an object, whose property names correspond to the names given to each control. Unlike the setValue method, values are not required for all form controls. |
reset(val) | resets the form to its pristine and untouched state and uses the specified value to populate the form controls. |
Displaying Validation Messages with a Form Group
Name | Description |
---|---|
getError(v, path) | returns the error message, if there is one, for the specified validator. The optional path argument is used to identify the control. |
hasError(v, path) | returns true if the specified validator has generated an error message. The optional path argument is used to identify the control. |
form.getError("required", "category")
will return category control's required error message if any. But this is not
very useful. Let's create a reusable and custom structural directives to display all errors for a control.
Custom directive to display all errors for a control ❤️
Create src/app/shared/validation/validationErrors.directive.ts and import/export in the shared.module:
import {Directive, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {ValidationHelper} from './validationHelper';
/**
* A structural directive to generate validation errors for a formGroup field control
*/
@Directive({selector: '[validationErrors]'})
export class ValidationErrorsDirective implements OnInit {
constructor(private container: ViewContainerRef, private template: TemplateRef<unknown>) {}
@Input('validationErrorsControl') name = ''; // control: 'xxx' from <li *validationErrors="productForm; control: 'name'; let err">
@Input('validationErrorsLabel') label?: string; // label: 'xxx'
@Input('validationErrors') formGroup?: FormGroup; // e.g. productForm
ngOnInit() {
const formatter = new ValidationHelper();
if (this.formGroup && this.name) {
const control = this.formGroup?.get(this.name);
if (control) {
control.statusChanges.subscribe(() => {
if (this.container.length > 0) {
this.container.clear();
}
if (control && control.dirty && control.invalid && control.errors) {
formatter.formatMessages(control.errors, this.label ?? this.name).forEach((err) => {
this.container.createEmbeddedView(this.template, {$implicit: err});
});
}
});
}
}
}
}
Now our form.component.html can use the directive to show all errors for a specific control:
<input class="form-control" formControlName="price" />
<ul class="text-danger list-unstyled mt-1">
+ <li *validationErrors="productForm; control: 'price'; let err">{{ err }}</li>
</ul>
Nested FormGroup and Validation
export interface ContactFormGroup {
name: FormControl<string>;
email: FormControl<string>;
category: FormControl<string>;
price: FormControl<string>;
details: FormGroup<{
supplier: FormControl<string>;
keywords: FormControl<string>;
}>;
contactNumber?: FormControl<number | null>; //? makes controls as optional
}
productForm = new FormGroup<ContactFormGroup>({
name: new FormControl<string>('', {
validators: [Validators.required, Validators.minLength(3), Validators.pattern('^[A-Za-z ]+$')],
updateOn: 'change',
nonNullable: true
}),
email: new FormControl<string>('', {nonNullable: false}),
category: new FormControl('', {validators: Validators.required}),
price: new FormControl('', {validators: [Validators.required, Validators.pattern('^[0-9.]+$')]}),
+ details: new FormGroup({
+ supplier: new FormControl('', {validators: Validators.required}),
+ keywords: new FormControl('', {validators: Validators.required}),
+ }),
});
<ng-container formGroupName="details">
<div class="form-group">
<label>Supplier</label>
<input class="form-control" formControlName="supplier" />
<ul class="text-danger list-unstyled mt-1">
<li *validationErrors="productForm; control: 'details.supplier'; label: 'supplier'; let err">{{ err }}</li>
</ul>
</div>
<div class="form-group">
<label>Keywords</label>
<input class="form-control" formControlName="keywords" />
<ul class="text-danger list-unstyled mt-1">
<li *validationErrors="productForm; control: 'details.keywords'; label: 'keyword'; let err">{{ err }}</li>
</ul>
</div>
</ng-container>
FormArray - Creating Form Components Dynamically
The FormGroup
class is useful when the structure and number of elements in the form are known in advance. For
applications that need to dynamically add and remove elements, Angular provides the FormArray class. Both
FormArray
and FormControl
are derived from the AbstractControl
class and provide the same features for managing
FormGroup objects; the difference is that the FormArray
class allows FormControl
objects to be created without
specifying names and stores its controls as an array, making it easier to add and remove controls.
Useful FormArray Members for Managing Controls
controls
: This property returns an array containing the child controls.length
: This property returns the number of controls that are in the FormArray.at(index)
push(control)
insert(index, control)
setControl(index, control)
removeAt(index)
: This method replaces the control at the specified index.clear()
The FormArray Methods for Setting Values
setValue(values)
patchValue(values)
reset(values)
Now let's change keywords type to string[]
.
export class Product {
constructor(
public id?: number,
public name?: string,
public category?: string,
public price?: number,
public details?: Details,
) {}
}
export class Details {
+ constructor(public supplier?: string, public keywords?: string[]) {}
}
export class ProductDataSource {
constructor() {
this.data = new Array<Product>(
+ new Product(1, 'Kayak', 'Watersports', 275, {supplier: 'Acme', keywords: ['boat', 'small']}),
+ new Product(2, 'Lifejacket', 'Watersports', 48.95, {supplier: 'Smoot Co', keywords: ['safety']}),
new Product(3, 'Soccer Ball', 'Soccer', 19.5),
);
}
}
export class FormComponent implements OnInit {
+ keywordGroup = new FormArray([this.createKeywordFormControl()]);
productForm = new FormGroup({
name: new FormControl('', {
validators: [Validators.required, Validators.minLength(3), Validators.pattern('^[A-Za-z ]+$')],
updateOn: 'change',
}),
category: new FormControl('', {validators: Validators.required}),
price: new FormControl('', {validators: [Validators.required, Validators.pattern('^[0-9.]+$')]}),
details: new FormGroup({
supplier: new FormControl('', {validators: Validators.required}),
- keywords: new FormControl('', {validators: Validators.required}),
+ keywords: this.keywordGroup,
}),
});
createKeywordFormControl(): FormControl {
+ return new FormControl('', {validators: Validators.pattern('^[A-Za-z ]+$')});
}
+ addKeywordControl() {
+ this.keywordGroup.push(this.createKeywordFormControl());
+ }
+ removeKeywordControl(index: number) {
+ this.keywordGroup.removeAt(index);
+ }
handleStateChange(newState: StateUpdate) {
this.editing = newState.mode == MODES.EDIT;
+ this.keywordGroup.clear();
if (this.editing && newState.id) {
Object.assign(this.product, this.repository.getProduct(newState.id) ?? new Product());
+ // create keyword input, whose count is based on the product keywords
+ this.product.details?.keywords?.forEach((val) => {
+ this.keywordGroup.push(this.createKeywordFormControl());
+ });
} else {
this.product = new Product();
}
+ // at least create one keyword input
+ if (this.keywordGroup.length == 0) {
+ this.keywordGroup.push(this.createKeywordFormControl());
+ }
+ // populate or clear the form controls when user clicks the Create New Product or Edit button:
+ this.productForm.reset(this.product);
}
submitForm() {
if (this.productForm.valid) {
// filter out empty keyword controls
+ this.product = this.productForm.value;
+ const keywords = this.product.details?.keywords?.filter((keyword) => keyword === '');
+ if (this.product.details) {
+ this.product.details.keywords = keywords;
+ }
this.repository.saveProduct(this.product);
this.product = new Product();
this.keywordGroup.clear();
this.keywordGroup.push(this.createKeywordFormControl());
this.productForm.reset();
}
}
resetForm() {
+ this.keywordGroup.clear();
+ this.keywordGroup.push(this.createKeywordFormControl());
this.editing = true;
this.product = new Product();
this.productForm.reset();
}
}
<ng-container formGroupName="details">
<div class="form-group">
<label>Supplier</label>
<input class="form-control" formControlName="supplier" />
<ul class="text-danger list-unstyled mt-1">
<li *validationErrors="productForm; control: 'details.supplier'; label: 'supplier'; let err">{{ err }}</li>
</ul>
</div>
<!-- <div class="form-group">
<label>Keywords</label>
<input class="form-control" formControlName="keywords" />
<ul class="text-danger list-unstyled mt-1">
<li *validationErrors="productForm; control: 'details.keywords'; label: 'keyword'; let err">{{ err }}</li>
</ul>
</div> -->
<ng-container formGroupName="keywords">
<button class="btn btn-sm btn-primary my-2" (click)="addKeywordControl()" type="button">Add Keyword</button>
<div class="form-group" *ngFor="let c of keywordGroup.controls; let i = index; let count = count">
<label>Keyword {{ i + 1 }}</label>
<div class="input-group">
<input class="form-control" [formControlName]="i" [value]="c.value" />
<button *ngIf="count > 1" class="btn btn-danger" type="button" (click)="removeKeywordControl(i)">Delete</button>
</div>
<ul class="text-danger list-unstyled mt-1">
<li *validationErrors="productForm; control: 'details.keywords.' + i; label: 'keyword'; let err">{{ err }}</li>
</ul>
</div>
</ng-container>
</ng-container>
We can create many keyword controls, when user submit the form, several keyword controls can be empty, we can use
required validator to forbidden the submission. But I'd rather not force user to stop and hit delete button and allow
empty values to be submitted. We can filter out empty strings from form.value
.
The other way is to use the below FilteredFormArray:
import {FormArray} from '@angular/forms';
export type ValueFilter = (value: any) => boolean;
/**
* filter out unwanted values ('' or null) for FormArray controls
* There are two issues with using methods like this.
* 1. that internal methods `_updateValue` are subject to change or removal without notice, which means that figure releases of Angular may remove the _ updateValue method and break the code
* 2. Angular packages are compiled using a TypeScript setting that excludes internal methods from the type declaration files that are used during project development. So TypeScript compiler doesn’t know that the FormArray class defines an _updateValue method and won’t allow the use of the override or the super keywords. For this reason, I have had to copy the original code from the FormArray class and integrate support for filtering, rather than just calling the FormArray implementation of the method and filtering the result.
*/
export class FilteredFormArray extends FormArray {
filter: ValueFilter | undefined = (val) => val === '' || val == null;
// _updateValue is internal API: originally defined as an abstract method in the AbstractControl class and then overridden in the FormArray class.
_updateValue() {
(this as {value: any}).value = this.controls
.filter((control) => (control.enabled || this.disabled) && !this.filter?.(control.value))
.map((control) => control.value);
}
}