Last active
April 30, 2020 06:19
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:host { | |
// Style here the elements of your-array-entry.component.html as you wish | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:host { | |
// Style here the elements of your-array.component.html as you wish | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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