Skip to content

Instantly share code, notes, and snippets.

@ManuelTS
Last active April 30, 2020 06:19
Show Gist options
  • Save ManuelTS/e010f7287b15c61e901f76371ba7afeb to your computer and use it in GitHub Desktop.
Save ManuelTS/e010f7287b15c61e901f76371ba7afeb to your computer and use it in GitHub Desktop.
Angular Forms: A ControlValueAccessor for a FormArray with a Validator to give you the full control of single array element access and validation. For an explanation of the single files please read: https://www.redlink.at/en/controlvalueaccessor-for-an-array-plus-validator/
<!-- 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>
<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>
:host {
// Style here the elements of your-array-entry.component.html as you wish
}
// 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;
}
}
<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>
:host {
// Style here the elements of your-array.component.html as you wish
}
@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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment