Skip to content

Commit 79272ad

Browse files
committed
feat(input) add support for datetime-local
partially closes angular#757
1 parent bfcc0c8 commit 79272ad

File tree

2 files changed

+258
-10
lines changed

2 files changed

+258
-10
lines changed

src/ng/directive/input.js

+141-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\
1212
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/;
1313
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
1414
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
15+
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)$/;
1516

1617
var inputType = {
1718

@@ -156,6 +157,71 @@ var inputType = {
156157
*/
157158
'date': dateInputType,
158159

160+
/**
161+
* @ngdoc inputType
162+
* @name ng.directive:input.dateTimeLocal
163+
*
164+
* @description
165+
* HTML5 or text input with datetime validation and transformation. In browsers that do not yet support
166+
* the HTML5 date input, a text element will be used. The text must be entered in a valid ISO-8601
167+
* local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:12`. Will also accept a valid ISO
168+
* datetime string or Date object as model input, but will always output a Date object to the model.
169+
*
170+
* @param {string} ngModel Assignable angular expression to data-bind to.
171+
* @param {string=} name Property name of the form under which the control is published.
172+
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
173+
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
174+
* @param {string=} required Sets `required` validation error key if the value is not entered.
175+
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
176+
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
177+
* `required` when you want to data-bind to the `required` attribute.
178+
* @param {string=} ngChange Angular expression to be executed when input changes due to user
179+
* interaction with the input element.
180+
*
181+
* @example
182+
<doc:example>
183+
<doc:source>
184+
<script>
185+
function Ctrl($scope) {
186+
$scope.value = '2010-12-28T14:57:12';
187+
}
188+
</script>
189+
<form name="myForm" ng-controller="Ctrl as dateCtrl">
190+
Pick a date between in 2013:
191+
<input type="datetime-local" name="input" ng-model="value"
192+
placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
193+
<span class="error" ng-show="myForm.input.$error.required">
194+
Required!</span>
195+
<span class="error" ng-show="myForm.input.$error.datetimelocal">
196+
Not a valid date!</span>
197+
<tt>value = {{value}}</tt><br/>
198+
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
199+
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
200+
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
201+
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
202+
</form>
203+
</doc:source>
204+
<doc:scenario>
205+
it('should initialize to model', function() {
206+
expect(binding('value')).toEqual('2010-12-28T14:57:12');
207+
expect(binding('myForm.input.$valid')).toEqual('true');
208+
});
209+
210+
it('should be invalid if empty', function() {
211+
input('value').enter('');
212+
expect(binding('value')).toEqual('');
213+
expect(binding('myForm.input.$valid')).toEqual('false');
214+
});
215+
216+
it('should be invalid if over max', function() {
217+
input('value').enter('2015-01-01T23:59:59');
218+
expect(binding('value')).toEqual('');
219+
expect(binding('myForm.input.$valid')).toEqual('false');
220+
});
221+
</doc:scenario>
222+
</doc:example>
223+
*/
224+
'datetime-local': dateTimeLocalInputType,
159225
/**
160226
* @ngdoc inputType
161227
* @name ng.directive:input.number
@@ -603,7 +669,78 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
603669
}
604670
}
605671

606-
function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) {
672+
function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
673+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
674+
675+
ctrl.$parsers.push(function(value) {
676+
if(ctrl.$isEmpty(value)) {
677+
ctrl.$setValidity('datetimelocal', true);
678+
return value;
679+
}
680+
681+
if(DATETIMELOCAL_REGEXP.test(value)) {
682+
ctrl.$setValidity('datetimelocal', true);
683+
return new Date(getTime(value));
684+
}
685+
686+
ctrl.$setValidity('datetimelocal', false);
687+
return undefined;
688+
});
689+
690+
ctrl.$formatters.push(function(value) {
691+
if(isDate(value)) {
692+
return $filter('date')(value, 'yyyy-MM-ddTHH:mm:ss');
693+
}
694+
return ctrl.$isEmpty(value) ? '' : '' + value;
695+
});
696+
697+
if(attr.min) {
698+
var minValidator = function(value) {
699+
var valid = ctrl.$isEmpty(value) ||
700+
(getTime(value) >= getTime(attr.min));
701+
ctrl.$setValidity('min', valid);
702+
return valid ? value : undefined;
703+
};
704+
705+
ctrl.$parsers.push(minValidator);
706+
ctrl.$formatters.push(minValidator);
707+
}
708+
709+
if(attr.max) {
710+
var maxValidator = function(value) {
711+
var valid = ctrl.$isEmpty(value) ||
712+
(getTime(value) <= getTime(attr.max));
713+
ctrl.$setValidity('max', valid);
714+
return valid ? value : undefined;
715+
};
716+
717+
ctrl.$parsers.push(maxValidator);
718+
ctrl.$formatters.push(maxValidator);
719+
}
720+
721+
function getTime(iso) {
722+
if(isDate(iso)) {
723+
return +iso;
724+
}
725+
726+
if(isString(iso)) {
727+
DATETIMELOCAL_REGEXP.lastIndex = 0;
728+
var parts = DATETIMELOCAL_REGEXP.exec(iso),
729+
yyyy = +parts[1],
730+
MM = +parts[2] - 1,
731+
dd = +parts[3],
732+
HH = +parts[4],
733+
mm = +parts[5],
734+
ss = +parts[6];
735+
736+
return +new Date(yyyy, MM, dd, HH, mm, ss);
737+
}
738+
739+
return NaN;
740+
}
741+
}
742+
743+
function dateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
607744
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
608745

609746
ctrl.$parsers.push(function(value) {
@@ -623,13 +760,7 @@ function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) {
623760

624761
ctrl.$formatters.push(function(value) {
625762
if(isDate(value)) {
626-
var year = value.getFullYear(),
627-
month = value.getMonth() + 1,
628-
day = value.getDate();
629-
630-
month = (month < 10 ? '0' : '') + month;
631-
day = (day < 10 ? '0' : '') + day;
632-
return year + '-' + month + '-' + day;
763+
return $filter('date')(value, 'yyyy-MM-dd');
633764
}
634765
return ctrl.$isEmpty(value) ? '' : '' + value;
635766
});
@@ -950,14 +1081,14 @@ function checkboxInputType(scope, element, attr, ctrl) {
9501081
</doc:scenario>
9511082
</doc:example>
9521083
*/
953-
var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
1084+
var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) {
9541085
return {
9551086
restrict: 'E',
9561087
require: '?ngModel',
9571088
link: function(scope, element, attr, ctrl) {
9581089
if (ctrl) {
9591090
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
960-
$browser);
1091+
$browser, $filter);
9611092
}
9621093
}
9631094
};

