diff --git a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.html b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.html index 61d376645..cff85a6a6 100644 --- a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.html +++ b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.html @@ -11,4 +11,10 @@ End is required. + +
+ + An interval must have a start and end + +
diff --git a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.spec.ts b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.spec.ts index e02046ea3..07ba55fe3 100644 --- a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.spec.ts +++ b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.spec.ts @@ -1,7 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Interval, IntervalInputComponent } from './interval-input.component'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { Component, DebugElement, OnInit, ViewChild } from '@angular/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -37,6 +37,35 @@ class TestHostComponent implements OnInit { } } +/** + * Test host component to simulate parent component. + */ +@Component({ + template: ` +
+ + + +
` +}) +class NoValueRequiredTestHostComponent implements OnInit { + + @ViewChild('intervalInput') intervalInputComponent: IntervalInputComponent; + + form: FormGroup; + + constructor(private _fb: FormBuilder) { + } + + ngOnInit(): void { + + this.form = this._fb.group({ + interval: new FormControl(null) + }); + + } +} + describe('InvertalInputComponent', () => { let testHostComponent: TestHostComponent; let testHostFixture: ComponentFixture; @@ -121,4 +150,51 @@ describe('InvertalInputComponent', () => { }); + it('should mark the form\'s validity correctly', () => { + expect(testHostComponent.intervalInputComponent.valueRequiredValidator).toBe(true); + expect(testHostComponent.intervalInputComponent.form.valid).toBe(true); + + testHostComponent.intervalInputComponent.startIntervalControl.setValue(null); + + testHostComponent.intervalInputComponent._handleInput(); + + expect(testHostComponent.intervalInputComponent.form.valid).toBe(false); + }); + +}); + +describe('InvertalInputComponent', () => { + let testHostComponent: NoValueRequiredTestHostComponent; + let testHostFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, BrowserAnimationsModule], + declarations: [IntervalInputComponent, NoValueRequiredTestHostComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + testHostFixture = TestBed.createComponent(NoValueRequiredTestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + }); + + it('should recieve the propagated valueRequiredValidator from the parent component', () => { + expect(testHostComponent.intervalInputComponent.valueRequiredValidator).toBe(false); + }); + + it('should mark the form\'s validity correctly', () => { + expect(testHostComponent.intervalInputComponent.form.valid).toBe(true); + + testHostComponent.intervalInputComponent.startIntervalControl.setValue(1); + + testHostComponent.intervalInputComponent._handleInput(); + + expect(testHostComponent.intervalInputComponent.form.valid).toBe(false); + }); + }); diff --git a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts index afe537010..dc55ed513 100644 --- a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts +++ b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-input/interval-input.component.ts @@ -1,6 +1,6 @@ -import { Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, Optional, Self } from '@angular/core'; +import { Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Self } from '@angular/core'; import { MatFormFieldControl } from '@angular/material/form-field'; -import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgControl, NgForm, Validators } from '@angular/forms'; +import { AbstractControl, ControlValueAccessor, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgControl, NgForm, ValidatorFn, Validators } from '@angular/forms'; import { Subject } from 'rxjs'; import { FocusMonitor } from '@angular/cdk/a11y'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -27,6 +27,17 @@ export class IntervalInputErrorStateMatcher implements ErrorStateMatcher { } } +/** Interval must have a start and end of the same type, either both numbers or both null */ +export function startEndSameTypeValidator(otherInterval: FormControl): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + + // valid if both start and end are null or have values + const invalid = !(control.value === null && otherInterval.value === null || control.value !== null && otherInterval.value !== null); + + return invalid ? { 'startEndSameTypeRequired': { value: control.value } } : null; + }; +} + class MatInputBase { constructor(public _defaultErrorStateMatcher: ErrorStateMatcher, public _parentForm: NgForm, @@ -43,7 +54,7 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase = styleUrls: ['./interval-input.component.scss'], providers: [{ provide: MatFormFieldControl, useExisting: IntervalInputComponent }] }) -export class IntervalInputComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, DoCheck, CanUpdateErrorState, OnDestroy { +export class IntervalInputComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl, DoCheck, CanUpdateErrorState, OnDestroy, OnInit { static nextId = 0; form: FormGroup; @@ -53,11 +64,16 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro errorState = false; controlType = 'dsp-interval-input'; matcher = new IntervalInputErrorStateMatcher(); - onChange = (_: any) => { }; - onTouched = () => { }; + + startIntervalControl: FormControl; + endIntervalControl: FormControl; @Input() intervalStartLabel = 'start'; @Input() intervalEndLabel = 'end'; + @Input() valueRequiredValidator = true; + + onChange = (_: any) => { }; + onTouched = () => { }; get empty() { const userInput = this.form.value; @@ -127,6 +143,10 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro } else { this.form.setValue({ start: null, end: null }); } + + this.startIntervalControl.updateValueAndValidity(); + this.endIntervalControl.updateValueAndValidity(); + this.stateChanges.next(); } @@ -142,9 +162,12 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); + this.startIntervalControl = new FormControl(null); + this.endIntervalControl = new FormControl(null); + this.form = fb.group({ - start: [null, Validators.required], - end: [null, Validators.required] + start: this.startIntervalControl, + end: this.endIntervalControl }); _fm.monitor(_elRef.nativeElement, true).subscribe(origin => { @@ -157,6 +180,19 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro } } + ngOnInit() { + if (this.valueRequiredValidator) { + this.startIntervalControl.setValidators([Validators.required, startEndSameTypeValidator(this.endIntervalControl)]); + this.endIntervalControl.setValidators([Validators.required, startEndSameTypeValidator(this.startIntervalControl)]); + } else { + this.startIntervalControl.setValidators(startEndSameTypeValidator(this.endIntervalControl)); + this.endIntervalControl.setValidators(startEndSameTypeValidator(this.startIntervalControl)); + } + + this.startIntervalControl.updateValueAndValidity(); + this.endIntervalControl.updateValueAndValidity(); + } + ngDoCheck() { if (this.ngControl) { this.updateErrorState(); @@ -190,6 +226,8 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro } _handleInput(): void { + this.startIntervalControl.updateValueAndValidity(); + this.endIntervalControl.updateValueAndValidity(); this.onChange(this.value); } diff --git a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.html b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.html index 83bd20222..6021777b1 100644 --- a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.html +++ b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.html @@ -6,7 +6,7 @@ - + New value must be different than the current value. diff --git a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.spec.ts b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.spec.ts index 47678685d..c1862f7f7 100644 --- a/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.spec.ts +++ b/projects/dsp-ui/src/lib/viewer/values/interval-value/interval-value.component.spec.ts @@ -33,6 +33,7 @@ class TestIntervalInputComponent implements ControlValueAccessor, MatFormFieldCo @Input() required: boolean; @Input() shouldLabelFloat: boolean; @Input() errorStateMatcher: ErrorStateMatcher; + @Input() valueRequiredValidator = true; errorState = false; focused = false; @@ -113,12 +114,37 @@ class TestHostCreateValueComponent implements OnInit { } } +/** + * Test host component to simulate parent component. + */ +@Component({ + template: ` + ` +}) +class TestHostCreateValueNoValueRequiredComponent implements OnInit { + + @ViewChild('inputVal') inputValueComponent: IntervalValueComponent; + + mode: 'read' | 'update' | 'create' | 'search'; + + ngOnInit() { + + this.mode = 'create'; + } +} + describe('IntervalValueComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [IntervalValueComponent, TestHostDisplayValueComponent, TestIntervalInputComponent, TestHostCreateValueComponent], + declarations: [ + IntervalValueComponent, + TestHostDisplayValueComponent, + TestIntervalInputComponent, + TestHostCreateValueComponent, + TestHostCreateValueNoValueRequiredComponent + ], imports: [ ReactiveFormsModule, MatInputModule, @@ -456,4 +482,28 @@ describe('IntervalValueComponent', () => { }); }); + + describe('create an interval value no value required', () => { + + let testHostComponent: TestHostCreateValueNoValueRequiredComponent; + let testHostFixture: ComponentFixture; + + beforeEach(() => { + testHostFixture = TestBed.createComponent(TestHostCreateValueNoValueRequiredComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + + expect(testHostComponent).toBeTruthy(); + }); + + it('should not create an empty value', () => { + expect(testHostComponent.inputValueComponent.getNewValue()).toBe(false); + expect(testHostComponent.inputValueComponent.form.valid).toBe(true); + }); + + it('should propagate valueRequiredValidator to child component', () => { + expect(testHostComponent.inputValueComponent.valueRequiredValidator).toBe(false); + }); + + }); });