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

Commit ede8135

Browse files
author
Gonzalo Ruiz de Villa
committed
feat(form): add support for ngFormTopLevel attribute
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore preventing the propagation of its state to its parent. I It maybe used like this: <ng:form name="parent"> <ng:form name="child" ng-form-top-level="true"> <input ng:model="modelA" name="inputA"> <input ng:model="modelB" name="inputB"> </ng:form> </ng:form> Closes: #5858
1 parent 2a5d588 commit ede8135

File tree

2 files changed

+229
-10
lines changed

2 files changed

+229
-10
lines changed

src/ng/directive/form.js

+51-10
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+
var topLevel = $scope.$eval(attrs.ngFormTopLevel) || false;
68+
69+
var parentForm = form.$$parentForm =
70+
(!topLevel && element.parent().controller('form'))
71+
|| nullFormCtrl;
6872

6973
// init state
7074
form.$error = {};
@@ -77,6 +81,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
7781
form.$invalid = false;
7882
form.$submitted = false;
7983

84+
form.$$topLevel = topLevel;
85+
8086
parentForm.$addControl(form);
8187

8288
/**
@@ -299,6 +305,9 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
299305
*
300306
* @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
301307
* related scope, under this name.
308+
* @param {boolean} ngFormTopLevel Value which indicates that the form should be considered as a top level
309+
* and that it should not propagate its state to its parent form (if there is one). By default,
310+
* child forms propagate their state ($dirty, $pristine, $valid, ...) to its parent form.
302311
*
303312
*/
304313

@@ -400,6 +409,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
400409
angular.module('formExample', [])
401410
.controller('FormController', ['$scope', function($scope) {
402411
$scope.userType = 'guest';
412+
$scope.submitted = false;
413+
$scope.submit = function (){
414+
$scope.submitted = true;
415+
}
403416
}]);
404417
</script>
405418
<style>
@@ -412,15 +425,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
412425
background: red;
413426
}
414427
</style>
415-
<form name="myForm" ng-controller="FormController" class="my-form">
416-
userType: <input name="input" ng-model="userType" required>
417-
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
418-
<tt>userType = {{userType}}</tt><br>
419-
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
420-
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
421-
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
422-
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
423-
</form>
428+
<div ng-controller="FormController" >
429+
<form name="myForm" class="my-form">
430+
userType: <input name="input" ng-model="userType" required>
431+
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
432+
<tt>userType = {{userType}}</tt><br>
433+
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
434+
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
435+
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
436+
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
437+
</form>
438+
<form name="parentForm" ng-submit="submit()">
439+
Submitted: {{submitted}}
440+
<ng-form name="topLevelForm" ng-form-top-level="true">
441+
<input id="topLevelFormInput" name="topLevelFormInput" ng-model="topLevelFormInput">
442+
</ng-form>
443+
</form>
444+
</div>
424445
</file>
425446
<file name="protractor.js" type="protractor">
426447
it('should initialize to model', function() {
@@ -442,6 +463,17 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
442463
expect(userType.getText()).toEqual('userType =');
443464
expect(valid.getText()).toContain('false');
444465
});
466+
467+
it('should not propagate keypress enter event from top level forms', function(){
468+
var topLevelFormInput = element(by.model('topLevelFormInput'));
469+
var submitted = element(by.binding('submitted'));
470+
471+
expect(submitted.getText()).toEqual('Submitted: false');
472+
473+
topLevelFormInput.sendKeys(protractor.Key.ENTER);
474+
475+
expect(submitted.getText()).toEqual('Submitted: false');
476+
});
445477
</file>
446478
</example>
447479
*
@@ -479,13 +511,22 @@ var formDirectiveFactory = function(isNgForm) {
479511
event.preventDefault();
480512
};
481513

514+
var handleKeypress = function(event) {
515+
if (controller.$$topLevel && event.keyCode === 13 && event.target.nodeName === "INPUT") {
516+
event.stopPropagation();
517+
event.preventDefault();
518+
}
519+
};
520+
482521
addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
522+
addEventListenerFn(formElement[0], 'keypress', handleKeypress);
483523

