Skip to content

Commit

Permalink
fix(Interval Value Validators): Propagate valueRequiredValidator to c…
Browse files Browse the repository at this point in the history
…hild component (DSP-1193) (#253)

* fix (interval-value-comp): propagate valueRequiredValidator to child component

* test(interval value & interval input value): adds more unit tests to test form validity

* refactor(interval-input): adds startEndSameTypeValidator to FormControls even if the value is not required & refactors startEndSameTypeValidator validity logic

* test(interval-input): adds additional assertion for startEndSameTypeValidator functionality
  • Loading branch information
mdelez committed Jan 13, 2021
1 parent 4ba898b commit 3424831
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 10 deletions.
Expand Up @@ -11,4 +11,10 @@
End is <strong>required</strong>.
</mat-error>
</mat-form-field>

<div class="date-form-error">
<mat-error *ngIf="((startIntervalControl.hasError('startEndSameTypeRequired') || endIntervalControl.hasError('startEndSameTypeRequired')))">
<span class="custom-error-message">An interval must have a <strong>start</strong> and <strong>end</strong></span>
</mat-error>
</div>
</div>
@@ -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';
Expand Down Expand Up @@ -37,6 +37,35 @@ class TestHostComponent implements OnInit {
}
}

/**
* Test host component to simulate parent component.
*/
@Component({
template: `
<div [formGroup]="form">
<mat-form-field>
<dsp-interval-input #intervalInput [formControlName]="'interval'" [valueRequiredValidator]="false"></dsp-interval-input>
</mat-form-field>
</div>`
})
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<TestHostComponent>;
Expand Down Expand Up @@ -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<NoValueRequiredTestHostComponent>;

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);
});

});
@@ -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';
Expand All @@ -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,
Expand All @@ -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<Interval>, DoCheck, CanUpdateErrorState, OnDestroy {
export class IntervalInputComponent extends _MatInputMixinBase implements ControlValueAccessor, MatFormFieldControl<Interval>, DoCheck, CanUpdateErrorState, OnDestroy, OnInit {
static nextId = 0;

form: FormGroup;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -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 => {
Expand All @@ -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();
Expand Down Expand Up @@ -190,6 +226,8 @@ export class IntervalInputComponent extends _MatInputMixinBase implements Contro
}

_handleInput(): void {
this.startIntervalControl.updateValueAndValidity();
this.endIntervalControl.updateValueAndValidity();
this.onChange(this.value);
}

Expand Down
Expand Up @@ -6,7 +6,7 @@
<ng-template #showForm>
<span [formGroup]="form">
<mat-form-field class="large-field child-value-component" floatLabel="never">
<dsp-interval-input #intervalInput [formControlName]="'value'" class="value" [errorStateMatcher]="matcher"></dsp-interval-input>
<dsp-interval-input #intervalInput [formControlName]="'value'" class="value" [errorStateMatcher]="matcher" [valueRequiredValidator]="valueRequiredValidator"></dsp-interval-input>
<mat-error *ngIf="valueFormControl.hasError('valueNotChanged') &&
(valueFormControl.touched || valueFormControl.dirty)">
<span class="custom-error-message">New value must be different than the current value.</span>
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -113,12 +114,37 @@ class TestHostCreateValueComponent implements OnInit {
}
}

/**
* Test host component to simulate parent component.
*/
@Component({
template: `
<dsp-interval-value #inputVal [mode]="mode" [valueRequiredValidator]="false"></dsp-interval-value>`
})
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,
Expand Down Expand Up @@ -456,4 +482,28 @@ describe('IntervalValueComponent', () => {
});

});

describe('create an interval value no value required', () => {

let testHostComponent: TestHostCreateValueNoValueRequiredComponent;
let testHostFixture: ComponentFixture<TestHostCreateValueNoValueRequiredComponent>;

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);
});

});
});

0 comments on commit 3424831

Please sign in to comment.