feat(FormController): commit $viewValue of all child controls when form is submitted

Use the new `NgModelController.$commitViewValue()` method to commit the
`$viewValue` on all the child controls (including nested `ngForm`s) when the form
receives the `submit` event. This will happen immediately, overriding any
`updateOn` and `debounce` settings from `ngModelOptions`.

If you wish to access the committed `$modelValue`s then you can use the `ngSubmit`
directive to provide a handler.  Don't use `ngClick` on the submit button, as this
handler would be called before the pending `$viewValue`s have been committed.

Closes #7017
This commit is contained in:
Shahar Talmi
2014-05-09 02:52:37 +03:00
committed by Peter Bacon Darwin
parent adfc322b04
commit a0ae07bd4e
2 changed files with 79 additions and 3 deletions

View File

@@ -74,6 +74,23 @@ function FormController(element, attrs, $scope, $animate) {
$animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
}
/**
* @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.
*/
form.$commitViewValue = function() {
forEach(controls, function(control) {
control.$commitViewValue();
});
};
/**
* @ngdoc method
* @name form.FormController#$addControl
@@ -286,6 +303,10 @@ function FormController(element, attrs, $scope, $animate) {
* 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.
*
* @param {string=} name Name of the form. If specified, the form controller will be published into
* related scope, under this name.
*
@@ -381,19 +402,23 @@ var formDirectiveFactory = function(isNgForm) {
// 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) {
var handleFormSubmission = function(event) {
scope.$apply(function() {
controller.$commitViewValue();
});
event.preventDefault
? event.preventDefault()
: event.returnValue = false; // IE
};
addEventListenerFn(formElement[0], 'submit', preventDefaultListener);
addEventListenerFn(formElement[0], '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() {
removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
}, 0, false);
});
}

View File

@@ -154,6 +154,57 @@ describe('form', function() {
}).toThrowMinErr('ng', 'badname');
});
describe('triggering commit value on submit', function () {
it('should trigger update on form submit', function() {
var form = $compile(
'<form name="test" ng-model-options="{ updateOn: \'\' }" >' +
'<input type="text" ng-model="name" />' +
'</form>')(scope);
scope.$digest();
var inputElm = form.find('input').eq(0);
changeInputValue(inputElm, 'a');
expect(scope.name).toEqual(undefined);
browserTrigger(form, 'submit');
expect(scope.name).toEqual('a');
dealoc(form);
});
it('should trigger update on form submit with nested forms', function() {
var form = $compile(
'<form name="test" ng-model-options="{ updateOn: \'\' }" >' +
'<div class="ng-form" name="child">' +
'<input type="text" ng-model="name" />' +
'</div>' +
'</form>')(scope);
scope.$digest();
var inputElm = form.find('input').eq(0);
changeInputValue(inputElm, 'a');
expect(scope.name).toEqual(undefined);
browserTrigger(form, 'submit');
expect(scope.name).toEqual('a');
dealoc(form);
});
it('should trigger update before ng-submit is invoked', function() {
var form = $compile(
'<form name="test" ng-submit="submit()" ' +
'ng-model-options="{ updateOn: \'\' }" >' +
'<input type="text" ng-model="name" />' +
'</form>')(scope);
scope.$digest();
var inputElm = form.find('input').eq(0);
changeInputValue(inputElm, 'a');
scope.submit = jasmine.createSpy('submit').andCallFake(function() {
expect(scope.name).toEqual('a');
});
browserTrigger(form, 'submit');
expect(scope.submit).toHaveBeenCalled();
dealoc(form);
});
});
describe('preventing default submission', function() {