test/ng/directive/inputSpec.js

+117
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,123 @@ describe('input', function() {
717717

718718
// INPUT TYPES
719719

720+
721+
describe('datetime-local', function () {
722+
it('should set the view if the model is valid ISO8601 local datetime', function() {
723+
compileInput('<input type="datetime-local" ng-model="lunchtime"/>');
724+
725+
scope.$apply(function(){
726+
scope.lunchtime = '2013-12-16T11:30:15';
727+
});
728+
729+
expect(inputElm.val()).toBe('2013-12-16T11:30:15');
730+
});
731+
732+
it('should set the view if the model if a valid Date object.', function(){
733+
compileInput('<input type="datetime-local" ng-model="tenSecondsToNextYear"/>');
734+
735+
scope.$apply(function (){
736+
scope.tenSecondsToNextYear = new Date(2013, 11, 31, 23, 59, 50);
737+
});
738+
739+
expect(inputElm.val()).toBe('2013-12-31T23:59:50');
740+
});
741+
742+
it('should set the model undefined if the view is invalid', function (){
743+
compileInput('<input type="datetime-local" ng-model="breakMe"/>');
744+
745+
scope.$apply(function (){
746+
scope.breakMe = new Date(2009, 0, 6, 16, 25, 10);
747+
});
748+
749+
expect(inputElm.val()).toBe('2009-01-06T16:25:10');
750+
751+
try {
752+
//set to text for browsers with datetime-local validation.
753+
inputElm[0].setAttribute('type', 'text');
754+
} catch(e) {
755+
//for IE8
756+
}
757+
758+
changeInputValueTo('stuff');
759+
expect(inputElm.val()).toBe('stuff');
760+
expect(scope.breakMe).toBeUndefined();
761+
expect(inputElm).toBeInvalid();
762+
});
763+
764+
describe('min', function (){
765+
beforeEach(function (){
766+
compileInput('<input type="datetime-local" ng-model="value" name="alias" min="2000-01-01T12:30:10" />');
767+
scope.$digest();
768+
});
769+
770+
it('should invalidate', function (){
771+
changeInputValueTo('1999-12-31T01:02:03');
772+
expect(inputElm).toBeInvalid();
773+
expect(scope.value).toBeFalsy();
774+
expect(scope.form.alias.$error.min).toBeTruthy();
775+
});
776+
777+
it('should validate', function (){
778+
changeInputValueTo('2000-01-01T23:02:01');
779+
expect(inputElm).toBeValid();
780+
expect(+scope.value).toBe(+new Date(2000, 0, 1, 23, 2, 1));
781+
expect(scope.form.alias.$error.min).toBeFalsy();
782+
});
783+
});
784+
785+
describe('max', function (){
786+
beforeEach(function (){
787+
compileInput('<input type="datetime-local" ng-model="value" name="alias" max="2019-01-01T01:02:03" />');
788+
scope.$digest();
789+
});
790+
791+
it('should invalidate', function (){
792+
changeInputValueTo('2019-12-31T01:02:03');
793+
expect(inputElm).toBeInvalid();
794+
expect(scope.value).toBeFalsy();
795+
expect(scope.form.alias.$error.max).toBeTruthy();
796+
});
797+
798+
it('should validate', function() {
799+
changeInputValueTo('2000-01-01T01:02:03');
800+
expect(inputElm).toBeValid();
801+
expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3));
802+
expect(scope.form.alias.$error.max).toBeFalsy();
803+
});
804+
});
805+
806+
it('should validate even if max value changes on-the-fly', function(done) {
807+
scope.max = '2013-01-01T01:02:03';
808+
compileInput('<input type="datetime-local" ng-model="value" name="alias" max="{{max}}" />');
809+
scope.$digest();
810+
811+
changeInputValueTo('2014-01-01T12:34:56');
812+
expect(inputElm).toBeInvalid();
813+
814+
scope.max = '2001-01-01T01:02:03';
815+
scope.$digest(function () {
816+
expect(inputElm).toBeValid();
817+
done();
818+
});
819+
});
820+
821+
it('should validate even if min value changes on-the-fly', function(done) {
822+
scope.min = '2013-01-01T01:02:03';
823+
compileInput('<input type="datetime-local" ng-model="value" name="alias" min="{{min}}" />');
824+
scope.$digest();
825+
826+
changeInputValueTo('2010-01-01T12:34:56');
827+
expect(inputElm).toBeInvalid();
828+
829+
scope.min = '2014-01-01T01:02:03';
830+
scope.$digest(function () {
831+
expect(inputElm).toBeValid();
832+
done();
833+
});
834+
});
835+
});
836+
720837
describe('date', function () {
721838
it('should set the view if the model is valid ISO8601 date', function() {
722839
compileInput('<input type="date" ng-model="birthday"/>');

0 commit comments

Comments
 (0)