30. April 2020   Manuel Schrempf

ControlValueAccessor for an Array plus Validator

During the development of our web pages with Angular, a client specification for a highly customized form exceeded the default capabilities of Angular’s reactive forms.

A custom form component able to handle Arrays was written, which implements the ControlValueAccessor and Validator interfaces to have the control of sub-form and main form re-validation on each state change of a single array element.

The combination of ControlValueAccessor for an array with Validator seam to be a special case and infrequent on the Internet. In the following, a minimal example with comments is given as a free service from Redlink GmbH for easy reusage and a well economy of your time.

Classes

The YourArrayComponent class in the your-array.component.ts file contains the form value, being an array, and its validation method.

@Component({
selector: 'app-your-array',
templateUrl: './your-array.component.html',
styleUrls: ['./your-array.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR, // Is an InjectionToken required by the ControlValueAccessor interface to provide a form value
useExisting: forwardRef(() => YourArrayComponent), // tells Angular to use the existing instance
multi: true,
},
{
provide: NG_VALIDATORS, // Is an InjectionToken required by this class to be able to be used as an Validator
useExisting: forwardRef(() => YourArrayComponent),
multi: true,
}]
})
export class YourArrayComponent implements ControlValueAccessor, Validator {
yourArray: YourType[] = [];
discloseChange = (_: any) => {}; // Called on a value change
discloseTouched = () => {}; // Called if you care if the form was touched
discloseValidatorChange = () => {}; // Called on a validator change or re-validation;
newEntry(yourEntry: YourType): void {
const index = this.yourArray.findIndex(alreadyAdded => alreadyAdded.property === yourEntry.property);
if (index > -1) {
this.yourArray.splice(index, 1, yourEntry);
} else {
this.yourArray.splice(0, 0, yourEntry);
}
this.value = this.yourArray; // Invokes bottom setter
}
deleteEntry(yourEntry: YourType): void{
const index = this.yourEntry.findIndex(alreadyAdded => alreadyAdded.property === yourEntry.property);
this.yourArray.splice(index, 1);
this.value = this.yourArray; // Invokes bottom setter
}
get value(): YourType[] {
return this.yourArray;
}
set value(newValue: YourType[]) {
this.yourArray = newValue;
this.discloseChange(this.yourArray);
this.discloseValidatorChange();
}
registerOnChange(fn: any): void {
this.discloseChange = fn;
}
registerOnTouched(fn: any): void {
this.discloseTouched = fn;
}
writeValue(obj: YourType[]): void {
this.value = obj;
}
validate(control: AbstractControl): ValidationErrors | null {
let valid = true;
if (!!this.yourArray && this.yourArray.length > 0) {
this.yourArray.forEach(yourEntry => valid = valid && !!yourEntry); // Perform here your single item validation
}
return valid ? null : {invalid: true};
}
registerOnValidatorChange?(fn: () => void): void {
this.discloseValidatorChange = fn;
}
}

The your-array.component.scss file contains the style for the your-array.component.html file.

:host {
// Style here the elements of your-array.component.html as you wish
}

The your-array.component.html file renders your array and invokes via received events from the list elements the validation of the array. Our client desired a new list entry creation with the possibility to add the entry to an existing list. Hence, the your-array.component.html looks as follows:

<div>
<app-your-array-entry [yourEntry]="" class="empty" (newEntry)="newEntry($event)">
</app-your-array-entry>
<app-your-array-entry *ngFor="let yourEntry of value"
[yourEntry]="yourEntry"
(newEntry)="newEntry($event)
(deleteEntry)="deleteEntry($event)"
(changeEntry)="discloseValidatorChange()">
</app-your-array-entry>
</div>

The single app-your-array-entry.component.ts file renders the single list entries, which may emit the events to invoke methods in the your-array.component.ts file or YourArrayComponent class, respectively.

// The states are used to render the single entries as the single states say:
export enum YourState {
EMPTY = 0, // Typescript indexes automatically from the first one onward
DISPLAY, // All single states may be used to show or hide some icons, buttons...
EDIT
}
@Component({
selector: 'app-your-array-entry',
templateUrl: './your-array-entry.component.html',
styleUrls: ['./your-array-entry.component.scss']
})
export class YourArrayEntryComponent implements OnInit {
@Input()
yourEntry: YourType;
@Output()
deleteEntry = new EventEmitter<YourType>();
@Output()
newEntry = new EventEmitter<YourType>();
@Output()
changeEntry = new EventEmitter<void>();
state: YourState = YourState.EMPTY;
ngOnInit() {
// Perform rendering depending on the contents of the field "yourEntry" and set the correct state
}
onClick() {
switch (this.state) {
case YourState.EMPTY: // Create a new array entry (= new object)
// Perform any preprocessing here on a new entry created by the user
this.newEntry.emit(new YourType({
...this.yourEntry
}));
this.yourEntry = undefined; // You may invoke a clear method here
break;
case YourState.DISPLAY: // Switch from DISPLAY to EDIT state
// Perform any preprocessing here before the user can edit the entry
this.state = YourState.EDIT;
break;
default: // YourState.EDIT: Switch from EDIT to DISPLAY state
// Perform any processing here on edit entry changes from by the user
this.state = YourState.DISPLAY;
break;
}
this.changeEntry.emit();
}
onDeleteClick () {
this.state = YourState.DISPLAY;
this.delete.emit(this.yourEntry);
}
isStatusEdit(): boolean { // for a more readble your-array-entry.component.html
return this.state === YourState.EDIT;
}
isStatusEmpty(): boolean { // for a more readble your-array-entry.component.html
return this.state === YourState.EMPTY;
}
isStatusDisplay(): boolean { // for a more readble your-array-entry.component.html
return this.state === YourState.DISPLAY;
}
}

The your-array-entry.component.scss file contains the style for the your-array-entry.component.html file.

:host {
// Style here the elements of your-array-entry.component.html as you wish
}

The your-array-entry.component.html files renders one single array entry and adapts its looks to the state accordingly:

<div>
<p>{{yourEntry.yourProperty}} is in the state of {{state}}.</p>
<button (click)="onClick()">
<ng-container *ngIf="isStatusEmpty()">
Create new Array Entry
</ng-container>
<ng-container *ngIf="isStatusDisplay()">
Edit Array Entry
</ng-container>
<ng-container *ngIf="isStatusEdit()">
Submit edited Array Entry
</ng-container>
</button>
<button *ngIf="!isStatusEmpty()" type="button" (click)="onDeleteClick()">
Delete Array Entry
</button>
</div>

You now can validate single array entries on each of their state changes and customize all involved steps as you wish. You then can include the created YourArrayComponent in your form as follows:

<!-- In your main form, include the generated app-your-array component simply as -->
<form>
<!-- ... -->
<app-your-array [(ngModel)]="yourFormObject.yourArrayProperty" name="yourArray">
</app-your-array>
<!-- ... -->
</form>
view raw form.html hosted with ❤ by GitHub

Happy efficient and valid Angular coding!