mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-19 08:00:23 +00:00
330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
|
|
var nullFormCtrl = {
|
|
$addControl: noop,
|
|
$removeControl: noop,
|
|
$setValidity: noop,
|
|
$setDirty: noop
|
|
};
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name ng.directive: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 {Object} $error Is an object hash, containing references to all invalid controls or
|
|
* forms, where:
|
|
*
|
|
* - keys are validation tokens (error names) — such as `required`, `url` or `email`),
|
|
* - values are arrays of controls or forms that are invalid with given error.
|
|
*
|
|
* @description
|
|
* `FormController` keeps track of all its controls and nested forms as well as 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'];
|
|
function FormController(element, attrs) {
|
|
var form = this,
|
|
parentForm = element.parent().controller('form') || nullFormCtrl,
|
|
invalidCount = 0, // used to easily determine if we are valid
|
|
errors = form.$error = {};
|
|
|
|
// init state
|
|
form.$name = attrs.name;
|
|
form.$dirty = false;
|
|
form.$pristine = true;
|
|
form.$valid = true;
|
|
form.$invalid = false;
|
|
|
|
parentForm.$addControl(form);
|
|
|
|
// Setup initial state of the control
|
|
element.addClass(PRISTINE_CLASS);
|
|
toggleValidCss(true);
|
|
|
|
// convenience method for easy toggling of classes
|
|
function toggleValidCss(isValid, validationErrorKey) {
|
|
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
|
|
element.
|
|
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
|
|
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
|
|
}
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.directive:form.FormController#$addControl
|
|
* @methodOf ng.directive:form.FormController
|
|
*
|
|
* @description
|
|
* Register a control with the form.
|
|
*
|
|
* Input elements using ngModelController do this automatically when they are linked.
|
|
*/
|
|
form.$addControl = function(control) {
|
|
if (control.$name && !form.hasOwnProperty(control.$name)) {
|
|
form[control.$name] = control;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.directive:form.FormController#$removeControl
|
|
* @methodOf ng.directive:form.FormController
|
|
*
|
|
* @description
|
|
* Deregister a control from the form.
|
|
*
|
|
* Input elements using ngModelController do this automatically when they are destroyed.
|
|
*/
|
|
form.$removeControl = function(control) {
|
|
if (control.$name && form[control.$name] === control) {
|
|
delete form[control.$name];
|
|
}
|
|
forEach(errors, function(queue, validationToken) {
|
|
form.$setValidity(validationToken, true, control);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.directive:form.FormController#$setValidity
|
|
* @methodOf ng.directive:form.FormController
|
|
*
|
|
* @description
|
|
* Sets the validity of a form control.
|
|
*
|
|
* This method will also propagate to parent forms.
|
|
*/
|
|
form.$setValidity = function(validationToken, isValid, control) {
|
|
var queue = errors[validationToken];
|
|
|
|
if (isValid) {
|
|
if (queue) {
|
|
arrayRemove(queue, control);
|
|
if (!queue.length) {
|
|
invalidCount--;
|
|
if (!invalidCount) {
|
|
toggleValidCss(isValid);
|
|
form.$valid = true;
|
|
form.$invalid = false;
|
|
}
|
|
errors[validationToken] = false;
|
|
toggleValidCss(true, validationToken);
|
|
parentForm.$setValidity(validationToken, true, form);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if (!invalidCount) {
|
|
toggleValidCss(isValid);
|
|
}
|
|
if (queue) {
|
|
if (includes(queue, control)) return;
|
|
} else {
|
|
errors[validationToken] = queue = [];
|
|
invalidCount++;
|
|
toggleValidCss(false, validationToken);
|
|
parentForm.$setValidity(validationToken, false, form);
|
|
}
|
|
queue.push(control);
|
|
|
|
form.$valid = false;
|
|
form.$invalid = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.directive:form.FormController#$setDirty
|
|
* @methodOf ng.directive:form.FormController
|
|
*
|
|
* @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.
|
|
*/
|
|
form.$setDirty = function() {
|
|
element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
|
|
form.$dirty = true;
|
|
form.$pristine = false;
|
|
parentForm.$setDirty();
|
|
};
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ng.directive:ngForm
|
|
* @restrict EAC
|
|
*
|
|
* @description
|
|
* Nestable alias of {@link ng.directive:form `form`} directive. HTML
|
|
* does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
|
|
* sub-group of controls needs to be determined.
|
|
*
|
|
* @param {string=} name|ngForm Name of the form. If specified, the form controller will be published into
|
|
* related scope, under this name.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ng.directive:form
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* Directive that instantiates
|
|
* {@link ng.directive:form.FormController FormController}.
|
|
*
|
|
* If `name` attribute is specified, the form controller is published onto the current scope under
|
|
* this name.
|
|
*
|
|
* # Alias: {@link ng.directive:ngForm `ngForm`}
|
|
*
|
|
* In angular 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, for this
|
|
* reason angular provides {@link ng.directive:ngForm `ngForm`} alias
|
|
* which behaves identical to `<form>` but allows form nesting.
|
|
*
|
|
*
|
|
* # CSS classes
|
|
* - `ng-valid` Is set if the form is valid.
|
|
* - `ng-invalid` Is set if the form is invalid.
|
|
* - `ng-pristine` Is set if the form is pristine.
|
|
* - `ng-dirty` Is set if the form is dirty.
|
|
*
|
|
*
|
|
* # Submitting a form and preventing default action
|
|
*
|
|
* Since the role of forms in client-side Angular 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 application specific way.
|
|
*
|
|
* For this reason, Angular 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 ngSubmit or ngClick directives. This
|
|
* is because of the following form submission rules coming from the html spec:
|
|
*
|
|
* - If a form has only one input field then hitting enter in this field triggers form submit
|
|
* (`ngSubmit`)
|
|
* - if a form has 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`)
|
|
*
|
|
* @param {string=} name Name of the form. If specified, the form controller will be published into
|
|
* related scope, under this name.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.userType = 'guest';
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
userType: <input name="input" ng-model="userType" required>
|
|
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
|
|
<tt>userType = {{userType}}</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('userType')).toEqual('guest');
|
|
expect(binding('myForm.input.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('userType').enter('');
|
|
expect(binding('userType')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
var formDirectiveFactory = function(isNgForm) {
|
|
return ['$timeout', function($timeout) {
|
|
var formDirective = {
|
|
name: 'form',
|
|
restrict: 'E',
|
|
controller: FormController,
|
|
compile: function() {
|
|
return {
|
|
pre: function(scope, formElement, attr, controller) {
|
|
if (!attr.action) {
|
|
// 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 preventDefaultListener = function(event) {
|
|
event.preventDefault
|
|
? event.preventDefault()
|
|
: event.returnValue = false; // IE
|
|
};
|
|
|
|
addEventListenerFn(formElement[0], 'submit', preventDefaultListener);
|
|
|
|
// 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.bind('$destroy', function() {
|
|
$timeout(function() {
|
|
removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
|
|
}, 0, false);
|
|
});
|
|
}
|
|
|
|
var parentFormCtrl = formElement.parent().controller('form'),
|
|
alias = attr.name || attr.ngForm;
|
|
|
|
if (alias) {
|
|
scope[alias] = controller;
|
|
}
|
|
if (parentFormCtrl) {
|
|
formElement.bind('$destroy', function() {
|
|
parentFormCtrl.$removeControl(controller);
|
|
if (alias) {
|
|
scope[alias] = undefined;
|
|
}
|
|
extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
return isNgForm ? extend(copy(formDirective), {restrict: 'EAC'}) : formDirective;
|
|
}];
|
|
};
|
|
|
|
var formDirective = formDirectiveFactory();
|
|
var ngFormDirective = formDirectiveFactory(true);
|