/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ng/directive/form.js
727 строк
26 KB
Martin Staffa
feat(form.FormController): add $getControls()
18 июн 2018, 17:02
Не верифицирован
18 июн 2018, 17:02
5b11145
Код
Авторство
О чём код?
'use strict'; /* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, $getControls: valueFn([]), $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, $setPristine: noop, $setSubmitted: noop, $$setSubmitted: noop }, PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { control.$name = name; } /** * @ngdoc type * @name form.FormController * * @property {boolean} $pristine True if user has not interacted with the form yet. * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. * @property {boolean} $submitted True if user has submitted the form even if its invalid. * * @property {Object} $pending An object hash, containing references to controls or forms with * pending validators, where: * * - keys are validations tokens (error names). * - values are arrays of controls or forms that have a pending validator for the given error name. * * See {@link form.FormController#$error $error} for a list of built-in validation tokens. * * @property {Object} $error An object hash, containing references to controls or forms with failing * validators, where: * * - keys are validation tokens (error names), * - values are arrays of controls or forms that have a failing validator for the given error name. * * Built-in validation tokens: * - `email` * - `max` * - `maxlength` * - `min` * - `minlength` * - `number` * - `pattern` * - `required` * - `url` * - `date` * - `datetimelocal` * - `time` * - `week` * - `month` * * @description * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance * of `FormController`. * */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; function FormController($element, $attrs, $scope, $animate, $interpolate) { this.$$controls = []; // init state this.$error = {}; this.$$success = {}; this.$pending = undefined; this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); this.$dirty = false; this.$pristine = true; this.$valid = true; this.$invalid = false; this.$submitted = false; this.$$parentForm = nullFormCtrl; this.$$element = $element; this.$$animate = $animate; setupValidity(this); } FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue * * @description * Rollback all form controls pending updates to the `$modelValue`. * * Updates may be pending by a debounced event or because the input is waiting for a some future * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ $rollbackViewValue: function() { forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); }, /** * @ngdoc method * @name form.FormController#$commitViewValue * * @description * Commit all form controls pending updates to the `$modelValue`. * * Updates may be pending by a debounced event or because the input is waiting for a some future * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ $commitViewValue: function() { forEach(this.$$controls, function(control) { control.$commitViewValue(); }); }, /** * @ngdoc method * @name form.FormController#$addControl * @param {object} control control object, either a {@link form.FormController} or an * {@link ngModel.NgModelController} * * @description * Register a control with the form. Input elements using ngModelController do this automatically * when they are linked. * * Note that the current state of the control will not be reflected on the new parent form. This * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine` * state. * * However, if the method is used programmatically, for example by adding dynamically created controls, * or controls that have been previously removed without destroying their corresponding DOM element, * it's the developers responsibility to make sure the current state propagates to the parent form. * * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); this.$$controls.push(control); if (control.$name) { this[control.$name] = control; } control.$$parentForm = this; }, /** * @ngdoc method * @name form.FormController#$getControls * @returns {Array} the controls that are currently part of this form * * @description * This method returns a **shallow copy** of the controls that are currently part of this form. * The controls can be instances of {@link form.FormController `FormController`} * ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}. * If you need access to the controls of child-forms, you have to call `$getControls()` * recursively on them. * This can be used for example to iterate over all controls to validate them. * * The controls can be accessed normally, but adding to, or removing controls from the array has * no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and * {@link form.FormController#$removeControl `$removeControl()`} for this use-case. * Likewise, adding a control to, or removing a control from the form is not reflected * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time * you need access to the controls. */ $getControls: function() { return shallowCopy(this.$$controls); }, // Private API: rename a form control $$renameControl: function(control, newName) { var oldName = control.$name; if (this[oldName] === control) { delete this[oldName]; } this[newName] = control; control.$name = newName; }, /** * @ngdoc method * @name form.FormController#$removeControl * @param {object} control control object, either a {@link form.FormController} or an * {@link ngModel.NgModelController} * * @description * Deregister a control from the form. * * Input elements using ngModelController do this automatically when they are destroyed. * * Note that only the removed control's validation state (`$errors`etc.) will be removed from the * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ $removeControl: function(control) { if (control.$name && this[control.$name] === control) { delete this[control.$name]; } forEach(this.$pending, function(value, name) { // eslint-disable-next-line no-invalid-this this.$setValidity(name, null, control); }, this); forEach(this.$error, function(value, name) { // eslint-disable-next-line no-invalid-this this.$setValidity(name, null, control); }, this); forEach(this.$$success, function(value, name) { // eslint-disable-next-line no-invalid-this this.$setValidity(name, null, control); }, this); arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; }, /** * @ngdoc method * @name form.FormController#$setDirty * * @description * Sets the form to a dirty state. * * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ $setDirty: function() { this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); this.$$animate.addClass(this.$$element, DIRTY_CLASS); this.$dirty = true; this.$pristine = false; this.$$parentForm.$setDirty(); }, /** * @ngdoc method * @name form.FormController#$setPristine * * @description * Sets the form to its pristine state. * * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` * state to false. * * This method will also propagate to all the controls contained in this form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ $setPristine: function() { this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); this.$dirty = false; this.$pristine = true; this.$submitted = false; forEach(this.$$controls, function(control) { control.$setPristine(); }); }, /** * @ngdoc method * @name form.FormController#$setUntouched * * @description * Sets the form to its untouched state. * * This method can be called to remove the 'ng-touched' class and set the form controls to their * untouched state (ng-untouched class). * * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ $setUntouched: function() { forEach(this.$$controls, function(control) { control.$setUntouched(); }); }, /** * @ngdoc method * @name form.FormController#$setSubmitted * * @description * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and * parent forms of the form. */ $setSubmitted: function() { var rootForm = this; while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) { rootForm = rootForm.$$parentForm; } rootForm.$$setSubmitted(); }, $$setSubmitted: function() { this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); this.$submitted = true; forEach(this.$$controls, function(control) { if (control.$$setSubmitted) { control.$$setSubmitted(); } }); } }; /** * @ngdoc method * @name form.FormController#$setValidity * * @description * Change the validity state of the form, and notify the parent form (if any). * * Application developers will rarely need to call this method directly. It is used internally, by * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a * control's validity state to the parent `FormController`. * * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for * unfulfilled `$asyncValidators`), so that it is available for data-binding. The * `validationErrorKey` should be in camelCase and will get converted into dash-case for * class name. Example: `myError` will result in `ng-valid-my-error` and * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`. * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`. * Skipped is used by AngularJS when validators do not run because of parse errors and when * `$asyncValidators` do not run because any of the `$validators` failed. * @param {NgModelController | FormController} controller - The controller whose validity state is * triggering the change. */ addSetValidityMethod({ clazz: FormController, set: function(object, property, controller) { var list = object[property]; if (!list) { object[property] = [controller]; } else { var index = list.indexOf(controller); if (index === -1) { list.push(controller); } } }, unset: function(object, property, controller) { var list = object[property]; if (!list) { return; } arrayRemove(list, controller); if (list.length === 0) { delete object[property]; } } }); /** * @ngdoc directive * @name ngForm * @restrict EAC * * @description * Helper directive that makes it possible to create control groups inside a * {@link ng.directive:form `form`} directive. * These "child forms" can be used, for example, to determine the validity of a sub-group of * controls. * * <div class="alert alert-danger"> * **Note**: `ngForm` cannot be used as a replacement for `<form>`, because it lacks its * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element). * Specifically, you cannot submit `ngForm` like a `<form>` tag. That means, * you cannot send data to the server with `ngForm`, or integrate it with * {@link ng.directive:ngSubmit `ngSubmit`}. * </div> * * @param {string=} ngForm|name Name of the form. If specified, the form controller will * be published into the related scope, under this name. * */ /** * @ngdoc directive * @name form * @restrict E * * @description * Directive that instantiates * {@link form.FormController FormController}. * * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * * ## Alias: {@link ng.directive:ngForm `ngForm`} * * In AngularJS, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so * AngularJS provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group * of controls needs to be determined. * * ## CSS classes * - `ng-valid` is set if the form is valid. * - `ng-invalid` is set if the form is invalid. * - `ng-pending` is set if the form is pending. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. * - `ng-submitted` is set if the form was submitted. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * * * ## Submitting a form and preventing the default action * * Since the role of forms in client-side AngularJS applications is different than in classical * roundtrip apps, it is desirable for the browser not to translate the form submission into a full * page reload that sends the data to the server. Instead some javascript logic should be triggered * to handle the form submission in an application-specific way. * * For this reason, AngularJS prevents the default action (form submission to the server) unless the * `<form>` element has an `action` attribute specified. * * You can use one of the following two ways to specify what javascript method should be called when * a form is submitted: * * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element * - {@link ng.directive:ngClick ngClick} directive on the first * button or input field of type submit (input[type=submit]) * * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} * or {@link ng.directive:ngClick ngClick} directives. * This is because of the following form submission rules in the HTML specification: * * - If a form has only one input field then hitting enter in this field triggers form submit * (`ngSubmit`) * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter * doesn't trigger submit * - if a form has one or more input fields and one or more buttons or input[type=submit] then * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * * @animations * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any * other validations that are performed within the form. Animations in ngForm are similar to how * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well * as JS animations. * * The following example shows a simple way to utilize CSS transitions to style a form element * that has been rendered as invalid after it has been validated: * * <pre> * //be sure to include ngAnimate as a module to hook into more * //advanced animations * .my-form { * transition:0.5s linear all; * background: white; * } * .my-form.ng-invalid { * background: red; * color:white; * } * </pre> * * @example <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> angular.module('formExample', []) .controller('FormController', ['$scope', function($scope) { $scope.userType = 'guest'; }]); </script> <style> .my-form { transition:all linear 0.5s; background: transparent; } .my-form.ng-invalid { background: red; } </style> <form name="myForm" ng-controller="FormController" class="my-form"> userType: <input name="input" ng-model="userType" required> <span class="error" ng-show="myForm.input.$error.required">Required!</span><br> <code>userType = {{userType}}</code><br> <code>myForm.input.$valid = {{myForm.input.$valid}}</code><br> <code>myForm.input.$error = {{myForm.input.$error}}</code><br> <code>myForm.$valid = {{myForm.$valid}}</code><br> <code>myForm.$error.required = {{!!myForm.$error.required}}</code><br> </form> </file> <file name="protractor.js" type="protractor"> it('should initialize to model', function() { var userType = element(by.binding('userType')); var valid = element(by.binding('myForm.input.$valid')); expect(userType.getText()).toContain('guest'); expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { var userType = element(by.binding('userType')); var valid = element(by.binding('myForm.input.$valid')); var userInput = element(by.model('userType')); userInput.clear(); userInput.sendKeys(''); expect(userType.getText()).toEqual('userType ='); expect(valid.getText()).toContain('false'); }); </file> </example> * * @param {string=} name Name of the form. If specified, the form controller will be published into * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', '$parse', function($timeout, $parse) { var formDirective = { name: 'form', restrict: isNgForm ? 'EAC' : 'E', require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form controller: FormController, compile: function ngFormCompile(formElement, attr) { // Setup initial state of the control formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); return { pre: function ngFormPreLink(scope, formElement, attr, ctrls) { var controller = ctrls[0]; // if `action` attr is not present on the form, prevent the default action (submission) if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. var handleFormSubmission = function(event) { scope.$apply(function() { controller.$commitViewValue(); controller.$setSubmitted(); }); event.preventDefault(); }; formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } var parentFormCtrl = ctrls[1] || controller.$$parentForm; parentFormCtrl.$addControl(controller); var setter = nameAttr ? getSetter(controller.$name) : noop; if (nameAttr) { setter(scope, controller); attr.$observe(nameAttr, function(newValue) { if (controller.$name === newValue) return; setter(scope, undefined); controller.$$parentForm.$$renameControl(controller, newValue); setter = getSetter(controller.$name); setter(scope, controller); }); } formElement.on('$destroy', function() { controller.$$parentForm.$removeControl(controller); setter(scope, undefined); extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); } }; } }; return formDirective; function getSetter(expression) { if (expression === '') { //create an assignable expression, so forms with an empty name can be renamed later return $parse('this[""]').assign; } return $parse(expression).assign || noop; } }]; }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); // helper methods function setupValidity(instance) { instance.$$classCache = {}; instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); } function addSetValidityMethod(context) { var clazz = context.clazz, set = context.set, unset = context.unset; clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { if (isUndefined(state)) { createAndSet(this, '$pending', validationErrorKey, controller); } else { unsetAndCleanup(this, '$pending', validationErrorKey, controller); } if (!isBoolean(state)) { unset(this.$error, validationErrorKey, controller); unset(this.$$success, validationErrorKey, controller); } else { if (state) { unset(this.$error, validationErrorKey, controller); set(this.$$success, validationErrorKey, controller); } else { set(this.$error, validationErrorKey, controller); unset(this.$$success, validationErrorKey, controller); } } if (this.$pending) { cachedToggleClass(this, PENDING_CLASS, true); this.$valid = this.$invalid = undefined; toggleValidationCss(this, '', null); } else { cachedToggleClass(this, PENDING_CLASS, false); this.$valid = isObjectEmpty(this.$error); this.$invalid = !this.$valid; toggleValidationCss(this, '', this.$valid); } // re-read the state as the set/unset methods could have // combined state in this.$error[validationError] (used for forms), // where setting/unsetting only increments/decrements the value, // and does not replace it. var combinedState; if (this.$pending && this.$pending[validationErrorKey]) { combinedState = undefined; } else if (this.$error[validationErrorKey]) { combinedState = false; } else if (this.$$success[validationErrorKey]) { combinedState = true; } else { combinedState = null; } toggleValidationCss(this, validationErrorKey, combinedState); this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); }; function createAndSet(ctrl, name, value, controller) { if (!ctrl[name]) { ctrl[name] = {}; } set(ctrl[name], value, controller); } function unsetAndCleanup(ctrl, name, value, controller) { if (ctrl[name]) { unset(ctrl[name], value, controller); } if (isObjectEmpty(ctrl[name])) { ctrl[name] = undefined; } } function cachedToggleClass(ctrl, className, switchValue) { if (switchValue && !ctrl.$$classCache[className]) { ctrl.$$animate.addClass(ctrl.$$element, className); ctrl.$$classCache[className] = true; } else if (!switchValue && ctrl.$$classCache[className]) { ctrl.$$animate.removeClass(ctrl.$$element, className); ctrl.$$classCache[className] = false; } } function toggleValidationCss(ctrl, validationErrorKey, isValid) { validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); } } function isObjectEmpty(obj) { if (obj) { for (var prop in obj) { if (obj.hasOwnProperty(prop)) { return false; } } } return true; }