From 8ae29a0f833af1bbbda6d493f387ee7484714b32 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 12 Dec 2017 12:45:44 +0100 Subject: [PATCH 1/2] fix(form): set $submitted to true on child forms when parent is submitted Closes #10071 BREAKING CHANGE: Forms will now set $submitted on child forms when they are submitted. For example: ```
Submitting this form will set $submitted on "parentform" and "childform". Previously, it was only set on "parentform". This change was introduced because mixing form and ngForm does not create logically separate forms, but rather something like input groups. Therefore, child forms should inherit the submission state from their parent form. --- src/ng/directive/form.js | 20 ++++++- test/ng/directive/formSpec.js | 107 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 1fc3b6f1cff1..2beb0837ecfd 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -9,7 +9,8 @@ var nullFormCtrl = { $setValidity: noop, $setDirty: noop, $setPristine: noop, - $setSubmitted: noop + $setSubmitted: noop, + $$setSubmitted: noop }, PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; @@ -274,12 +275,25 @@ FormController.prototype = { * @name form.FormController#$setSubmitted * * @description - * Sets the form to its submitted state. + * 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; - this.$$parentForm.$setSubmitted(); + forEach(this.$$controls, function(control) { + if (control.$$setSubmitted) { + control.$$setSubmitted(); + } + }); } }; diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index b48ff1084468..2d996bb359e1 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -539,6 +539,113 @@ describe('form', function() { expect(parent.$submitted).toBeTruthy(); }); + it('should set $submitted to true on child forms when parent is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child; + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + }); + + + it('should not propagate $submitted state on removed child forms when parent is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + grandchild = scope.grandchild, + ggchild = scope.greatgrandchild; + + parent.$removeControl(child); + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + parent.$addControl(child); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + parent.$removeControl(child); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + parent.$setPristine(); // sets $submitted to false + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + grandchild.$setPristine(); + expect(grandchild.$submitted).not.toBeTruthy(); + + child.$setSubmitted(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + child.$setPristine(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + // Test upwards submission setting + grandchild.$setSubmitted(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + }); + + + it('should set $submitted to true on child and parent forms when form is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + grandchild = scope.grandchild; + + child.$setSubmitted(); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + }); it('should deregister a child form when its DOM is removed', function() { doc = jqLite( From e98bb35d032911f11addff8771b989a9c173bf06 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 12 Dec 2017 13:23:08 +0100 Subject: [PATCH 2/2] docs(ngForm): clarify usage and limitations --- src/ng/directive/form.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 2beb0837ecfd..2677921c0d74 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -352,16 +352,21 @@ addSetValidityMethod({ * @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. - * - * Note: the purpose of `ngForm` is to group controls, - * but not to be a replacement for the `
` tag with all of its capabilities - * (e.g. posting to the server, ...). - * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * 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. + * + *
+ * **Note**: `ngForm` cannot be used as a replacement for ``, because it lacks its + * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element). + * Specifically, you cannot submit `ngForm` like a `` tag. That means, + * you cannot send data to the server with `ngForm`, or integrate it with + * {@link ng.directive:ngSubmit `ngSubmit`}. + *
+ * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will + * be published into the related scope, under this name. * */