Skip to content

Commit

Permalink
fix(datepicker): datepicker-popup compatibility with ngModelOptions
Browse files Browse the repository at this point in the history
- 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 angular-ui#2069

Fixes angular-ui#3349
  • Loading branch information
chrisirhc committed Apr 6, 2015
1 parent 971a1b5 commit d024dd7
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 24 deletions.
64 changes: 40 additions & 24 deletions src/datepicker/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -533,35 +539,52 @@ 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
viewValue = new Date(viewValue);
}

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

Expand All @@ -570,30 +593,23 @@ 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;
element[0].focus();
}
};

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() {
Expand Down Expand Up @@ -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));
Expand Down
64 changes: 64 additions & 0 deletions src/datepicker/test/datepicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div><input ng-model="date" ' +
'ng-model-options="{ debounce: 10000 }" ' +
'datepicker-popup><div>')($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 () {
Expand Down

0 comments on commit d024dd7

Please sign in to comment.