Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Add support for <input type="date"/> #5256

Closed
wants to merge 1 commit into from
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
140 changes: 140 additions & 0 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;

var inputType = {

Expand Down Expand Up @@ -89,6 +90,71 @@ var inputType = {
*/
'text': textInputType,

/**
* @ngdoc inputType
* @name ng.directive:input.date
*
* @description
* HTML5 or text input with date validation and transformation. In browsers that do not yet support
* the HTML5 date input, a text element will be used. The text must be entered in a valid ISO-8601
* date format (yyyy-MM-dd), for example: `2009-01-06`. Will also accept a valid ISO date or Date object
* as model input, but will always output a Date object to the model.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl($scope) {
$scope.value = '2013-10-22';
}
</script>
<form name="myForm" ng-controller="Ctrl as dateCtrl">
Pick a date between in 2013:
<input type="date" name="input" ng-model="value"
placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.date">
Not a valid date!</span>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('value')).toEqual('2013-10-22');
expect(binding('myForm.input.$valid')).toEqual('true');
});

it('should be invalid if empty', function() {
input('value').enter('');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});

it('should be invalid if over max', function() {
input('value').enter('2015-01-01');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
'date': dateInputType,

/**
* @ngdoc inputType
Expand Down Expand Up @@ -530,6 +596,80 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}

function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);

ctrl.$parsers.push(function(value) {
if(ctrl.$isEmpty(value)) {
ctrl.$setValidity('date', true);
return value;
}

if(DATE_REGEXP.test(value)) {
ctrl.$setValidity('date', true);
return new Date(getTime(value));
}

ctrl.$setValidity('date', false);
return undefined;
});

ctrl.$formatters.push(function(value) {
if(isDate(value)) {
var year = value.getFullYear(),
month = value.getMonth() + 1,
day = value.getDate();

month = (month < 10 ? '0' : '') + month;
day = (day < 10 ? '0' : '') + day;
return year + '-' + month + '-' + day;
}
return ctrl.$isEmpty(value) ? '' : '' + value;
});

if(attr.min) {
var minValidator = function(value) {
var valid = ctrl.$isEmpty(value) ||
(getTime(value) >= getTime(attr.min));
ctrl.$setValidity('min', valid);
return valid ? value : undefined;
};

ctrl.$parsers.push(minValidator);
ctrl.$formatters.push(minValidator);
}

if(attr.max) {
var maxValidator = function(value) {
var valid = ctrl.$isEmpty(value) ||
(getTime(value) <= getTime(attr.max));
ctrl.$setValidity('max', valid);
return valid ? value : undefined;
};

ctrl.$parsers.push(maxValidator);
ctrl.$formatters.push(maxValidator);
}

function getTime(iso) {
if(isDate(iso)) {
return +iso;
}

if(isString(iso)) {
DATE_REGEXP.lastIndex = 0;
var parts = DATE_REGEXP.exec(iso),
yyyy = +parts[1],
mm = +parts[2] - 1,
dd = +parts[3],
time = new Date(yyyy, mm, dd);
return +time;
}

return NaN;
}
}

function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);

Expand Down
116 changes: 116 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,122 @@ describe('input', function() {

// INPUT TYPES

describe('date', function () {
it('should set the view if the model is valid ISO8601 date', function() {
compileInput('<input type="date" ng-model="birthday"/>');

scope.$apply(function(){
scope.birthday = '1977-10-22';
});

expect(inputElm.val()).toBe('1977-10-22');
});

it('should set the view if the model if a valid Date object.', function(){
compileInput('<input type="date" ng-model="christmas"/>');

scope.$apply(function (){
scope.christmas = new Date(2013, 11, 25);
});

expect(inputElm.val()).toBe('2013-12-25');
});

it('should set the model undefined if the view is invalid', function (){
compileInput('<input type="date" ng-model="arrMatey"/>');

scope.$apply(function (){
scope.arrMatey = new Date(2014, 8, 14);
});

expect(inputElm.val()).toBe('2014-09-14');

try {
//set to text for browsers with date validation.
inputElm[0].setAttribute('type', 'text');
} catch(e) {
//for IE8
}

changeInputValueTo('1-2-3');
expect(inputElm.val()).toBe('1-2-3');
expect(scope.arrMatey).toBeUndefined();
expect(inputElm).toBeInvalid();
});

describe('min', function (){
beforeEach(function (){
compileInput('<input type="date" ng-model="value" name="alias" min="2000-01-01" />');
scope.$digest();
});

it('should invalidate', function (){
changeInputValueTo('1999-12-31');
expect(inputElm).toBeInvalid();
expect(scope.value).toBeFalsy();
expect(scope.form.alias.$error.min).toBeTruthy();
});

it('should validate', function (){
changeInputValueTo('2000-01-01');
expect(inputElm).toBeValid();
expect(+scope.value).toBe(+new Date(2000, 0, 1));
expect(scope.form.alias.$error.min).toBeFalsy();
});
});

describe('max', function (){
beforeEach(function (){
compileInput('<input type="date" ng-model="value" name="alias" max="2019-01-01" />');
scope.$digest();
});

it('should invalidate', function (){
changeInputValueTo('2019-12-31');
expect(inputElm).toBeInvalid();
expect(scope.value).toBeFalsy();
expect(scope.form.alias.$error.max).toBeTruthy();
});

it('should validate', function() {
changeInputValueTo('2000-01-01');
expect(inputElm).toBeValid();
expect(+scope.value).toBe(+new Date(2000, 0, 1));
expect(scope.form.alias.$error.max).toBeFalsy();
});
});

it('should validate even if max value changes on-the-fly', function(done) {
scope.max = '2013-01-01';
compileInput('<input type="date" ng-model="value" name="alias" max="{{max}}" />');
scope.$digest();

changeInputValueTo('2014-01-01');
expect(inputElm).toBeInvalid();

scope.max = '2001-01-01';
scope.$digest(function () {
expect(inputElm).toBeValid();
done();
});
});

it('should validate even if min value changes on-the-fly', function(done) {
scope.min = '2013-01-01';
compileInput('<input type="date" ng-model="value" name="alias" min="{{min}}" />');
scope.$digest();

changeInputValueTo('2010-01-01');
expect(inputElm).toBeInvalid();

scope.min = '2014-01-01';
scope.$digest(function () {
expect(inputElm).toBeValid();
done();
});
});
});

describe('number', function() {

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