Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Commit

Permalink
feat(datepicker): support HTML5 date input type
Browse files Browse the repository at this point in the history
Closes #3499
  • Loading branch information
chrisirhc authored and rvanbaalen committed Apr 29, 2015
1 parent e909b92 commit 1a9e88f
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 29 deletions.
20 changes: 17 additions & 3 deletions src/dateparser/dateparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ angular.module('ui.bootstrap.dateparser', [])
};
}

this.parse = function(input, format) {
this.parse = function(input, format, baseDate) {
if ( !angular.isString(input) || !format ) {
return input;
}
Expand All @@ -124,7 +124,20 @@ angular.module('ui.bootstrap.dateparser', [])
results = input.match(regex);

if ( results && results.length ) {
var fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }, dt;
var fields, dt;
if (baseDate) {
fields = {
year: baseDate.getFullYear(),
month: baseDate.getMonth(),
date: baseDate.getDate(),
hours: baseDate.getHours(),
minutes: baseDate.getMinutes(),
seconds: baseDate.getSeconds(),
milliseconds: baseDate.getMilliseconds()
};
} else {
fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
}

for( var i = 1, n = results.length; i < n; i++ ) {
var mapper = map[i-1];
Expand All @@ -134,7 +147,8 @@ angular.module('ui.bootstrap.dateparser', [])
}

if ( isValid(fields.year, fields.month, fields.date) ) {
dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds, fields.milliseconds);
dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds,
fields.milliseconds || 0);
}

return dt;
Expand Down
80 changes: 54 additions & 26 deletions src/datepicker/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst

.constant('datepickerPopupConfig', {
datepickerPopup: 'yyyy-MM-dd',
html5Types: {
date: 'yyyy-MM-dd',
'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss'
// TODO: Add other formats/type support
},
currentText: 'Today',
clearText: 'Clear',
closeText: 'Done',
Expand Down Expand Up @@ -479,16 +484,34 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
};

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;
}
});
var isHtml5DateInput = false;
if (datepickerPopupConfig.html5Types[attrs.type]) {
dateFormat = datepickerPopupConfig.html5Types[attrs.type];
isHtml5DateInput = true;
} else {
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;

if (!dateFormat) {
throw new Error('datepickerPopup must have a date format specified.');
}
}
});
}

if (!dateFormat) {
throw new Error('datepickerPopup must have a date format specified.');
}

if (isHtml5DateInput && attrs.datepickerPopup) {
throw new Error('HTML5 date input types do not support custom formats.');
}

// popup element used to display calendar
var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
Expand Down Expand Up @@ -547,8 +570,6 @@ 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
Expand All @@ -560,7 +581,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
} else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
return viewValue;
} else if (angular.isString(viewValue)) {
var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue);
if (isNaN(date)) {
return undefined;
} else {
Expand Down Expand Up @@ -588,24 +609,31 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
}
}

ngModel.$validators.date = validator;
ngModel.$parsers.unshift(parseDate);

ngModel.$formatters.push(function (value) {
scope.date = value;
return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
});
if (!isHtml5DateInput) {
// Internal API to maintain the correct ng-invalid-[key] class
ngModel.$$parserName = 'date';
ngModel.$validators.date = validator;
ngModel.$parsers.unshift(parseDate);
ngModel.$formatters.push(function (value) {
scope.date = value;
return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
});
}
else {
ngModel.$formatters.push(function (value) {
scope.date = value;
return value;
});
}

// Inner change
scope.dateSelection = function(dt) {
if (angular.isDefined(dt)) {
scope.date = dt;
}
if (dateFormat) {
var date = scope.date ? dateFilter(scope.date, dateFormat) : '';
element.val(date);
}
ngModel.$setViewValue(scope.date);
var date = scope.date ? dateFilter(scope.date, dateFormat) : '';
element.val(date);
ngModel.$setViewValue(date);

if ( closeOnDateSelection ) {
scope.isOpen = false;
Expand All @@ -615,7 +643,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi

// Detect changes in the view from the text box
ngModel.$viewChangeListeners.push(function () {
scope.date = ngModel.$viewValue;
scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue);

This comment has been minimized.

Copy link
@IRus

IRus Jul 20, 2015

Why there are || new Date()? if dateParser actually can't parse that should mean that this is empty/invalid input

});

var documentClickBind = function(event) {
Expand Down
9 changes: 9 additions & 0 deletions src/datepicker/docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ <h4>Popup</h4>
</span>
</p>
</div>

<div class="col-md-6">
<p class="input-group">
<input type="date" class="form-control" datepicker-popup ng-model="dt" is-open="opened" min-date="minDate" max-date="'2015-06-22'" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" ng-required="true" close-text="Close" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
Expand Down
68 changes: 68 additions & 0 deletions src/datepicker/test/datepicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ describe('datepicker directive', function () {
return element.find('tbody').find('button');
}

function selectedElementIndex() {
var buttons = getAllOptionsEl();
for (var i = 0; i < buttons.length; i++) {
if (angular.element(buttons[i]).hasClass('btn-info')) {
return i;
}
}
}

function expectSelectedElement( index ) {
var buttons = getAllOptionsEl();
angular.forEach( buttons, function( button, idx ) {
Expand Down Expand Up @@ -1402,6 +1411,65 @@ describe('datepicker directive', function () {
});
});

describe('works with HTML5 date input types', function () {
var date2 = new Date('October 1, 2010 12:34:56.789');
beforeEach(inject(function(_$document_) {
$document = _$document_;
$rootScope.isopen = true;
$rootScope.date = new Date('September 30, 2010 15:30:00');
}));

it('works as date', function() {
setupInputWithType('date');
expect(dropdownEl).toBeHidden();
expect(inputEl.val()).toBe('2010-09-30');

changeInputValueTo(inputEl, '1980-03-05');

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']
]);
expect(selectedElementIndex()).toEqual( 10 );
});

it('works as datetime-local', function() {
setupInputWithType('datetime-local');
expect(inputEl.val()).toBe('2010-09-30T15:30:00.000');

changeInputValueTo(inputEl, '1980-03-05T12:34:56.000');

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']
]);
expect(selectedElementIndex()).toEqual( 10 );
});

function setupInputWithType(type) {
var wrapElement = $compile('<div><input type="' +
type + '" ng-model="date" datepicker-popup><div>')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}
});

});

describe('attribute `datepickerOptions`', function () {
Expand Down

0 comments on commit 1a9e88f

Please sign in to comment.