Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit cca3f39

Browse files
author
Gonzalo Ruiz de Villa
committed
feat(form): add support for ngFormOptions attribute and form isolation
Child forms propagate always their state to its parent form. Following the pattern of ngModelOptions a new optional attribute is defined for forms, ngFormOptions that will allow to define now if the form should be considered as 'root', therefore preventing the propagation of its state to its parent. In the future, if more options are needed this new attribute ngFormOptions may be the place to define them. Options are exposed in the controller, but the isolated property is read only when the NgFormController is executed, so the behavior won't change if its value is changed later. It maybe used like this: <ng:form name="parent"> <ng:form name="child" ng-form-options="{root:true}"> <input ng:model="modelA" name="inputA"> <input ng:model="modelB" name="inputB"> </ng:form> </ng:form> Closes: #5858
1 parent 325eecf commit cca3f39

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

src/ng/directive/form.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
6464
var form = this,
6565
controls = [];
6666

67-
var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
67+
form.$options = $scope.$eval(attrs.ngFormOptions) || {};
68+
69+
var parentForm = form.$$parentForm =
70+
(!form.$options.root && element.parent().controller('form'))
71+
|| nullFormCtrl;
6872

6973
// init state
7074
form.$error = {};
@@ -296,6 +300,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
296300
*
297301
* @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
298302
* related scope, under this name.
303+
* @param {Object} ngFormOptions options to apply to the current form.
304+
* - `root`: boolean value which indicates that the form should be considered as a root
305+
* and that it should not propagate its state to its parent form (if there is one). By default,
306+
* child forms propagate their state ($dirty, $pristine, $valid, ...) to its parent form.
299307
*
300308
*/
301309

test/ng/directive/formSpec.js

