+ Too early!
+ Too late!
+ Date unavailable!
+ [startView]="yearView ? 'year' : 'month'">
@@ -49,13 +53,13 @@
Result
[(ngModel)]="date"
[min]="minDate"
[max]="maxDate"
+ [mdDatepickerFilter]="filterOdd ? dateFilter : null"
placeholder="Pick a date">
+ [startView]="yearView ? 'year' : 'month'">
diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts
index 7a70a9c0b4bb..0b2264e380dd 100644
--- a/src/demo-app/datepicker/datepicker-demo.ts
+++ b/src/demo-app/datepicker/datepicker-demo.ts
@@ -15,5 +15,5 @@ export class DatepickerDemo {
maxDate: Date;
startAt: Date;
date: Date;
- dateFilter = (date: Date) => date.getMonth() % 2 == 0 && date.getDate() % 2 == 0;
+ dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;
}
diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts
index 33148d3aa714..938d318339af 100644
--- a/src/lib/core/datetime/date-adapter.ts
+++ b/src/lib/core/datetime/date-adapter.ts
@@ -140,6 +140,14 @@ export abstract class DateAdapter {
*/
abstract addCalendarDays(date: D, days: number): D;
+ /**
+ * Gets the RFC 3339 compatible date string (https://tools.ietf.org/html/rfc3339) for the given
+ * date.
+ * @param date The date to get the ISO date string for.
+ * @returns The ISO date string date string.
+ */
+ abstract getISODateString(date: D): string;
+
/**
* Sets the locale used for all dates.
* @param locale The new locale.
diff --git a/src/lib/core/datetime/native-date-adapter.ts b/src/lib/core/datetime/native-date-adapter.ts
index 7ea5c8e4efac..9e952076eb86 100644
--- a/src/lib/core/datetime/native-date-adapter.ts
+++ b/src/lib/core/datetime/native-date-adapter.ts
@@ -160,6 +160,14 @@ export class NativeDateAdapter extends DateAdapter {
this.getYear(date), this.getMonth(date), this.getDate(date) + days);
}
+ getISODateString(date: Date): string {
+ return [
+ date.getUTCFullYear(),
+ this._2digit(date.getUTCMonth() + 1),
+ this._2digit(date.getUTCDate())
+ ].join('-');
+ }
+
/** Creates a date but allows the month and date to overflow. */
private _createDateWithOverflow(year: number, month: number, date: number) {
let result = new Date(year, month, date);
@@ -171,4 +179,13 @@ export class NativeDateAdapter extends DateAdapter {
}
return result;
}
+
+ /**
+ * Pads a number to make it two digits.
+ * @param n The number to pad.
+ * @returns The padded number.
+ */
+ private _2digit(n: number) {
+ return ('00' + n).slice(-2);
+ }
}
diff --git a/src/lib/datepicker/datepicker-content.html b/src/lib/datepicker/datepicker-content.html
index dd8a696741cb..0f3a70ec7dff 100644
--- a/src/lib/datepicker/datepicker-content.html
+++ b/src/lib/datepicker/datepicker-content.html
@@ -4,7 +4,7 @@
[startView]="datepicker.startView"
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
- [dateFilter]="datepicker.dateFilter"
+ [dateFilter]="datepicker._dateFilter"
[selected]="datepicker._selected"
(selectedChange)="datepicker._selectAndClose($event)">
diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts
index 7767fcae98ef..d40868dd69ac 100644
--- a/src/lib/datepicker/datepicker-input.ts
+++ b/src/lib/datepicker/datepicker-input.ts
@@ -11,7 +11,16 @@ import {
Renderer
} from '@angular/core';
import {MdDatepicker} from './datepicker';
-import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';
import {MdInputContainer} from '../input/input-container';
import {DOWN_ARROW} from '../core/keyboard/keycodes';
@@ -27,22 +36,30 @@ export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
};
+export const MD_DATEPICKER_VALIDATORS: any = {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => MdDatepickerInput),
+ multi: true
+};
+
+
/** Directive used to connect an input to a MdDatepicker. */
@Directive({
selector: 'input[mdDatepicker], input[matDatepicker]',
- providers: [MD_DATEPICKER_VALUE_ACCESSOR],
+ providers: [MD_DATEPICKER_VALUE_ACCESSOR, MD_DATEPICKER_VALIDATORS],
host: {
'[attr.aria-expanded]': '_datepicker?.opened || "false"',
'[attr.aria-haspopup]': 'true',
'[attr.aria-owns]': '_datepicker?.id',
- '[min]': '_min',
- '[max]': '_max',
+ '[attr.min]': 'min ? _dateAdapter.getISODateString(min) : null',
+ '[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
'(input)': '_onInput($event.target.value)',
'(blur)': '_onTouched()',
'(keydown)': '_onKeydown($event)',
}
})
-export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor, OnDestroy {
+export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor, OnDestroy,
+ Validator {
/** The datepicker that this input is associated with. */
@Input()
set mdDatepicker(value: MdDatepicker) {
@@ -53,8 +70,17 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
}
_datepicker: MdDatepicker;
- @Input()
- set matDatepicker(value: MdDatepicker) { this.mdDatepicker = value; }
+ @Input() set matDatepicker(value: MdDatepicker) { this.mdDatepicker = value; }
+
+ @Input() set mdDatepickerFilter(filter: (date: D | null) => boolean) {
+ this._dateFilter = filter;
+ this._validatorOnChange();
+ }
+ _dateFilter: (date: D | null) => boolean;
+
+ @Input() set matDatepickerFilter(filter: (date: D | null) => boolean) {
+ this.mdDatepickerFilter = filter;
+ }
/** The value of the input. */
@Input()
@@ -73,20 +99,58 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
}
/** The minimum valid date. */
- @Input() min: D;
+ @Input()
+ get min(): D { return this._min; }
+ set min(value: D) {
+ this._min = value;
+ this._validatorOnChange();
+ }
+ private _min: D;
/** The maximum valid date. */
- @Input() max: D;
+ @Input()
+ get max(): D { return this._max; }
+ set max(value: D) {
+ this._max = value;
+ this._validatorOnChange();
+ }
+ private _max: D;
/** Emits when the value changes (either due to user input or programmatic change). */
_valueChange = new EventEmitter();
- _onChange = (value: any) => {};
-
_onTouched = () => {};
+ private _cvaOnChange = (value: any) => {};
+
+ private _validatorOnChange = () => {};
+
private _datepickerSubscription: Subscription;
+ /** The form control validator for the min date. */
+ private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
+ return (!this.min || !control.value ||
+ this._dateAdapter.compareDate(this.min, control.value) < 0) ?
+ null : {'mdDatepickerMin': {'min': this.min, 'actual': control.value}};
+ }
+
+ /** The form control validator for the max date. */
+ private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
+ return (!this.max || !control.value ||
+ this._dateAdapter.compareDate(this.max, control.value) > 0) ?
+ null : {'mdDatepickerMax': {'max': this.max, 'actual': control.value}};
+ }
+
+ /** The form control validator for the date filter. */
+ private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
+ return !this._dateFilter || !control.value || this._dateFilter(control.value) ?
+ null : {'mdDatepickerFilter': true};
+ }
+
+ /** The combined form control validator for this input. */
+ private _validator: ValidatorFn =
+ Validators.compose([this._minValidator, this._maxValidator, this._filterValidator]);
+
constructor(
private _elementRef: ElementRef,
private _renderer: Renderer,
@@ -106,7 +170,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
this._datepickerSubscription =
this._datepicker.selectedChanged.subscribe((selected: D) => {
this.value = selected;
- this._onChange(selected);
+ this._cvaOnChange(selected);
});
}
}
@@ -117,6 +181,14 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
}
}
+ registerOnValidatorChange(fn: () => void): void {
+ this._validatorOnChange = fn;
+ }
+
+ validate(c: AbstractControl): ValidationErrors | null {
+ return this._validator ? this._validator(c) : null;
+ }
+
/**
* Gets the element that the datepicker popup should be connected to.
* @return The element to connect the popup to.
@@ -132,7 +204,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
// Implemented as part of ControlValueAccessor
registerOnChange(fn: (value: any) => void): void {
- this._onChange = fn;
+ this._cvaOnChange = fn;
}
// Implemented as part of ControlValueAccessor
@@ -154,7 +226,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
_onInput(value: string) {
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
- this._onChange(date);
+ this._cvaOnChange(date);
this._valueChange.emit(date);
}
}
diff --git a/src/lib/datepicker/datepicker.md b/src/lib/datepicker/datepicker.md
index 56278e9931b6..ec235cfcc3dc 100644
--- a/src/lib/datepicker/datepicker.md
+++ b/src/lib/datepicker/datepicker.md
@@ -60,20 +60,22 @@ startDate = new Date(1990, 0, 1);
```
-### Preventing selection of specific dates
-There are two ways to restrict the dates available for selection in the datepicker. The first is by
-using the `min` and `max` properties of the input. This will disable all dates on the calendar
-before or after the respective given dates. It will also prevent the user from advancing the
-calendar past the `month` or `year` (depending on current view) containing the `min` or `max` date.
+### Date validation
+There are three properties that add date validation to the datepicker input. The first two are the
+`min` and `max` properties. In addition to enforcing validation on the input, these properties will
+disable all dates on the calendar popup before or after the respective values and prevent the user
+from advancing the calendar past the `month` or `year` (depending on current view) containing the
+`min` or `max` date.
-The second way to restrict selection is using the `dateFilter` property of `md-datepicker`. The
-`dateFilter` property accepts a function of ` => boolean` (where `` is the date type used by
+The second way to add date validation is using the `mdDatepickerFilter` property of the datepicker
+input. This property accepts a function of ` => boolean` (where `` is the date type used by
the datepicker, see section on
[choosing a date implementation](#choosing-a-date-implementation-and-date-format-settings)).
-A result of `true` indicates that the date is selectable and a result of `false` indicates that it
-is not. One important difference between using `dateFilter` vs using `min` or `max` is that
-filtering out all dates before a certain point, will not prevent the user from advancing the
-calendar past that point.
+A result of `true` indicates that the date is valid and a result of `false` indicates that it is
+not. Again this will also disable the dates on the calendar that are invalid. However, one important
+difference between using `mdDatepickerFilter` vs using `min` or `max` is that filtering out all
+dates before or after a certain point, will not prevent the user from advancing the calendar past
+that point.
```ts
myFilter = (d: Date) => d.getFullYear() > 2005
@@ -82,12 +84,18 @@ maxDate = new Date(2020, 11, 31);
```
```html
-
-
+
+
```
In this example the user can back past 2005, but all of the dates before then will be unselectable.
-They will not be able to go further back in the calendar than 2000.
+They will not be able to go further back in the calendar than 2000. If they manually type in a date
+that is before the min, after the max, or filtered out, the input will have validation errors.
+
+Each validation property has a different error that can be checked:
+ * A value that violates the `min` property will have a `mdDatepickerMin` error.
+ * A value that violates the `max` property will have a `mdDatepickerMax` error.
+ * A value that violates the `mdDatepickerFilter` property will have a `mdDatepickerFilter` error.
### Touch UI mode
The datepicker normally opens as a popup under the input. However this is not ideal for touch
diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts
index f92e76672fa9..19d91acfbc2e 100644
--- a/src/lib/datepicker/datepicker.spec.ts
+++ b/src/lib/datepicker/datepicker.spec.ts
@@ -30,6 +30,7 @@ describe('MdDatepicker', () => {
ReactiveFormsModule,
],
declarations: [
+ DatepickerWithFilterAndValidation,
DatepickerWithFormControl,
DatepickerWithMinAndMax,
DatepickerWithNgModel,
@@ -428,6 +429,58 @@ describe('MdDatepicker', () => {
expect(testComponent.datepicker._maxDate).toEqual(new Date(2020, JAN, 1));
});
});
+
+ describe('datepicker with filter and validation', () => {
+ let fixture: ComponentFixture;
+ let testComponent: DatepickerWithFilterAndValidation;
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(DatepickerWithFilterAndValidation);
+ fixture.detectChanges();
+
+ testComponent = fixture.componentInstance;
+ }));
+
+ afterEach(async(() => {
+ testComponent.datepicker.close();
+ fixture.detectChanges();
+ }));
+
+ it('should mark input invalid', async(() => {
+ testComponent.date = new Date(2017, JAN, 1);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
+ .toContain('ng-invalid');
+
+ testComponent.date = new Date(2017, JAN, 2);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
+ .not.toContain('ng-invalid');
+ });
+ });
+ }));
+
+ it('should disable filtered calendar cells', () => {
+ fixture.detectChanges();
+
+ testComponent.datepicker.open();
+ fixture.detectChanges();
+
+ expect(document.querySelector('md-dialog-container')).not.toBeNull();
+
+ let cells = document.querySelectorAll('.mat-calendar-body-cell');
+ expect(cells[0].classList).toContain('mat-calendar-body-disabled');
+ expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
+ });
+ });
});
describe('with missing DateAdapter and MD_DATE_FORMATS', () => {
@@ -440,17 +493,7 @@ describe('MdDatepicker', () => {
NoopAnimationsModule,
ReactiveFormsModule,
],
- declarations: [
- DatepickerWithFormControl,
- DatepickerWithMinAndMax,
- DatepickerWithNgModel,
- DatepickerWithStartAt,
- DatepickerWithToggle,
- InputContainerDatepicker,
- MultiInputDatepicker,
- NoInputDatepicker,
- StandardDatepicker,
- ],
+ declarations: [StandardDatepicker],
});
TestBed.compileComponents();
@@ -567,3 +610,17 @@ class DatepickerWithMinAndMax {
maxDate = new Date(2020, JAN, 1);
@ViewChild('d') datepicker: MdDatepicker;
}
+
+
+@Component({
+ template: `
+
+
+
+ `,
+})
+class DatepickerWithFilterAndValidation {
+ @ViewChild('d') datepicker: MdDatepicker;
+ date: Date;
+ filter = (date: Date) => date.getDate() != 1;
+}
diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts
index 17f8945e2c44..4ba113254244 100644
--- a/src/lib/datepicker/datepicker.ts
+++ b/src/lib/datepicker/datepicker.ts
@@ -116,9 +116,6 @@ export class MdDatepicker implements OnDestroy {
*/
@Input() touchUi = false;
- /** A function used to filter which dates are selectable. */
- @Input() dateFilter: (date: D) => boolean;
-
/** Emits new selected date when selected date changes. */
@Output() selectedChanged = new EventEmitter();
@@ -141,6 +138,10 @@ export class MdDatepicker implements OnDestroy {
return this._datepickerInput && this._datepickerInput.max;
}
+ get _dateFilter(): (date: D | null) => boolean {
+ return this._datepickerInput && this._datepickerInput._dateFilter;
+ }
+
/** A reference to the overlay when the calendar is opened as a popup. */
private _popupRef: OverlayRef;