484524
// unregister the preventDefault listener so that we don't not leak memory but in a
485525
// way that will achieve the prevention of the default action.
486526
formElement.on('$destroy', function() {
487527
$timeout(function() {
488528
removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
529+
removeEventListenerFn(formElement[0], 'keypress', handleKeypress);
489530
}, 0, false);
490531
});
491532
}

test/ng/directive/formSpec.js

+178
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,183 @@ describe('form', function() {
941941
expect(scope.form.$submitted).toBe(false);
942942
});
943943
});
944+
945+
describe('ngFormTopLevel attribute', function() {
946+
it('should allow define a form as top level form', function() {
947+
doc = jqLite(
948+
'<ng:form name="parent">' +
949+
'<ng:form name="child" ng-form-top-level="true">' +
950+
'<input ng:model="modelA" name="inputA">' +
951+
'<input ng:model="modelB" name="inputB">' +
952+
'</ng:form>' +
953+
'</ng:form>');
954+
$compile(doc)(scope);
955+
956+
var parent = scope.parent,
957+
child = scope.child,
958+
inputA = child.inputA,
959+
inputB = child.inputB;
960+
961+
inputA.$setValidity('MyError', false);
962+
inputB.$setValidity('MyError', false);
963+
expect(parent.$error.MyError).toBeFalsy();
964+
expect(child.$error.MyError).toEqual([inputA, inputB]);
965+
966+
inputA.$setValidity('MyError', true);
967+
expect(parent.$error.MyError).toBeFalsy();
968+
expect(child.$error.MyError).toEqual([inputB]);
969+
970+
inputB.$setValidity('MyError', true);
971+
expect(parent.$error.MyError).toBeFalsy();
972+
expect(child.$error.MyError).toBeFalsy();
973+
974+
child.$setDirty();
975+
expect(parent.$dirty).toBeFalsy();
976+
977+
child.$setSubmitted();
978+
expect(parent.$submitted).toBeFalsy();
979+
});
980+
981+
982+
983+
it('should stop enter triggered submit from propagating to parent forms', function() {
984+
var form = $compile(
985+
'<form name="parent">' +
986+
'<ng-form name="topLevelForm" ng-form-top-level="true">' +
987+
'<input type="text" name="i"/>' +
988+
'</ng-form>' +
989+
'</form>')(scope);
990+
scope.$digest();
991+
992+
var inputElm = form.find('input').eq(0);
993+
var topLevelFormElm = form.find('ng-form').eq(0);
994+
995+
var parentFormKeypress = jasmine.createSpy('parentFormKeypress');
996+
var topLevelFormKeyPress = jasmine.createSpy('topLevelFormKeyPress');
997+
998+
form.on('keypress', parentFormKeypress);
999+
topLevelFormElm.on('keypress', topLevelFormKeyPress);
1000+
1001+
browserTrigger(inputElm[0], 'keypress', {bubbles: true, keyCode:13});
1002+
1003+
expect(parentFormKeypress).not.toHaveBeenCalled();
1004+
expect(topLevelFormKeyPress).toHaveBeenCalled();
1005+
1006+
dealoc(form);
1007+
});
1008+
1009+
1010+
it('should chain nested forms as default behaviour', function() {
1011+
doc = jqLite(
1012+
'<ng:form name="parent">' +
1013+
'<ng:form name="child" >' +
1014+
'<input ng:model="modelA" name="inputA">' +
1015+
'<input ng:model="modelB" name="inputB">' +
1016+
'</ng:form>' +
1017+
'</ng:form>');
1018+
$compile(doc)(scope);
1019+
1020+
var parent = scope.parent,
1021+
child = scope.child,
1022+
inputA = child.inputA,
1023+
inputB = child.inputB;
1024+
1025+
inputA.$setValidity('MyError', false);
1026+
inputB.$setValidity('MyError', false);
1027+
expect(parent.$error.MyError).toEqual([child]);
1028+
expect(child.$error.MyError).toEqual([inputA, inputB]);
1029+
1030+
inputA.$setValidity('MyError', true);
1031+
expect(parent.$error.MyError).toEqual([child]);
1032+
expect(child.$error.MyError).toEqual([inputB]);
1033+
1034+
inputB.$setValidity('MyError', true);
1035+
expect(parent.$error.MyError).toBeFalsy();
1036+
expect(child.$error.MyError).toBeFalsy();
1037+
1038+
child.$setDirty();
1039+
expect(parent.$dirty).toBeTruthy();
1040+
1041+
child.$setSubmitted();
1042+
expect(parent.$submitted).toBeTruthy();
1043+
});
1044+
1045+
it('should chain nested forms when "ng-form-top-level" is false', function() {
1046+
doc = jqLite(
1047+
'<ng:form name="parent">' +
1048+
'<ng:form name="child" ng-form-top-level="false">' +
1049+
'<input ng:model="modelA" name="inputA">' +
1050+
'<input ng:model="modelB" name="inputB">' +
1051+
'</ng:form>' +
1052+
'</ng:form>');
1053+
$compile(doc)(scope);
1054+
1055+
var parent = scope.parent,
1056+
child = scope.child,
1057+
inputA = child.inputA,
1058+
inputB = child.inputB;
1059+
1060+
inputA.$setValidity('MyError', false);
1061+
inputB.$setValidity('MyError', false);
1062+
expect(parent.$error.MyError).toEqual([child]);
1063+
expect(child.$error.MyError).toEqual([inputA, inputB]);
1064+
1065+
inputA.$setValidity('MyError', true);
1066+
expect(parent.$error.MyError).toEqual([child]);
1067+
expect(child.$error.MyError).toEqual([inputB]);
1068+
1069+
inputB.$setValidity('MyError', true);
1070+
expect(parent.$error.MyError).toBeFalsy();
1071+
expect(child.$error.MyError).toBeFalsy();
1072+
1073+
child.$setDirty();
1074+
expect(parent.$dirty).toBeTruthy();
1075+
1076+
child.$setSubmitted();
1077+
expect(parent.$submitted).toBeTruthy();
1078+
});
1079+
1080+
it('should maintain the default behavior for children of a root form', function() {
1081+
doc = jqLite(
1082+
'<ng:form name="parent">' +
1083+
'<ng:form name="child" ng-form-top-level="true">' +
1084+
'<ng:form name="grandchild">' +
1085+
'<input ng:model="modelA" name="inputA">' +
1086+
'<input ng:model="modelB" name="inputB">' +
1087+
'</ng:form>' +
1088+
'</ng:form>' +
1089+
'</ng:form>');
1090+
$compile(doc)(scope);
1091+
1092+
var parent = scope.parent,
1093+
child = scope.child,
1094+
grandchild = scope.grandchild,
1095+
inputA = grandchild.inputA,
1096+
inputB = grandchild.inputB;
1097+
1098+
inputA.$setValidity('MyError', false);
1099+
inputB.$setValidity('MyError', false);
1100+
expect(parent.$error.MyError).toBeFalsy();
1101+
expect(child.$error.MyError).toEqual([grandchild]);
1102+
expect(grandchild.$error.MyError).toEqual([inputA, inputB]);
1103+
1104+
inputA.$setValidity('MyError', true);
1105+
expect(parent.$error.MyError).toBeFalsy();
1106+
expect(child.$error.MyError).toEqual([grandchild]);
1107+
expect(grandchild.$error.MyError).toEqual([inputB]);
1108+
1109+
inputB.$setValidity('MyError', true);
1110+
expect(parent.$error.MyError).toBeFalsy();
1111+
expect(child.$error.MyError).toBeFalsy();
1112+
expect(grandchild.$error.MyError).toBeFalsy();
1113+
1114+
child.$setDirty();
1115+
expect(parent.$dirty).toBeFalsy();
1116+
1117+
child.$setSubmitted();
1118+
expect(parent.$submitted).toBeFalsy();
1119+
});
1120+
});
9441121
});
9451122

9461123
describe('form animations', function() {
@@ -1018,4 +1195,5 @@ describe('form animations', function() {
10181195
assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error');
10191196
assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error');
10201197
}));
1198+
10211199
});

0 commit comments

Comments
 (0)