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;
+ }
}]);
-
+
+
+
+
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');
}));
+
});