diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index bc2354f110..a4ae9b6314 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -440,7 +440,10 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi currentText: '@', clearText: '@', closeText: '@', - dateDisabled: '&' + dateDisabled: '&', + minDate: '@', + maxDate: '@' + }, link: function(scope, element, attrs, ngModel) { var dateFormat, @@ -500,6 +503,60 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi if (attrs.dateDisabled) { datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); } + function compareDates(date1, date2) { + if (isNaN(date1) || isNaN(date2)) { + return undefined; + } + else { + return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()) ); + } + } + + function getDateLimitToCheck(limitName) { + var watchDate = scope.watchData[limitName]; + if (!watchDate) { + return null; + } else { + return new Date(watchDate); + } + } + + function isDateLimitMet(limitName, dateToCheck, viewValue) { + var parsedDate = dateParser.parse(viewValue, dateFormat); + var dateCompare = compareDates(parsedDate, dateToCheck); + if (limitName == 'minDate') { + return !dateCompare || dateCompare > 0; + } else if (limitName == 'maxDate') { + return !dateCompare || dateCompare < 0; + } + } + + function dateLimitParseFormat(limitName, viewValue, isFormatOnly) { + var dateLimit = getDateLimitToCheck(limitName); + if (dateLimit) { + var isMet = isDateLimitMet(limitName, dateLimit, viewValue); + ngModel.$setValidity(limitName, isMet); + return (isFormatOnly || isMet) ? viewValue : undefined; + } else { + return viewValue; + } + } + + function minLimitParse(viewValue) { + return dateLimitParseFormat('minDate', viewValue, false); + } + + function minLimitFormat(viewValue) { + return dateLimitParseFormat('minDate', viewValue, true); + } + + function maxLimitParse(viewValue) { + return dateLimitParseFormat('maxDate', viewValue, false); + } + + function maxLimitFormat(viewValue) { + return dateLimitParseFormat('maxDate', viewValue, true); + } function parseDate(viewValue) { if (!viewValue) { @@ -523,6 +580,10 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } } ngModel.$parsers.unshift(parseDate); + ngModel.$parsers.unshift(minLimitParse); + ngModel.$formatters.unshift(minLimitFormat); + ngModel.$parsers.unshift(maxLimitParse); + ngModel.$formatters.unshift(maxLimitFormat); ngModel.$formatters.push(function (value) { return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index b673d162da..1da684c431 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -1122,12 +1122,21 @@ describe('datepicker directive', function () { $document = _$document_; $sniffer = _$sniffer_; $rootScope.isopen = true; + $rootScope.format='yyyy-MM-dd'; $rootScope.date = new Date('September 30, 2010 15:30:00'); - var wrapElement = $compile('
')($rootScope); + $rootScope.minimumDate = new Date('January 1, 1970'); + $rootScope.maximumDate = new Date('January 1, 2020'); + $rootScope.dateDisabledHandler = jasmine.createSpy('dateDisabledHandler'); + var wrapElement = $compile('
')($rootScope); $rootScope.$digest(); assignElements(wrapElement); + })); + it('executes the dateDisabled expression for each visible day plus one for validation', function() { + expect($rootScope.dateDisabledHandler.calls.length).toEqual(42+1); + }); + it('datepicker is displayed', function() { expect(dropdownEl).not.toBeHidden(); }); @@ -1161,6 +1170,41 @@ describe('datepicker directive', function () { $rootScope.date = new Date('January 10, 1983 10:00:00'); $rootScope.$digest(); expect(inputEl.val()).toBe('1983-01-10'); + expect(inputEl).not.toHaveClass('ng-invalid'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + }); + + it('updates the input correctly when model changes to a date prior to minimum date', function() { + $rootScope.date = new Date('January 10, 1963 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('1963-01-10'); + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + }); + + it('input is not marked with invalid minimum date when minimum is not a valid date', function() { + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + $rootScope.minimumDate = 'pizza'; + $rootScope.$digest(); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + }); + + it('updates the input correctly when model changes to a date past the maximum date', function() { + $rootScope.date = new Date('January 10, 2023 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('2023-01-10'); + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-max-date'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + }); + + it('input is not marked with invalid maximum date when maximum is not a valid date', function() { + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + $rootScope.maximumDate = new Date('pizza'); + $rootScope.$digest(); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); }); it('closes the dropdown when a day is clicked', function() { @@ -1214,7 +1258,42 @@ describe('datepicker directive', function () { expect(inputEl).not.toHaveClass('ng-invalid-date'); }); - describe('focus', function () { + it('sets `ng-invalid` for date prior to minimum date', function() { + changeInputValueTo(inputEl, '1960-12-01'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('1960-12-01'); + }); + + it('sets `ng-invalid` for date (european format) prior to minimum date', function() { + $rootScope.format='dd.MM.yyyy'; + $rootScope.$digest(); + changeInputValueTo(inputEl, '20.12.1960'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('20.12.1960'); + }); + + it('sets `ng-invalid` for date past the maximum date', function() { + changeInputValueTo(inputEl, '2050-12-01'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).toHaveClass('ng-invalid-max-date'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('2050-12-01'); + }); + + describe('focus', function () { beforeEach(function() { var body = $document.find('body'); body.append(inputEl);