/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ng/directive/ngModelOptions.js
580 строк
22 KB
Martin Staffa
feat(ngModelOptions): add `timeStripZeroSeconds` and `timeSecondsFormat`
13 июн 2018, 00:45
Не верифицирован
13 июн 2018, 00:45
83f7980
Код
Авторство
О чём код?
'use strict'; /* exported defaultModelOptions */ var defaultModelOptions; var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; /** * @ngdoc type * @name ModelOptions * @description * A container for the options set by the {@link ngModelOptions} directive */ function ModelOptions(options) { this.$$options = options; } ModelOptions.prototype = { /** * @ngdoc method * @name ModelOptions#getOption * @param {string} name the name of the option to retrieve * @returns {*} the value of the option * @description * Returns the value of the given option */ getOption: function(name) { return this.$$options[name]; }, /** * @ngdoc method * @name ModelOptions#createChild * @param {Object} options a hash of options for the new child that will override the parent's options * @return {ModelOptions} a new `ModelOptions` object initialized with the given options. */ createChild: function(options) { var inheritAll = false; // make a shallow copy options = extend({}, options); // Inherit options from the parent if specified by the value `"$inherit"` forEach(options, /** @this */ function(option, key) { if (option === '$inherit') { if (key === '*') { inheritAll = true; } else { options[key] = this.$$options[key]; // `updateOn` is special so we must also inherit the `updateOnDefault` option if (key === 'updateOn') { options.updateOnDefault = this.$$options.updateOnDefault; } } } else { if (key === 'updateOn') { // If the `updateOn` property contains the `default` event then we have to remove // it from the event list and set the `updateOnDefault` flag. options.updateOnDefault = false; options[key] = trim(option.replace(DEFAULT_REGEXP, function() { options.updateOnDefault = true; return ' '; })); } } }, this); if (inheritAll) { // We have a property of the form: `"*": "$inherit"` delete options['*']; defaults(options, this.$$options); } // Finally add in any missing defaults defaults(options, defaultModelOptions.$$options); return new ModelOptions(options); } }; defaultModelOptions = new ModelOptions({ updateOn: '', updateOnDefault: true, debounce: 0, getterSetter: false, allowInvalid: false, timezone: null }); /** * @ngdoc directive * @name ngModelOptions * @restrict A * @priority 10 * * @description * This directive allows you to modify the behaviour of {@link ngModel} directives within your * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel} * directives will use the options of their nearest `ngModelOptions` ancestor. * * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as * an AngularJS expression. This expression should evaluate to an object, whose properties contain * the settings. For example: `<div ng-model-options="{ debounce: 100 }"`. * * ## Inheriting Options * * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions` * directive by giving it the value of `"$inherit"`. * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings * will be used. * * For example given the following fragment of HTML * * * ```html * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> * <form ng-model-options="{ updateOn: 'blur', allowInvalid: '$inherit' }"> * <input ng-model-options="{ updateOn: 'default', allowInvalid: '$inherit' }" /> * </form> * </div> * ``` * * the `input` element will have the following settings * * ```js * { allowInvalid: true, updateOn: 'default', debounce: 0 } * ``` * * Notice that the `debounce` setting was not inherited and used the default value instead. * * You can specify that all undefined settings are automatically inherited from an ancestor by * including a property with key of `"*"` and value of `"$inherit"`. * * For example given the following fragment of HTML * * * ```html * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> * <form ng-model-options="{ updateOn: 'blur', "*": '$inherit' }"> * <input ng-model-options="{ updateOn: 'default', "*": '$inherit' }" /> * </form> * </div> * ``` * * the `input` element will have the following settings * * ```js * { allowInvalid: true, updateOn: 'default', debounce: 200 } * ``` * * Notice that the `debounce` setting now inherits the value from the outer `<div>` element. * * If you are creating a reusable component then you should be careful when using `"*": "$inherit"` * since you may inadvertently inherit a setting in the future that changes the behavior of your component. * * * ## Triggering and debouncing model updates * * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will * trigger a model update and/or a debouncing delay so that the actual update only takes place when * a timer expires; this timer will be reset after another change takes place. * * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might * be different from the value in the actual model. This means that if you update the model you * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in * order to make sure it is synchronized with the model and that any debounced action is canceled. * * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue} * method is by making sure the input is placed inside a form that has a `name` attribute. This is * important because `form` controllers are published to the related scope under the name in their * `name` attribute. * * Any pending changes will take place immediately when an enclosing form is submitted via the * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * * ### Overriding immediate updates * * The following example shows how to override immediate updates. Changes on the inputs within the * form will update the model only when the control loses focus (blur event). If `escape` key is * pressed while the input field is focused, the value is reset to the value in the current model. * * <example name="ngModelOptions-directive-blur" module="optionsExample"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="userForm"> * <label> * Name: * <input type="text" name="userName" * ng-model="user.name" * ng-model-options="{ updateOn: 'blur' }" * ng-keyup="cancel($event)" /> * </label><br /> * <label> * Other data: * <input type="text" ng-model="user.data" /> * </label><br /> * </form> * <pre>user.name = <span ng-bind="user.name"></span></pre> * </div> * </file> * <file name="app.js"> * angular.module('optionsExample', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.user = { name: 'say', data: '' }; * * $scope.cancel = function(e) { * if (e.keyCode === 27) { * $scope.userForm.userName.$rollbackViewValue(); * } * }; * }]); * </file> * <file name="protractor.js" type="protractor"> * var model = element(by.binding('user.name')); * var input = element(by.model('user.name')); * var other = element(by.model('user.data')); * * it('should allow custom events', function() { * input.sendKeys(' hello'); * input.click(); * expect(model.getText()).toEqual('say'); * other.click(); * expect(model.getText()).toEqual('say hello'); * }); * * it('should $rollbackViewValue when model changes', function() { * input.sendKeys(' hello'); * expect(input.getAttribute('value')).toEqual('say hello'); * input.sendKeys(protractor.Key.ESCAPE); * expect(input.getAttribute('value')).toEqual('say'); * other.click(); * expect(model.getText()).toEqual('say'); * }); * </file> * </example> * * ### Debouncing updates * * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. * * <example name="ngModelOptions-directive-debounce" module="optionsExample"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="userForm"> * Name: * <input type="text" name="userName" * ng-model="user.name" * ng-model-options="{ debounce: 1000 }" /> * <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br /> * </form> * <pre>user.name = <span ng-bind="user.name"></span></pre> * </div> * </file> * <file name="app.js"> * angular.module('optionsExample', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.user = { name: 'say' }; * }]); * </file> * </example> * * ### Default events, extra triggers, and catch-all debounce values * * This example shows the relationship between "default" update events and * additional `updateOn` triggers. * * `default` events are those that are bound to the control, and when fired, update the `$viewValue` * via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed * in `updateOn` is considered a "default" event, since different control types have different * default events. * * The control in this example updates by "default", "click", and "blur", with different `debounce` * values. You can see that "click" doesn't have an individual `debounce` value - * therefore it uses the `*` debounce value. * * There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue} * directly with a "custom" event. Since "custom" is not defined in the `updateOn` list, * it is considered a "default" event and will update the * control if "default" is defined in `updateOn`, and will receive the "default" debounce value. * Note that this is just to illustrate how custom controls would possibly call `$setViewValue`. * * You can change the `updateOn` and `debounce` configuration to test different scenarios. This * is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}. * <example name="ngModelOptions-advanced" module="optionsExample"> <file name="index.html"> <model-update-demo></model-update-demo> </file> <file name="app.js"> angular.module('optionsExample', []) .component('modelUpdateDemo', { templateUrl: 'template.html', controller: function() { this.name = 'Chinua'; this.options = { updateOn: 'default blur click', debounce: { default: 2000, blur: 0, '*': 1000 } }; this.updateEvents = function() { var eventList = this.options.updateOn.split(' '); eventList.push('*'); var events = {}; for (var i = 0; i < eventList.length; i++) { events[eventList[i]] = this.options.debounce[eventList[i]]; } this.events = events; }; this.updateOptions = function() { var options = angular.extend(this.options, { updateOn: Object.keys(this.events).join(' ').replace('*', ''), debounce: this.events }); this.form.input.$overrideModelOptions(options); }; // Initialize the event form this.updateEvents(); } }); </file> <file name="template.html"> <form name="$ctrl.form"> Input: <input type="text" name="input" ng-model="$ctrl.name" ng-model-options="$ctrl.options" /> </form> Model: <tt>{{$ctrl.name}}</tt> <hr> <button ng-click="$ctrl.form.input.$setViewValue('some value', 'custom')">Trigger setViewValue with 'some value' and 'custom' event</button> <hr> <form ng-submit="$ctrl.updateOptions()"> <b>updateOn</b><br> <input type="text" ng-model="$ctrl.options.updateOn" ng-change="$ctrl.updateEvents()" ng-model-options="{debounce: 500}"> <table> <tr> <th>Option</th> <th>Debounce value</th> </tr> <tr ng-repeat="(key, value) in $ctrl.events"> <td>{{key}}</td> <td><input type="number" ng-model="$ctrl.events[key]" /></td> </tr> </table> <br> <input type="submit" value="Update options"> </form> </file> </example> * * * ## Model updates and validation * * The default behaviour in `ngModel` is that the model value is set to `undefined` when the * validation determines that the value is invalid. By setting the `allowInvalid` property to true, * the model will still be updated even if the value is invalid. * * * ## Connecting to the scope * * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression * on the scope refers to a "getter/setter" function rather than the value itself. * * The following example shows how to bind to getter/setters: * * <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="userForm"> * <label> * Name: * <input type="text" name="userName" * ng-model="user.name" * ng-model-options="{ getterSetter: true }" /> * </label> * </form> * <pre>user.name = <span ng-bind="user.name()"></span></pre> * </div> * </file> * <file name="app.js"> * angular.module('getterSetterExample', []) * .controller('ExampleController', ['$scope', function($scope) { * var _name = 'Brian'; * $scope.user = { * name: function(newName) { * return angular.isDefined(newName) ? (_name = newName) : _name; * } * }; * }]); * </file> * </example> * * * ## Programmatically changing options * * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not * watched for changes. However, it is possible to override the options on a single * {@link ngModel.NgModelController} instance with * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}. * See also the example for * {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values * 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. * * **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 * the control, and when fired, update the `$viewValue` via `$setViewValue`. * * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event, * since different control types use different default events. * * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates * Triggering and debouncing model updates}. * * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: * ``` * ng-model-options="{ * updateOn: 'default blur', * debounce: { 'default': 500, 'blur': 0 } * }" * ``` * You can use the `*` key to specify a debounce value that applies to all events that are not * specifically listed. In the following example, `mouseup` would have a debounce delay of 1000: * ``` * ng-model-options="{ * updateOn: 'default blur mouseup', * debounce: { 'default': 500, 'blur': 0, '*': 1000 } * }" * ``` * - `allowInvalid`: boolean value which indicates that the model can be set with values that did * 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 * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) * If not specified, the timezone of the browser will be used. * 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']; function NgModelOptionsController($attrs, $scope) { this.$$attrs = $attrs; this.$$scope = $scope; } NgModelOptionsController.prototype = { $onInit: function() { var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions; var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); this.$options = parentOptions.createChild(modelOptionsDefinition); } }; return { restrict: 'A', // ngModelOptions needs to run before ngModel and input directives priority: 10, require: {parentCtrl: '?^^ngModelOptions'}, bindToController: true, controller: NgModelOptionsController }; }; // shallow copy over values from `src` that are not already specified on `dst` function defaults(dst, src) { forEach(src, function(value, key) { if (!isDefined(dst[key])) { dst[key] = value; } }); }