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

feat(datepicker): support HTML5 input types #3499

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
87 changes: 61 additions & 26 deletions src/datepicker/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,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',
'month': 'yyyy-MM'
},
currentText: 'Today',
clearText: 'Clear',
closeText: 'Done',
Expand Down Expand Up @@ -476,16 +481,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 All @@ -500,6 +523,13 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi

// datepicker element
var datepickerEl = angular.element(popupEl.children()[0]);
if (isHtml5DateInput) {
if (attrs.type == 'month') {
datepickerEl.attr('datepicker-mode', '"month"');
datepickerEl.attr('min-mode', 'month');
}
}

if ( attrs.datepickerOptions ) {
var options = scope.$parent.$eval(attrs.datepickerOptions);
if(options.initDate) {
Expand Down Expand Up @@ -544,8 +574,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 @@ -557,7 +585,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 @@ -585,24 +613,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 @@ -612,7 +647,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);
});

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
87 changes: 87 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,84 @@ 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 );
});

it('works as month', function() {
setupInputWithType('month');
expect(inputEl.val()).toBe('2010-09');

changeInputValueTo(inputEl, '1980-03');

expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(2);
expect($rootScope.date.getDate()).toEqual(30);

expect(getOptions()).toEqual([
['January', 'February', 'March'],
['April', 'May', 'June'],
['July', 'August', 'September'],
['October', 'November', 'December']
]);
expect(selectedElementIndex()).toEqual( 2 );
});

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