feat(ngModelOptions): add timeStripZeroSeconds and timeSecondsFormat

Closes #10721
Closes #16510
Closes #16584
This commit is contained in:
Martin Staffa
2018-06-12 22:45:17 +01:00
committed by GitHub
parent c9a92fcad5
commit 83f7980e2f
3 changed files with 286 additions and 13 deletions
+33 -2
View File
@@ -255,6 +255,10 @@ var inputType = {
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
* The format of the displayed time can be adjusted with the
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
* and `timeStripZeroSeconds`.
*
* @param {string} ngModel Assignable AngularJS 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`.
@@ -356,7 +360,12 @@ var inputType = {
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
* this is the timezone of the browser.
*
* The format of the displayed time can be adjusted with the
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
* and `timeStripZeroSeconds`.
*
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
@@ -1491,6 +1500,8 @@ function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var isTimeType = type === 'time' || type === 'datetimelocal';
var previousDate;
var previousTimezone;
@@ -1514,11 +1525,13 @@ function createDateInputType(type, regexp, parseDate, format) {
if (isValidDate(value)) {
previousDate = value;
var timezone = ctrl.$options.getOption('timezone');
if (timezone) {
previousTimezone = timezone;
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
}
return $filter('date')(value, format, timezone);
return formatter(value, timezone);
} else {
previousDate = null;
previousTimezone = null;
@@ -1573,6 +1586,24 @@ function createDateInputType(type, regexp, parseDate, format) {
}
return parsedDate;
}
function formatter(value, timezone) {
var targetFormat = format;
if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
targetFormat = format
.replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
.replace(/:$/, '');
}
var formatted = $filter('date')(value, targetFormat, timezone);
if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
}
return formatted;
}
};
}
+85 -7
View File
@@ -406,12 +406,6 @@ defaultModelOptions = new ModelOptions({
* </example>
*
*
* ## Specifying timezones
*
* You can specify the timezone that date/time input directives expect by providing its name in the
* `timezone` property.
*
*
* ## Programmatically changing options
*
* The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
@@ -423,8 +417,70 @@ defaultModelOptions = new ModelOptions({
* Default events, extra triggers, and catch-all debounce values}.
*
*
* ## Specifying timezones
*
* You can specify the timezone that date/time input directives expect by providing its name in the
* `timezone` property.
*
*
* ## Formatting the value of `time` and `datetime-local`
*
* With the options `timeSecondsFormat` and `timeStripZeroSeconds` it is possible to adjust the value
* that is displayed in the control. Note that browsers may apply their own formatting
* in the user interface.
*
<example name="ngModelOptions-time-format" module="timeExample">
<file name="index.html">
<time-example></time-example>
</file>
<file name="script.js">
angular.module('timeExample', [])
.component('timeExample', {
templateUrl: 'timeExample.html',
controller: function() {
this.time = new Date(1970, 0, 1, 14, 57, 0);
this.options = {
timeSecondsFormat: 'ss',
timeStripZeroSeconds: true
};
this.optionChange = function() {
this.timeForm.timeFormatted.$overrideModelOptions(this.options);
this.time = new Date(this.time);
};
}
});
</file>
<file name="timeExample.html">
<form name="$ctrl.timeForm">
<strong>Default</strong>:
<input type="time" ng-model="$ctrl.time" step="any" /><br>
<strong>With options</strong>:
<input type="time" name="timeFormatted" ng-model="$ctrl.time" step="any" ng-model-options="$ctrl.options" />
<br>
Options:<br>
<code>timeSecondsFormat</code>:
<input
type="text"
ng-model="$ctrl.options.timeSecondsFormat"
ng-change="$ctrl.optionChange()">
<br>
<code>timeStripZeroSeconds</code>:
<input
type="checkbox"
ng-model="$ctrl.options.timeStripZeroSeconds"
ng-change="$ctrl.optionChange()">
</form>
</file>
* </example>
*
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
* and its descendents. Valid keys are:
* and its descendents.
*
* **General options**:
*
* - `updateOn`: string specifying which event should the input be bound to. You can set several
* events using an space delimited list. There is a special event called `default` that
* matches the default events belonging to the control. These are the events that are bound to
@@ -457,6 +513,10 @@ defaultModelOptions = new ModelOptions({
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
* `ngModel` as getters/setters.
*
*
* **Input-type specific options**:
*
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
* continental US time zone abbreviations, but for general use, use a time zone offset, for
@@ -465,6 +525,24 @@ defaultModelOptions = new ModelOptions({
* Note that changing the timezone will have no effect on the current date, and is only applied after
* the next input / model change.
*
* - `timeSecondsFormat`: Defines if the `time` and `datetime-local` types should show seconds and
* milliseconds. The option follows the format string of {@link date date filter}.
* By default, the options is `undefined` which is equal to `'ss.sss'` (seconds and milliseconds).
* The other options are `'ss'` (strips milliseconds), and `''` (empty string), which strips both
* seconds and milliseconds.
* Note that browsers that support `time` and `datetime-local` require the hour and minutes
* part of the time string, and may show the value differently in the user interface.
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
*
* - `timeStripZeroSeconds`: Defines if the `time` and `datetime-local` types should strip the
* seconds and milliseconds from the formatted value if they are zero. This option is applied
* after `timeSecondsFormat`.
* This option can be used to make the formatting consistent over different browsers, as some
* browsers with support for `time` will natively hide the milliseconds and
* seconds if they are zero, but others won't, and browsers that don't implement these input
* types will always show the full string.
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
*
*/
var ngModelOptionsDirective = function() {
NgModelOptionsController.$inject = ['$attrs', '$scope'];
+168 -4
View File
@@ -1384,6 +1384,88 @@ describe('input', function() {
expect($rootScope.form.alias.$error.datetimelocal).toBeTruthy();
});
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="datetime-local" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
);
var ctrl = inputElm.controller('ngModel');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:05');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50.050');
});
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="datetime-local" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
);
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:00.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
});
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
var inputElm = helper.compileInput('<input type="datetime-local"' +
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
' ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
});
describe('min', function() {
var inputElm;
beforeEach(function() {
@@ -1593,7 +1675,7 @@ describe('input', function() {
});
it('should set the view if the model if a valid Date object.', function() {
it('should set the view if the model is a valid Date object.', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
@@ -1604,7 +1686,7 @@ describe('input', function() {
});
it('should set the model undefined if the view is invalid', function() {
it('should set the model to undefined if the view is invalid', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="breakMe"/>');
$rootScope.$apply(function() {
@@ -1623,7 +1705,7 @@ describe('input', function() {
});
it('should render as blank if null', function() {
it('should set blank if null', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
$rootScope.$apply('test = null');
@@ -1633,7 +1715,7 @@ describe('input', function() {
});
it('should come up blank when no value specified', function() {
it('should set blank when no value specified', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
expect(inputElm.val()).toBe('');
@@ -1644,6 +1726,88 @@ describe('input', function() {
expect(inputElm.val()).toBe('');
});
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="time" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
);
var ctrl = inputElm.controller('ngModel');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
});
expect(inputElm.val()).toBe('15:41:05');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
});
expect(inputElm.val()).toBe('15:41:50.050');
});
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="time" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
);
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41:50.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41:00.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
});
expect(inputElm.val()).toBe('15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
});
expect(inputElm.val()).toBe('15:41');
});
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
var inputElm = helper.compileInput('<input type="time"' +
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
' ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41');
});
it('should parse empty string to null', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');