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

Added support for <input type="datetime-local"/> #5422

Closed
wants to merge 2 commits 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
275 changes: 273 additions & 2 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
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 DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;

var inputType = {

Expand Down Expand Up @@ -89,6 +91,137 @@ 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
* @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 = '2010-12-28T14:57';
}
</script>
<form name="myForm" ng-controller="Ctrl as dateCtrl">
Pick a date in 2013:
<input type="datetime-local" name="input" ng-model="value"
placeholder="yyyy-MM-ddTHH:mm" min="2013-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
Expand Down Expand Up @@ -530,6 +663,144 @@ function textInputType(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) {
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)) {
return $filter('date')(value, 'yyyy-MM-dd');
}
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 Expand Up @@ -772,14 +1043,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
Loading