diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..210d9959860e920b5d0033709932f9d601c0907a --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -0,0 +1,159 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FormGroup, + FormGroupDirective, + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors, +} from '@angular/forms'; +import { FormControlEditorAbstractComponent } from '@ods/component'; +import { MockNgControl } from '../../../test/form/MockNgControl'; + +describe('FormControlEditorAbstractComponent', () => { + let component: TestComponent; + let fixture: ComponentFixture<TestComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, TestComponent], + providers: [FormGroupDirective], + }); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + component.control = new MockNgControl(); + fixture.detectChanges(); + }); + + describe('constructor', () => { + it('should set control value accessor', () => { + expect(component.control.valueAccessor).toBe(component); + }); + }); + + describe('ng on init', () => { + it('should set valueChange subscription', () => { + component.ngOnInit(); + + expect(component._changesSubscr).toBeDefined(); + }); + + it('should call field control on change handler when fieldControl value changes ', () => { + component._fieldControlOnChangeHandler = jest.fn(); + component.ngOnInit(); + + component.fieldControl.setValue('testValue'); + + expect(component._fieldControlOnChangeHandler).toHaveBeenCalledWith('testValue'); + }); + + it('should set statusChange subscription', () => { + component.ngOnInit(); + + expect(component._statusSubscr).toBeDefined(); + }); + + it('should call setErrors on statusChange', () => { + component.setErrors = jest.fn(); + component.ngOnInit(); + + component.control.control.setErrors({ required: true }); + + expect(component.setErrors).toHaveBeenCalled(); + }); + }); + + describe('writeValue', () => { + it('should set value to fieldControl', () => { + const value = 'testValue'; + + component.writeValue(value); + + expect(component.fieldControl.value).toBe(value); + }); + }); + + describe('setErrors', () => { + it('should set fieldControl errors', () => { + const errors: ValidationErrors = { required: true }; + + component.control.control.setErrors(errors); + + expect(component.fieldControl.errors).toEqual(errors); + }); + + it('should call update invalid params', () => { + component._updateInvalidParams = jest.fn(); + + component.setErrors(); + + expect(component._updateInvalidParams).toHaveBeenCalled(); + }); + }); + + describe('remove errors', () => { + it('should remove fieldControl errors', () => { + component.fieldControl.setErrors({ fehler: 'this is an validation error' }); + + component.removeErrors(); + + expect(component.fieldControl.errors).toBeNull(); + }); + + it('should call update invalid params', () => { + component._updateInvalidParams = jest.fn(); + + component.removeErrors(); + + expect(component._updateInvalidParams).toHaveBeenCalled(); + }); + + it('should call clear all parent controls', () => { + component._clearAllParentErrors = jest.fn(); + + component.removeErrors(); + + expect(component._clearAllParentErrors).toHaveBeenCalledWith(component.control.control); + }); + }); + + describe('clear all parent errors', () => { + const parentControl: UntypedFormGroup = new FormGroup({ child: new UntypedFormControl() }); + const childControl: UntypedFormControl = <UntypedFormControl>parentControl.get('child'); + + it('should set errors to null on parent control if parent exists', () => { + const setErrorsSpy: jest.SpyInstance = jest.spyOn(parentControl, 'setErrors'); + + component._clearAllParentErrors(childControl); + + expect(setErrorsSpy).toHaveBeenCalledWith(null); + }); + + it('should call clear all parent errors if parent exists', () => { + const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); + + component._clearAllParentErrors(childControl); + + expect(clearAllParentsSpy).toHaveBeenCalledWith(parentControl); + }); + + it('should not call clear all parent errors again if parent does not exist', () => { + const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); + + component._clearAllParentErrors(parentControl); + + expect(clearAllParentsSpy).toHaveBeenCalledTimes(1); + }); + }); +}); + +@Component({ + standalone: true, + template: '', + imports: [CommonModule, ReactiveFormsModule], + providers: [FormGroupDirective], +}) +class TestComponent extends FormControlEditorAbstractComponent {} diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 89c0cd5fb117bc5c11c2aa254dfd810ec7616b32..d84fcd376b6360c7f3670e9bb2c5340504d8fd29 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -23,7 +23,7 @@ */ import { InvalidParam } from '@alfa-client/tech-shared'; import { Component, OnDestroy, OnInit, Optional, Self } from '@angular/core'; -import { ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; +import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; @Component({ @@ -31,29 +31,29 @@ import { Subscription } from 'rxjs'; }) export abstract class FormControlEditorAbstractComponent implements ControlValueAccessor, OnInit, OnDestroy { readonly fieldControl: UntypedFormControl = new UntypedFormControl(); - public onChange = (text: string | Date) => undefined; + public onChange = (value: unknown) => undefined; public onTouched = () => undefined; public invalidParams: InvalidParam[] = []; - private changesSubscr: Subscription; - private statusSubscr: Subscription; + _changesSubscr: Subscription; + _statusSubscr: Subscription; disabled: boolean = false; constructor(@Self() @Optional() public control: NgControl | null) { if (this.control) this.control.valueAccessor = this; - - this.changesSubscr = this.fieldControl.valueChanges.subscribe((val) => { - this.onChange(val); - this.setErrors(); - }); } ngOnInit(): void { - if (!this.statusSubscr && this.control) - this.statusSubscr = this.control.statusChanges.subscribe(() => { - this.setErrors(); - }); + this._changesSubscr = this.fieldControl.valueChanges.subscribe(this._fieldControlOnChangeHandler); + if (this.control) { + this._statusSubscr = this.control.statusChanges.subscribe(this.setErrors); + } + } + + _fieldControlOnChangeHandler(value: unknown): void { + this.onChange(value); + this.removeErrors(); } touch(): void { @@ -62,7 +62,6 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue writeValue(text: string): void { this.fieldControl.setValue(text); - this.setErrors(); } registerOnChange(fn: (text: string | Date) => {}): void { @@ -78,20 +77,31 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnDestroy(): void { - if (this.changesSubscr) this.changesSubscr.unsubscribe(); - if (this.statusSubscr) this.statusSubscr.unsubscribe(); + if (this._changesSubscr) this._changesSubscr.unsubscribe(); + if (this._statusSubscr) this._statusSubscr.unsubscribe(); } setErrors(): void { if (this.control) { this.fieldControl.setErrors(this.control.errors); - if (this.control.invalid) { - this.fieldControl.markAsTouched(); - } this._updateInvalidParams(); } } + removeErrors(): void { + this.fieldControl.setErrors(null); + this._updateInvalidParams(); + this._clearAllParentErrors(this.control.control); + } + + _clearAllParentErrors(control: AbstractControl): void { + const parent: AbstractControl = control.parent; + if (parent) { + parent.setErrors(null); + this._clearAllParentErrors(parent); + } + } + _updateInvalidParams(): void { this.invalidParams = this.fieldControl.errors ? diff --git a/alfa-client/libs/design-component/test/form/MockNgControl.ts b/alfa-client/libs/design-component/test/form/MockNgControl.ts index 4d41bb1b1e4d2236d21bce9a8849007889c9010f..caad15ac47aed83a5ea74971986a00485c4130ff 100644 --- a/alfa-client/libs/design-component/test/form/MockNgControl.ts +++ b/alfa-client/libs/design-component/test/form/MockNgControl.ts @@ -28,9 +28,17 @@ import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } export class MockNgControl extends NgControl { valueAccessor: ControlValueAccessor | null = null; + private _control: AbstractControl = new UntypedFormControl(null); + get control(): AbstractControl { - return new UntypedFormControl(null); + return this._control; + } + + set control(ctrl: AbstractControl) { + this._control = ctrl; } viewToModelUpdate(newValue: any): void {} + + setErrors(errors: any): void {} }