Skip to content

Commit 0ba0dc1

Browse files
committed
feat(input): add handling for date input
partially closes angular#757
1 parent 7d6e5a2 commit 0ba0dc1

File tree

2 files changed

+256
-0
lines changed

2 files changed

+256
-0
lines changed

src/ng/directive/input.js

+140
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
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*$/;
14+
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
1415

1516
var inputType = {
1617

@@ -89,6 +90,71 @@ var inputType = {
8990
*/
9091
'text': textInputType,
9192

93+
/**
94+
* @ngdoc inputType
95+
* @name ng.directive:input.date
96+
*
97+
* @description
98+
* HTML5 or text input with date validation and transformation. In browsers that do not yet support
99+
* the HTML5 date input, a text element will be used. The text must be entered in a valid ISO-8601
100+
* date format (yyyy-MM-dd), for example: `2009-01-06`. Will also accept a valid ISO date or Date object
101+
* as model input, but will always output a Date object to the model.
102+
*
103+
* @param {string} ngModel Assignable angular expression to data-bind to.
104+
* @param {string=} name Property name of the form under which the control is published.
105+
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
106+
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
107+
* @param {string=} required Sets `required` validation error key if the value is not entered.
108+
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
109+
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
110+
* `required` when you want to data-bind to the `required` attribute.
111+
* @param {string=} ngChange Angular expression to be executed when input changes due to user
112+
* interaction with the input element.
113+
*
114+
* @example
115+
<doc:example>
116+
<doc:source>
117+
<script>
118+
function Ctrl($scope) {
119+
$scope.value = '2013-10-22';
120+
}
121+
</script>
122+
<form name="myForm" ng-controller="Ctrl as dateCtrl">
123+
Pick a date between in 2013:
124+
<input type="date" name="input" ng-model="value"
125+
placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
126+
<span class="error" ng-show="myForm.input.$error.required">
127+
Required!</span>
128+
<span class="error" ng-show="myForm.input.$error.date">
129+
Not a valid date!</span>
130+
<tt>value = {{value}}</tt><br/>
131+
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
132+
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
133+
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
134+
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
135+
</form>
136+
</doc:source>
137+
<doc:scenario>
138+
it('should initialize to model', function() {
139+
expect(binding('value')).toEqual('2013-10-22');
140+
expect(binding('myForm.input.$valid')).toEqual('true');
141+
});
142+
143+
it('should be invalid if empty', function() {
144+
input('value').enter('');
145+
expect(binding('value')).toEqual('');
146+
expect(binding('myForm.input.$valid')).toEqual('false');
147+
});
148+
149+
it('should be invalid if over max', function() {
150+
input('value').enter('2015-01-01');
151+
expect(binding('value')).toEqual('');
152+
expect(binding('myForm.input.$valid')).toEqual('false');
153+
});
154+
</doc:scenario>
155+
</doc:example>
156+
*/
157+
'date': dateInputType,
92158

93159
/**
94160
* @ngdoc inputType
@@ -539,6 +605,80 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
539605
}
540606
}
541607

608+
function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) {
609+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
610+
611+
ctrl.$parsers.push(function(value) {
612+
if(ctrl.$isEmpty(value)) {
613+
ctrl.$setValidity('date', true);
614+
return value;
615+
}
616+
617+
if(DATE_REGEXP.test(value)) {
618+
ctrl.$setValidity('date', true);
619+
return new Date(getTime(value));
620+
}
621+
622+
ctrl.$setValidity('date', false);
623+
return undefined;
624+
});
625+
626+
ctrl.$formatters.push(function(value) {
627+
if(isDate(value)) {
628+
var year = value.getFullYear(),
629+
month = value.getMonth() + 1,
630+
day = value.getDate();
631+
632+
month = (month < 10 ? '0' : '') + month;
633+
day = (day < 10 ? '0' : '') + day;
634+
return year + '-' + month + '-' + day;
635+
}
636+
return ctrl.$isEmpty(value) ? '' : '' + value;
637+
});
638+
639+
if(attr.min) {
640+
var minValidator = function(value) {
641+
var valid = ctrl.$isEmpty(value) ||
642+
(getTime(value) >= getTime(attr.min));
643+
ctrl.$setValidity('min', valid);
644+
return valid ? value : undefined;
645+
};
646+
647+
ctrl.$parsers.push(minValidator);
648+
ctrl.$formatters.push(minValidator);
649+
}
650+
651+
if(attr.max) {
652+
var maxValidator = function(value) {
653+
var valid = ctrl.$isEmpty(value) ||
654+
(getTime(value) <= getTime(attr.max));
655+
ctrl.$setValidity('max', valid);
656+
return valid ? value : undefined;
657+
};
658+
659+
ctrl.$parsers.push(maxValidator);
660+
ctrl.$formatters.push(maxValidator);
661+
}
662+
663+
function getTime(iso) {
664+
if(isDate(iso)) {
665+
return +iso;
666+
}
667+
668+
if(isString(iso)) {
669+
DATE_REGEXP.lastIndex = 0;
670+
var parts = DATE_REGEXP.exec(iso),
671+
yyyy = +parts[1],
672+
mm = +parts[2] - 1,
673+
dd = +parts[3],
674+
time = new Date(yyyy, mm, dd);
675+
return +time;
676+
}
677+
678+
return NaN;
679+
}
680+
}
681+
542682
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
543683
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
544684

test/ng/directive/inputSpec.js

+116
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,122 @@ describe('input', function() {
735735

736736
// INPUT TYPES
737737

738+
describe('date', function () {
739+
it('should set the view if the model is valid ISO8601 date', function() {
740+
compileInput('<input type="date" ng-model="birthday"/>');
741+
742+
scope.$apply(function(){
743+
scope.birthday = '1977-10-22';
744+
});
745+
746+
expect(inputElm.val()).toBe('1977-10-22');
747+
});
748+
749+
it('should set the view if the model if a valid Date object.', function(){
750+
compileInput('<input type="date" ng-model="christmas"/>');
751+
752+
scope.$apply(function (){
753+
scope.christmas = new Date(2013, 11, 25);
754+
});
755+
756+
expect(inputElm.val()).toBe('2013-12-25');
757+
});
758+
759+
it('should set the model undefined if the view is invalid', function (){
760+
compileInput('<input type="date" ng-model="arrMatey"/>');
761+
762+
scope.$apply(function (){
763+
scope.arrMatey = new Date(2014, 8, 14);
764+
});
765+
766+
expect(inputElm.val()).toBe('2014-09-14');
767+
768+
try {
769+
//set to text for browsers with date validation.
770+
inputElm[0].setAttribute('type', 'text');
771+
} catch(e) {
772+
//for IE8
773+
}
774+
775+
changeInputValueTo('1-2-3');
776+
expect(inputElm.val()).toBe('1-2-3');
777+
expect(scope.arrMatey).toBeUndefined();
778+
expect(inputElm).toBeInvalid();
779+
});
780+
781+
describe('min', function (){
782+
beforeEach(function (){
783+
compileInput('<input type="date" ng-model="value" name="alias" min="2000-01-01" />');
784+
scope.$digest();
785+
});
786+
787+
it('should invalidate', function (){
788+
changeInputValueTo('1999-12-31');
789+
expect(inputElm).toBeInvalid();
790+
expect(scope.value).toBeFalsy();
791+
expect(scope.form.alias.$error.min).toBeTruthy();
792+
});
793+
794+
it('should validate', function (){
795+
changeInputValueTo('2000-01-01');
796+
expect(inputElm).toBeValid();
797+
expect(+scope.value).toBe(+new Date(2000, 0, 1));
798+
expect(scope.form.alias.$error.min).toBeFalsy();
799+
});
800+
});
801+
802+
describe('max', function (){
803+
beforeEach(function (){
804+
compileInput('<input type="date" ng-model="value" name="alias" max="2019-01-01" />');
805+
scope.$digest();
806+
});
807+
808+
it('should invalidate', function (){
809+
changeInputValueTo('2019-12-31');
810+
expect(inputElm).toBeInvalid();
811+
expect(scope.value).toBeFalsy();
812+
expect(scope.form.alias.$error.max).toBeTruthy();
813+
});
814+
815+
it('should validate', function() {
816+
changeInputValueTo('2000-01-01');
817+
expect(inputElm).toBeValid();
818+
expect(+scope.value).toBe(+new Date(2000, 0, 1));
819+
expect(scope.form.alias.$error.max).toBeFalsy();
820+
});
821+
});
822+
823+
it('should validate even if max value changes on-the-fly', function(done) {
824+
scope.max = '2013-01-01';
825+
compileInput('<input type="date" ng-model="value" name="alias" max="{{max}}" />');
826+
scope.$digest();
827+
828+
changeInputValueTo('2014-01-01');
829+
expect(inputElm).toBeInvalid();
830+
831+
scope.max = '2001-01-01';
832+
scope.$digest(function () {
833+
expect(inputElm).toBeValid();
834+
done();
835+
});
836+
});
837+
838+
it('should validate even if min value changes on-the-fly', function(done) {
839+
scope.min = '2013-01-01';
840+
compileInput('<input type="date" ng-model="value" name="alias" min="{{min}}" />');
841+
scope.$digest();
842+
843+
changeInputValueTo('2010-01-01');
844+
expect(inputElm).toBeInvalid();
845+
846+
scope.min = '2014-01-01';
847+
scope.$digest(function () {
848+
expect(inputElm).toBeValid();
849+
done();
850+
});
851+
});
852+
});
853+
738854
describe('number', function() {
739855

740856
it('should reset the model if view is invalid', function() {

0 commit comments

Comments
 (0)