From d024dd77ed6a20983bea5b90b75481c93f17980f Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 5 Apr 2015 13:45:10 -0700 Subject: [PATCH] fix(datepicker): datepicker-popup compatibility with ngModelOptions - Separate validation from parsing so that validation still runs on model change - Remove direct calls to $render - Remove extra call to $render during intialization (only run when format is changed) - Save last date value in formatter - Remove use of ngModel.$modelValue as users may add parsers to convert $modelValue to other formats Relates to #2069 Fixes #3349 --- src/datepicker/datepicker.js | 64 ++++++++++++++++---------- src/datepicker/test/datepicker.spec.js | 64 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 24 deletions(-) diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index f1bb0cdea9..e594d62abf 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -476,9 +476,15 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; }; - attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.datepickerPopup; - ngModel.$render(); + dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; + attrs.$observe('datepickerPopup', function(value, oldValue) { + var newDateFormat = value || datepickerPopupConfig.datepickerPopup; + // Invalidate the $modelValue to ensure that formatters re-run + // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 + if (newDateFormat !== dateFormat) { + dateFormat = newDateFormat; + ngModel.$modelValue = null; + } }); // popup element used to display calendar @@ -533,6 +539,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); } + // Internal API to maintain the correct ng-invalid-[key] class + ngModel.$$parserName = 'date'; function parseDate(viewValue) { if (angular.isNumber(viewValue)) { // presumably timestamp to date object @@ -540,28 +548,43 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } if (!viewValue) { - ngModel.$setValidity('date', true); return null; } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - ngModel.$setValidity('date', true); return viewValue; } else if (angular.isString(viewValue)) { var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); if (isNaN(date)) { - ngModel.$setValidity('date', false); return undefined; } else { - ngModel.$setValidity('date', true); return date; } } else { - ngModel.$setValidity('date', false); return undefined; } } + + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; + if (angular.isNumber(value)) { + value = new Date(value); + } + if (!value) { + return true; + } else if (angular.isDate(value) && !isNaN(value)) { + return true; + } else if (angular.isString(value)) { + var date = dateParser.parse(value, dateFormat) || new Date(value); + return !isNaN(date); + } else { + return false; + } + } + + ngModel.$validators.date = validator; ngModel.$parsers.unshift(parseDate); ngModel.$formatters.push(function (value) { + scope.date = value; return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); }); @@ -570,8 +593,11 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi if (angular.isDefined(dt)) { scope.date = dt; } + if (dateFormat) { + var date = scope.date ? dateFilter(scope.date, dateFormat) : ''; + element.val(date); + } ngModel.$setViewValue(scope.date); - ngModel.$render(); if ( closeOnDateSelection ) { scope.isOpen = false; @@ -579,21 +605,11 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } }; - element.bind('input change keyup', function() { - scope.$apply(function() { - scope.date = ngModel.$modelValue; - }); + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function () { + scope.date = ngModel.$viewValue; }); - // Outer change - ngModel.$render = function () { - if (dateFormat) { - var date = ngModel.$viewValue ? dateFilter(parseDate(ngModel.$viewValue), dateFormat) : ''; - element.val(date); - scope.date = parseDate( ngModel.$modelValue ); - } - }; - var documentClickBind = function(event) { if (scope.isOpen && event.target !== element[0]) { scope.$apply(function() { @@ -634,8 +650,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi scope.select = function( date ) { if (date === 'today') { var today = new Date(); - if (angular.isDate(ngModel.$modelValue)) { - date = new Date(ngModel.$modelValue); + if (angular.isDate(scope.date)) { + date = new Date(scope.date); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { date = new Date(today.setHours(0, 0, 0, 0)); diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 49e783884a..c406a1b3e1 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -1338,6 +1338,70 @@ describe('datepicker directive', function () { $document.unbind('keydown', getKey); }); }); + + describe('works with ngModelOptions', function () { + var $timeout; + + beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) { + $document = _$document_; + $timeout = _$timeout_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should change model and update calendar after debounce timeout', function() { + changeInputValueTo(inputEl, 'March 5, 1980'); + + expect($rootScope.date.getFullYear()).toEqual(2010); + expect($rootScope.date.getMonth()).toEqual(9 - 1); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + + // No changes yet + $timeout.flush(2000); + expect($rootScope.date.getFullYear()).toEqual(2010); + expect($rootScope.date.getMonth()).toEqual(9 - 1); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions(true)).toEqual([ + ['29', '30', '31', '01', '02', '03', '04'], + ['05', '06', '07', '08', '09', '10', '11'], + ['12', '13', '14', '15', '16', '17', '18'], + ['19', '20', '21', '22', '23', '24', '25'], + ['26', '27', '28', '29', '30', '01', '02'], + ['03', '04', '05', '06', '07', '08', '09'] + ]); + + $timeout.flush(10000); + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expectSelectedElement( 10 ); + }); + }); + }); describe('attribute `datepickerOptions`', function () {