Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Interval Value Validators): Propagate valueRequiredValidator to child component (DSP-1193) #253

Merged
merged 7 commits into from Jan 13, 2021
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')))">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for null checks for start or end as in case of the date?
Or is this taken care of by startEndSameTypeValidator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is handled in startEndSameTypeValidator

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test case for the startEndSameTypeValidator? Or is this not possible because the required operator will always trigger anyway (start or end is null)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 19f996d

});

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)]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add the startEndSameTypeValidator also when the required validator is not set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, I forgot. Added in 3c0a216

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

});
});