Skip to content

Commit

Permalink
fix(datepicker): validate min/max date
Browse files Browse the repository at this point in the history
VPAT-664
  • Loading branch information
kevinbuhmann committed Oct 31, 2022
1 parent cb32ff2 commit e9adcc3
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 4 deletions.
23 changes: 19 additions & 4 deletions projects/angular/clarity.api.md
Expand Up @@ -4,6 +4,7 @@

```ts

import { AbstractControl } from '@angular/forms';
import { AfterContentChecked } from '@angular/core';
import { AfterContentInit } from '@angular/core';
import { AfterViewChecked } from '@angular/core';
Expand Down Expand Up @@ -48,6 +49,8 @@ import { Subscription } from 'rxjs';
import { TemplateRef } from '@angular/core';
import { TrackByFunction } from '@angular/core';
import { Type } from '@angular/core';
import { ValidationErrors } from '@angular/forms';
import { Validator } from '@angular/forms';
import { ViewContainerRef } from '@angular/core';

// @public (undocumented)
Expand Down Expand Up @@ -86,7 +89,7 @@ export class ClarityModule {
// Warning: (ae-forgotten-export) The symbol "i20" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<ClarityModule, never, never, [typeof i1.ClrEmphasisModule, typeof i2_4.ClrDataModule, typeof i3_2.ClrIconModule, typeof i4_12.ClrModalModule, typeof i5_7.ClrLoadingModule, typeof i6_2.ClrConditionalModule, typeof i7_5.ClrFocusTrapModule, typeof i8_6.ClrFocusOnViewInitModule, typeof i9_4.ClrForTypeAheadModule, typeof i10_4.ClrButtonModule, typeof i11_2.ClrFormsModule, typeof i12_3.ClrLayoutModule, typeof i13_2.ClrPopoverModule, typeof i14_2.ClrWizardModule, typeof i15_2.ClrDragAndDropModule, typeof i16_2.ClrStepperModule, typeof i17_2.ClrSpinnerModule, typeof i18_2.ClrProgressBarModule, typeof i19_2.ClrPopoverModuleNext, typeof i20_2.ClrTimelineModule]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<ClarityModule, never, never, [typeof i1.ClrEmphasisModule, typeof i2_4.ClrDataModule, typeof i3_2.ClrIconModule, typeof i4_12.ClrModalModule, typeof i5_7.ClrLoadingModule, typeof i6_2.ClrConditionalModule, typeof i7_5.ClrFocusTrapModule, typeof i8_6.ClrFocusOnViewInitModule, typeof i9_5.ClrForTypeAheadModule, typeof i10_4.ClrButtonModule, typeof i11_2.ClrFormsModule, typeof i12_3.ClrLayoutModule, typeof i13_2.ClrPopoverModule, typeof i14_2.ClrWizardModule, typeof i15_2.ClrDragAndDropModule, typeof i16_2.ClrStepperModule, typeof i17_2.ClrSpinnerModule, typeof i18_2.ClrProgressBarModule, typeof i19_2.ClrPopoverModuleNext, typeof i20_2.ClrTimelineModule]>;
}

// @public (undocumented)
Expand Down Expand Up @@ -2029,6 +2032,17 @@ export class ClrDateInput extends WrappedFormControl<ClrDateContainer> implement
static ɵfac: i0.ɵɵFactoryDeclaration<ClrDateInput, [null, null, null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, null, { optional: true; }, null]>;
}

// @public (undocumented)
export class ClrDateInputValidator implements Validator {
constructor(dateIOService: DateIOService);
// (undocumented)
validate(control: AbstractControl): ValidationErrors;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<ClrDateInputValidator, "[clrDate]", never, {}, {}, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ClrDateInputValidator, [{ optional: true; }]>;
}

// @public (undocumented)
export class ClrDatepickerModule {
constructor();
Expand All @@ -2044,9 +2058,10 @@ export class ClrDatepickerModule {
// Warning: (ae-forgotten-export) The symbol "i6" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "i7" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "i8" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "i9" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrDatepickerModule, [typeof i1_16.ClrDay, typeof i2_12.ClrDateContainer, typeof i3_12.ClrDateInput, typeof i4_8.ClrDatepickerViewManager, typeof i5_6.ClrMonthpicker, typeof i6_6.ClrYearpicker, typeof i7_4.ClrDaypicker, typeof i8_4.ClrCalendar], [typeof i6.CommonModule, typeof i7_3.ClrHostWrappingModule, typeof i6_2.ClrConditionalModule, typeof i19_2.ClrPopoverModuleNext, typeof i3_2.ClrIconModule, typeof i7_5.ClrFocusTrapModule, typeof i2_6.ClrCommonFormsModule], [typeof i1_16.ClrDay, typeof i2_12.ClrDateContainer, typeof i3_12.ClrDateInput, typeof i4_8.ClrDatepickerViewManager, typeof i5_6.ClrMonthpicker, typeof i6_6.ClrYearpicker, typeof i7_4.ClrDaypicker, typeof i8_4.ClrCalendar]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrDatepickerModule, [typeof i1_16.ClrDay, typeof i2_12.ClrDateContainer, typeof i3_12.ClrDateInput, typeof i4_8.ClrDateInputValidator, typeof i5_6.ClrDatepickerViewManager, typeof i6_6.ClrMonthpicker, typeof i7_4.ClrYearpicker, typeof i8_4.ClrDaypicker, typeof i9_3.ClrCalendar], [typeof i6.CommonModule, typeof i7_3.ClrHostWrappingModule, typeof i6_2.ClrConditionalModule, typeof i19_2.ClrPopoverModuleNext, typeof i3_2.ClrIconModule, typeof i7_5.ClrFocusTrapModule, typeof i2_6.ClrCommonFormsModule], [typeof i1_16.ClrDay, typeof i2_12.ClrDateContainer, typeof i3_12.ClrDateInput, typeof i4_8.ClrDateInputValidator, typeof i5_6.ClrDatepickerViewManager, typeof i6_6.ClrMonthpicker, typeof i7_4.ClrYearpicker, typeof i8_4.ClrDaypicker, typeof i9_3.ClrCalendar]>;
}

// @public (undocumented)
Expand Down Expand Up @@ -2471,7 +2486,7 @@ export class ClrFormsModule {
// Warning: (ae-forgotten-export) The symbol "i12" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrFormsModule, never, [typeof i6.CommonModule], [typeof i2_6.ClrCommonFormsModule, typeof i3_7.ClrCheckboxModule, typeof i4_5.ClrComboboxModule, typeof i5_5.ClrDatepickerModule, typeof i6_7.ClrInputModule, typeof i7_6.ClrPasswordModule, typeof i8_5.ClrRadioModule, typeof i9_3.ClrSelectModule, typeof i10_3.ClrTextareaModule, typeof i11_3.ClrRangeModule, typeof i12_2.ClrDatalistModule]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrFormsModule, never, [typeof i6.CommonModule], [typeof i2_6.ClrCommonFormsModule, typeof i3_7.ClrCheckboxModule, typeof i4_5.ClrComboboxModule, typeof i5_5.ClrDatepickerModule, typeof i6_7.ClrInputModule, typeof i7_6.ClrPasswordModule, typeof i8_5.ClrRadioModule, typeof i9_4.ClrSelectModule, typeof i10_3.ClrTextareaModule, typeof i11_3.ClrRangeModule, typeof i12_2.ClrDatalistModule]>;
}

// @public (undocumented)
Expand Down Expand Up @@ -4824,7 +4839,7 @@ export class ClrWizardModule {
// Warning: (ae-forgotten-export) The symbol "i11" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrWizardModule, [typeof i1_45.ClrWizard, typeof i2_34.ClrWizardPage, typeof i3_26.ClrWizardStepnav, typeof i4_17.ClrWizardStepnavItem, typeof i5_14.ClrWizardButton, typeof i6_10.ClrWizardHeaderAction, typeof i7_9.ClrWizardCustomTags, typeof i8_7.ClrWizardPageTitle, typeof i9_5.ClrWizardPageNavTitle, typeof i10_5.ClrWizardPageButtons, typeof i11_5.ClrWizardPageHeaderActions], [typeof i6.CommonModule, typeof i4_12.ClrModalModule, typeof i1_2.ClrAlertModule], [typeof i1_45.ClrWizard, typeof i2_34.ClrWizardPage, typeof i3_26.ClrWizardStepnav, typeof i4_17.ClrWizardStepnavItem, typeof i5_14.ClrWizardButton, typeof i6_10.ClrWizardHeaderAction, typeof i7_9.ClrWizardCustomTags, typeof i8_7.ClrWizardPageTitle, typeof i9_5.ClrWizardPageNavTitle, typeof i10_5.ClrWizardPageButtons, typeof i11_5.ClrWizardPageHeaderActions]>;
static ɵmod: i0.ɵɵNgModuleDeclaration<ClrWizardModule, [typeof i1_45.ClrWizard, typeof i2_34.ClrWizardPage, typeof i3_26.ClrWizardStepnav, typeof i4_17.ClrWizardStepnavItem, typeof i5_14.ClrWizardButton, typeof i6_10.ClrWizardHeaderAction, typeof i7_9.ClrWizardCustomTags, typeof i8_7.ClrWizardPageTitle, typeof i9_6.ClrWizardPageNavTitle, typeof i10_5.ClrWizardPageButtons, typeof i11_5.ClrWizardPageHeaderActions], [typeof i6.CommonModule, typeof i4_12.ClrModalModule, typeof i1_2.ClrAlertModule], [typeof i1_45.ClrWizard, typeof i2_34.ClrWizardPage, typeof i3_26.ClrWizardStepnav, typeof i4_17.ClrWizardStepnavItem, typeof i5_14.ClrWizardButton, typeof i6_10.ClrWizardHeaderAction, typeof i7_9.ClrWizardCustomTags, typeof i8_7.ClrWizardPageTitle, typeof i9_6.ClrWizardPageNavTitle, typeof i10_5.ClrWizardPageButtons, typeof i11_5.ClrWizardPageHeaderActions]>;
}

// @public
Expand Down
2 changes: 2 additions & 0 deletions projects/angular/src/forms/datepicker/all.spec.ts
Expand Up @@ -8,6 +8,7 @@ import { addHelpers } from '../../data/datagrid/helpers.spec';
import CalendarSpecs from './calendar.spec';
import DateContainerSpecs from './date-container.spec';
import DateInputSpecs from './date-input.spec';
import DateInputValidatorSpecs from './date-input.validator.spec';
import DatepickerViewManagerSpecs from './datepicker-view-manager.spec';
import DayComponentSpecs from './day.spec';
import DaypickerSpecs from './daypicker.spec';
Expand Down Expand Up @@ -54,5 +55,6 @@ describe('Datepicker', function () {
YearpickerSpecs();
CalendarSpecs();
DateInputSpecs();
DateInputValidatorSpecs();
});
});
80 changes: 80 additions & 0 deletions projects/angular/src/forms/datepicker/date-input.validator.spec.ts
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2016-2022 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

import { ClrDatepickerModule } from './datepicker.module';

@Component({
template: `
<clr-date-container>
<input type="date" clrDate min="2022-01-01" max="2022-12-31" [formControl]="formControl" />
</clr-date-container>
`,
})
class TestComponent {
readonly formControl = new FormControl();
}

export default function () {
describe('Date Input Validator', () => {
let fixture: ComponentFixture<TestComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, ClrDatepickerModule],
declarations: [TestComponent],
});

fixture = TestBed.createComponent(TestComponent);
});

it('should not allow a date less than the min date', () => {
fixture.componentInstance.formControl.setValue('12/31/2021');
fixture.detectChanges();

const expectedErrors = {
min: { min: '1/1/2022', actual: '12/31/2021' },
};

expect(fixture.componentInstance.formControl.errors).toEqual(expectedErrors);
});

it('should allow a date equal to the min date', () => {
fixture.componentInstance.formControl.setValue('1/1/2022');
fixture.detectChanges();

expect(fixture.componentInstance.formControl.errors).toBeNull();
});

it('should allow a date between the min and max dates', () => {
fixture.componentInstance.formControl.setValue('6/1/2022');
fixture.detectChanges();

expect(fixture.componentInstance.formControl.errors).toBeNull();
});

it('should allow a date equal to the max date', () => {
fixture.componentInstance.formControl.setValue('12/31/2022');
fixture.detectChanges();

expect(fixture.componentInstance.formControl.errors).toBeNull();
});

it('should not allow a date greater than the max date', () => {
fixture.componentInstance.formControl.setValue('1/1/2023');
fixture.detectChanges();

const expectedErrors = {
max: { max: '12/31/2022', actual: '1/1/2023' },
};

expect(fixture.componentInstance.formControl.errors).toEqual(expectedErrors);
});
});
}
34 changes: 34 additions & 0 deletions projects/angular/src/forms/datepicker/date-input.validator.ts
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2016-2022 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { Directive, Optional } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';

import { DateIOService } from './providers/date-io.service';

@Directive({
selector: '[clrDate]',
providers: [{ provide: NG_VALIDATORS, useExisting: ClrDateInputValidator, multi: true }],
})
export class ClrDateInputValidator implements Validator {
constructor(@Optional() private dateIOService: DateIOService) {}

validate(control: AbstractControl): ValidationErrors {
if (this.dateIOService) {
const value = this.dateIOService.getDateValueFromDateString(control.value);
const minDate = this.dateIOService.disabledDates.minDate.toDate();
const maxDate = this.dateIOService.disabledDates.maxDate.toDate();

if (value && value < this.dateIOService.disabledDates.minDate.toDate()) {
return { min: { min: minDate.toLocaleDateString(), actual: value.toLocaleDateString() } };
} else if (value && value > this.dateIOService.disabledDates.maxDate.toDate()) {
return { max: { max: maxDate.toLocaleDateString(), actual: value.toLocaleDateString() } };
}
}

return null;
}
}
2 changes: 2 additions & 0 deletions projects/angular/src/forms/datepicker/datepicker.module.ts
Expand Up @@ -24,6 +24,7 @@ import { ClrCommonFormsModule } from '../common/common.module';
import { ClrCalendar } from './calendar';
import { ClrDateContainer } from './date-container';
import { ClrDateInput } from './date-input';
import { ClrDateInputValidator } from './date-input.validator';
import { ClrDatepickerViewManager } from './datepicker-view-manager';
import { ClrDay } from './day';
import { ClrDaypicker } from './daypicker';
Expand All @@ -34,6 +35,7 @@ export const CLR_DATEPICKER_DIRECTIVES: Type<any>[] = [
ClrDay,
ClrDateContainer,
ClrDateInput,
ClrDateInputValidator,
ClrDatepickerViewManager,
ClrMonthpicker,
ClrYearpicker,
Expand Down
1 change: 1 addition & 0 deletions projects/angular/src/forms/datepicker/index.ts
Expand Up @@ -6,6 +6,7 @@

export * from './date-container';
export * from './date-input';
export * from './date-input.validator';
export * from './datepicker-view-manager';
export * from './daypicker';
export * from './monthpicker';
Expand Down
2 changes: 2 additions & 0 deletions projects/demo/src/app/datepicker/datepicker-min-max.html
Expand Up @@ -10,6 +10,8 @@ <h6>Min/Max Dates</h6>
<clr-date-container>
<label>Date</label>
<input type="date" [(ngModel)]="model" clrDate [min]="minDate" [max]="maxDate" />
<clr-control-error *clrIfError="'min'">Value must be on or after min date</clr-control-error>
<clr-control-error *clrIfError="'max'">Value must be on or before max date</clr-control-error>
</clr-date-container>
<clr-input-container>
<label>Min Date</label>
Expand Down

0 comments on commit e9adcc3

Please sign in to comment.