Skip to content

Commit

Permalink
feat(input) add support for datetime-local
Browse files Browse the repository at this point in the history
partially closes angular#757
  • Loading branch information
benlesh committed Dec 17, 2013
1 parent bfcc0c8 commit e60a44c
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 10 deletions.
150 changes: 140 additions & 10 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\
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 DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;

var inputType = {

Expand Down Expand Up @@ -156,6 +157,71 @@ var inputType = {
*/
'date': dateInputType,

/**
* @ngdoc inputType
* @name ng.directive:input.dateTimeLocal
*
* @description
* HTML5 or text input with datetime 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
* local datetime format (yyyy-MM-ddTHH:mm), for example: `2010-12-28T14:57`. Will also accept a valid ISO
* datetime string 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 = new Date(2010, 11, 28, 14, 57); // or '2010-12-28T14:57';
}
</script>
<form name="myForm" ng-controller="Ctrl as dateCtrl">
Pick a date between in 2013:
<input type="datetime-local" name="input" ng-model="value"
placeholder="yyyy-MM-ddTHH:mm" min="2001-01-01T00:00" max="2013-12-31T00:00" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.datetimelocal">
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('2010-12-28T14:57');
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-01T23:59');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
'datetime-local': dateTimeLocalInputType,
/**
* @ngdoc inputType
* @name ng.directive:input.number
Expand Down Expand Up @@ -603,7 +669,77 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}

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

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

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

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

ctrl.$formatters.push(function(value) {
if(isDate(value)) {
return $filter('date')(value, 'yyyy-MM-ddTHH:mm');
}
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)) {
DATETIMELOCAL_REGEXP.lastIndex = 0;
var parts = DATETIMELOCAL_REGEXP.exec(iso),
yyyy = +parts[1],
MM = +parts[2] - 1,
dd = +parts[3],
HH = +parts[4],
mm = +parts[5];

return +new Date(yyyy, MM, dd, HH, mm);
}

return NaN;
}
}

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

ctrl.$parsers.push(function(value) {
Expand All @@ -623,13 +759,7 @@ function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) {

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 $filter('date')(value, 'yyyy-MM-dd');
}
return ctrl.$isEmpty(value) ? '' : '' + value;
});
Expand Down Expand Up @@ -950,14 +1080,14 @@ function checkboxInputType(scope, element, attr, ctrl) {
</doc:scenario>
</doc:example>
*/
var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) {
return {
restrict: 'E',
require: '?ngModel',
link: function(scope, element, attr, ctrl) {
if (ctrl) {
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
$browser);
$browser, $filter);
}
}
};
Expand Down
117 changes: 117 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,123 @@ describe('input', function() {

// INPUT TYPES


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

scope.$apply(function(){
scope.lunchtime = '2013-12-16T11:30';
});

expect(inputElm.val()).toBe('2013-12-16T11:30');
});

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

scope.$apply(function (){
scope.tenSecondsToNextYear = new Date(2013, 11, 31, 23, 59);
});

expect(inputElm.val()).toBe('2013-12-31T23:59');
});

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

scope.$apply(function (){
scope.breakMe = new Date(2009, 0, 6, 16, 25);
});

expect(inputElm.val()).toBe('2009-01-06T16:25');

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

changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
expect(scope.breakMe).toBeUndefined();
expect(inputElm).toBeInvalid();
});

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

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

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

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

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

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

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

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

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

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

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

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

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

0 comments on commit e60a44c

Please sign in to comment.