diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 12b844a5f712..c4c319e41a9f 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -64,7 +64,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, controls = []; - var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + var topLevel = $scope.$eval(attrs.ngFormTopLevel) || false; + + var parentForm = form.$$parentForm = + (!topLevel && element.parent().controller('form')) + || nullFormCtrl; // init state form.$error = {}; @@ -77,6 +81,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { form.$invalid = false; form.$submitted = false; + form.$$topLevel = topLevel; + parentForm.$addControl(form); /** @@ -299,6 +305,9 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into * related scope, under this name. + * @param {boolean} ngFormTopLevel Value which indicates that the form should be considered as a top level + * and that it should not propagate its state to its parent form (if there is one). By default, + * child forms propagate their state ($dirty, $pristine, $valid, ...) to its parent form. * */ @@ -400,6 +409,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { angular.module('formExample', []) .controller('FormController', ['$scope', function($scope) { $scope.userType = 'guest'; + $scope.submitted = false; + $scope.submit = function (){ + $scope.submitted = true; + } }]); -
- userType: - Required!
- userType = {{userType}}
- myForm.input.$valid = {{myForm.input.$valid}}
- myForm.input.$error = {{myForm.input.$error}}
- myForm.$valid = {{myForm.$valid}}
- myForm.$error.required = {{!!myForm.$error.required}}
-
+
+
+ userType: + Required!
+ userType = {{userType}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ Submitted: {{submitted}} + + + +
+
it('should initialize to model', function() { @@ -442,6 +463,17 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { expect(userType.getText()).toEqual('userType ='); expect(valid.getText()).toContain('false'); }); + + it('should not propagate keypress enter event from top level forms', function(){ + var topLevelFormInput = element(by.model('topLevelFormInput')); + var submitted = element(by.binding('submitted')); + + expect(submitted.getText()).toEqual('Submitted: false'); + + topLevelFormInput.sendKeys(protractor.Key.ENTER); + + expect(submitted.getText()).toEqual('Submitted: false'); + }); * @@ -479,13 +511,22 @@ var formDirectiveFactory = function(isNgForm) { event.preventDefault(); }; + var handleKeypress = function(event) { + if (controller.$$topLevel && event.keyCode === 13 && event.target.nodeName === "INPUT") { + event.stopPropagation(); + event.preventDefault(); + } + }; + addEventListenerFn(formElement[0], 'submit', handleFormSubmission); + addEventListenerFn(formElement[0], 'keypress', handleKeypress); // 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', handleFormSubmission); + removeEventListenerFn(formElement[0], 'keypress', handleKeypress); }, 0, false); }); } diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index c1b52b2e0093..a8abe303f606 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -941,6 +941,183 @@ describe('form', function() { expect(scope.form.$submitted).toBe(false); }); }); + + describe('ngFormTopLevel attribute', function() { + it('should allow define a form as top level form', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + inputA = child.inputA, + inputB = child.inputB; + + inputA.$setValidity('MyError', false); + inputB.$setValidity('MyError', false); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toEqual([inputA, inputB]); + + inputA.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toEqual([inputB]); + + inputB.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toBeFalsy(); + + child.$setDirty(); + expect(parent.$dirty).toBeFalsy(); + + child.$setSubmitted(); + expect(parent.$submitted).toBeFalsy(); + }); + + + + it('should stop enter triggered submit from propagating to parent forms', function() { + var form = $compile( + '
' + + '' + + '' + + '' + + '
')(scope); + scope.$digest(); + + var inputElm = form.find('input').eq(0); + var topLevelFormElm = form.find('ng-form').eq(0); + + var parentFormKeypress = jasmine.createSpy('parentFormKeypress'); + var topLevelFormKeyPress = jasmine.createSpy('topLevelFormKeyPress'); + + form.on('keypress', parentFormKeypress); + topLevelFormElm.on('keypress', topLevelFormKeyPress); + + browserTrigger(inputElm[0], 'keypress', {bubbles: true, keyCode:13}); + + expect(parentFormKeypress).not.toHaveBeenCalled(); + expect(topLevelFormKeyPress).toHaveBeenCalled(); + + dealoc(form); + }); + + + it('should chain nested forms as default behaviour', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + inputA = child.inputA, + inputB = child.inputB; + + inputA.$setValidity('MyError', false); + inputB.$setValidity('MyError', false); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputA, inputB]); + + inputA.$setValidity('MyError', true); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputB]); + + inputB.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toBeFalsy(); + + child.$setDirty(); + expect(parent.$dirty).toBeTruthy(); + + child.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + }); + + it('should chain nested forms when "ng-form-top-level" is false', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + inputA = child.inputA, + inputB = child.inputB; + + inputA.$setValidity('MyError', false); + inputB.$setValidity('MyError', false); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputA, inputB]); + + inputA.$setValidity('MyError', true); + expect(parent.$error.MyError).toEqual([child]); + expect(child.$error.MyError).toEqual([inputB]); + + inputB.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toBeFalsy(); + + child.$setDirty(); + expect(parent.$dirty).toBeTruthy(); + + child.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + }); + + it('should maintain the default behavior for children of a root form', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + grandchild = scope.grandchild, + inputA = grandchild.inputA, + inputB = grandchild.inputB; + + inputA.$setValidity('MyError', false); + inputB.$setValidity('MyError', false); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toEqual([grandchild]); + expect(grandchild.$error.MyError).toEqual([inputA, inputB]); + + inputA.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toEqual([grandchild]); + expect(grandchild.$error.MyError).toEqual([inputB]); + + inputB.$setValidity('MyError', true); + expect(parent.$error.MyError).toBeFalsy(); + expect(child.$error.MyError).toBeFalsy(); + expect(grandchild.$error.MyError).toBeFalsy(); + + child.$setDirty(); + expect(parent.$dirty).toBeFalsy(); + + child.$setSubmitted(); + expect(parent.$submitted).toBeFalsy(); + }); + }); }); describe('form animations', function() { @@ -1018,4 +1195,5 @@ describe('form animations', function() { assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error'); assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error'); })); + });