+149
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,154 @@ describe('form', function() {
870870
expect(scope.form.$submitted).toBe(false);
871871
});
872872
});
873+
874+
describe('ngFormOptions attributes', function() {
875+
it('should allow define a form as root', function() {
876+
doc = jqLite(
877+
'<ng:form name="parent">' +
878+
'<ng:form name="child" ng-form-options="{root:true}">' +
879+
'<input ng:model="modelA" name="inputA">' +
880+
'<input ng:model="modelB" name="inputB">' +
881+
'</ng:form>' +
882+
'</ng:form>');
883+
$compile(doc)(scope);
884+
885+
var parent = scope.parent,
886+
child = scope.child,
887+
inputA = child.inputA,
888+
inputB = child.inputB;
889+
890+
inputA.$setValidity('MyError', false);
891+
inputB.$setValidity('MyError', false);
892+
expect(parent.$error.MyError).toBeFalsy();
893+
expect(child.$error.MyError).toEqual([inputA, inputB]);
894+
895+
inputA.$setValidity('MyError', true);
896+
expect(parent.$error.MyError).toBeFalsy();
897+
expect(child.$error.MyError).toEqual([inputB]);
898+
899+
inputB.$setValidity('MyError', true);
900+
expect(parent.$error.MyError).toBeFalsy();
901+
expect(child.$error.MyError).toBeFalsy();
902+
903+
child.$setDirty();
904+
expect(parent.$dirty).toBeFalsy();
905+
906+
child.$setSubmitted();
907+
expect(parent.$submitted).toBeFalsy();
908+
});
909+
910+
it('should chain nested forms as default behaviour', function() {
911+
doc = jqLite(
912+
'<ng:form name="parent">' +
913+
'<ng:form name="child" ng-form-options="{}">' +
914+
'<input ng:model="modelA" name="inputA">' +
915+
'<input ng:model="modelB" name="inputB">' +
916+
'</ng:form>' +
917+
'</ng:form>');
918+
$compile(doc)(scope);
919+
920+
var parent = scope.parent,
921+
child = scope.child,
922+
inputA = child.inputA,
923+
inputB = child.inputB;
924+
925+
inputA.$setValidity('MyError', false);
926+
inputB.$setValidity('MyError', false);
927+
expect(parent.$error.MyError).toEqual([child]);
928+
expect(child.$error.MyError).toEqual([inputA, inputB]);
929+
930+
inputA.$setValidity('MyError', true);
931+
expect(parent.$error.MyError).toEqual([child]);
932+
expect(child.$error.MyError).toEqual([inputB]);
933+
934+
inputB.$setValidity('MyError', true);
935+
expect(parent.$error.MyError).toBeFalsy();
936+
expect(child.$error.MyError).toBeFalsy();
937+
938+
child.$setDirty();
939+
expect(parent.$dirty).toBeTruthy();
940+
941+
child.$setSubmitted();
942+
expect(parent.$submitted).toBeTruthy();
943+
});
944+
945+
it('should chain nested forms when "root" is false', function() {
946+
doc = jqLite(
947+
'<ng:form name="parent">' +
948+
'<ng:form name="child" ng-form-options="{root:false}">' +
949+
'<input ng:model="modelA" name="inputA">' +
950+
'<input ng:model="modelB" name="inputB">' +
951+
'</ng:form>' +
952+
'</ng:form>');
953+
$compile(doc)(scope);
954+
955+
var parent = scope.parent,
956+
child = scope.child,
957+
inputA = child.inputA,
958+
inputB = child.inputB;
959+
960+
inputA.$setValidity('MyError', false);
961+
inputB.$setValidity('MyError', false);
962+
expect(parent.$error.MyError).toEqual([child]);
963+
expect(child.$error.MyError).toEqual([inputA, inputB]);
964+
965+
inputA.$setValidity('MyError', true);
966+
expect(parent.$error.MyError).toEqual([child]);
967+
expect(child.$error.MyError).toEqual([inputB]);
968+
969+
inputB.$setValidity('MyError', true);
970+
expect(parent.$error.MyError).toBeFalsy();
971+
expect(child.$error.MyError).toBeFalsy();
972+
973+
child.$setDirty();
974+
expect(parent.$dirty).toBeTruthy();
975+
976+
child.$setSubmitted();
977+
expect(parent.$submitted).toBeTruthy();
978+
});
979+
980+
it('should maintain the default behavior for children of a root form', function() {
981+
doc = jqLite(
982+
'<ng:form name="parent">' +
983+
'<ng:form name="child" ng-form-options="{root:true}">' +
984+
'<ng:form name="grandchild">' +
985+
'<input ng:model="modelA" name="inputA">' +
986+
'<input ng:model="modelB" name="inputB">' +
987+
'</ng:form>' +
988+
'</ng:form>' +
989+
'</ng:form>');
990+
$compile(doc)(scope);
991+
992+
var parent = scope.parent,
993+
child = scope.child,
994+
grandchild = scope.grandchild,
995+
inputA = grandchild.inputA,
996+
inputB = grandchild.inputB;
997+
998+
inputA.$setValidity('MyError', false);
999+
inputB.$setValidity('MyError', false);
1000+
expect(parent.$error.MyError).toBeFalsy();
1001+
expect(child.$error.MyError).toEqual([grandchild]);
1002+
expect(grandchild.$error.MyError).toEqual([inputA, inputB]);
1003+
1004+
inputA.$setValidity('MyError', true);
1005+
expect(parent.$error.MyError).toBeFalsy();
1006+
expect(child.$error.MyError).toEqual([grandchild]);
1007+
expect(grandchild.$error.MyError).toEqual([inputB]);
1008+
1009+
inputB.$setValidity('MyError', true);
1010+
expect(parent.$error.MyError).toBeFalsy();
1011+
expect(child.$error.MyError).toBeFalsy();
1012+
expect(grandchild.$error.MyError).toBeFalsy();
1013+
1014+
child.$setDirty();
1015+
expect(parent.$dirty).toBeFalsy();
1016+
1017+
child.$setSubmitted();
1018+
expect(parent.$submitted).toBeFalsy();
1019+
});
1020+
});
8731021
});
8741022

8751023
describe('form animations', function() {
@@ -947,4 +1095,5 @@ describe('form animations', function() {
9471095
assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error');
9481096
assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error');
9491097
}));
1098+
9501099
});

0 commit comments

Comments
 (0)