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

Commit 52535c3

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 level', 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 48c8b23 commit 52535c3

File tree

2 files changed

+197
-1
lines changed

2 files changed

+197
-1
lines changed

src/ng/directive/form.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
6565
var form = this,
6666
controls = [];
6767

68+
var topLevel = $scope.$eval(attrs.ngFormTopLevel) || false;
69+
6870
// init state
6971
form.$error = {};
7072
form.$$success = {};
@@ -76,6 +78,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
7678
form.$invalid = false;
7779
form.$submitted = false;
7880
form.$$parentForm = nullFormCtrl;
81+
form.$$topLevel = topLevel;
7982

8083
/**
8184
* @ngdoc method
@@ -318,6 +321,9 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
318321
*
319322
* @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into
320323
* related scope, under this name.
324+
* @param {boolean} ngFormTopLevel Value which indicates that the form should be considered as a top level
325+
* and that it should not propagate its state to its parent form (if there is one). By default,
326+
* child forms propagate their state ($dirty, $pristine, $valid, ...) to its parent form.
321327
*
322328
*/
323329

@@ -416,6 +422,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
416422
angular.module('formExample', [])
417423
.controller('FormController', ['$scope', function($scope) {
418424
$scope.userType = 'guest';
425+
$scope.submitted = false;
426+
$scope.submit = function (){
427+
$scope.submitted = true;
428+
}
419429
}]);
420430
</script>
421431
<style>
@@ -497,18 +507,27 @@ var formDirectiveFactory = function(isNgForm) {
497507
event.preventDefault();
498508
};
499509

510+
var handleKeypress = function(event) {
511+
if (controller.$$topLevel && event.keyCode === 13 && event.target.nodeName === "INPUT") {
512+
event.stopPropagation();
513+
event.preventDefault();
514+
}
515+
};
516+
500517
formElement[0].addEventListener('submit', handleFormSubmission);
518+
formElement[0].addEventListener('keypress', handleKeypress);
501519

502520
// unregister the preventDefault listener so that we don't not leak memory but in a
503521
// way that will achieve the prevention of the default action.
504522
formElement.on('$destroy', function() {
505523
$timeout(function() {
506524
formElement[0].removeEventListener('submit', handleFormSubmission);
525+
formElement[0].removeEventListener('keypress', handleKeypress);
507526
}, 0, false);
508527
});
509528
}
510529

511-
var parentFormCtrl = ctrls[1] || controller.$$parentForm;
530+
var parentFormCtrl = (!controller.$$topLevel && ctrls[1]) || controller.$$parentForm;
512531
parentFormCtrl.$addControl(controller);
513532

514533
var setter = nameAttr ? getSetter(controller.$name) : noop;

test/ng/directive/formSpec.js

+177
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,183 @@ describe('form', function() {
11761176
expect(scope.form.$submitted).toBe(false);
11771177
});
11781178
});
1179+
1180+
describe('ngFormTopLevel attribute', function() {
1181+
it('should allow define a form as top level form', function() {
1182+
doc = jqLite(
1183+
'<ng:form name="parent">' +
1184+
'<ng:form name="child" ng-form-top-level="true">' +
1185+
'<input ng:model="modelA" name="inputA">' +
1186+
'<input ng:model="modelB" name="inputB">' +
1187+
'</ng:form>' +
1188+
'</ng:form>');
1189+
$compile(doc)(scope);
1190+
1191+
var parent = scope.parent,
1192+
child = scope.child,
1193+
inputA = child.inputA,
1194+
inputB = child.inputB;
1195+
1196+
inputA.$setValidity('MyError', false);
1197+
inputB.$setValidity('MyError', false);
1198+
expect(parent.$error.MyError).toBeFalsy();
1199+
expect(child.$error.MyError).toEqual([inputA, inputB]);
1200+
1201+
inputA.$setValidity('MyError', true);
1202+
expect(parent.$error.MyError).toBeFalsy();
1203+
expect(child.$error.MyError).toEqual([inputB]);
1204+
1205+
inputB.$setValidity('MyError', true);
1206+
expect(parent.$error.MyError).toBeFalsy();
1207+
expect(child.$error.MyError).toBeFalsy();
1208+
1209+
child.$setDirty();
1210+
expect(parent.$dirty).toBeFalsy();
1211+
1212+
child.$setSubmitted();
1213+
expect(parent.$submitted).toBeFalsy();
1214+
});
1215+
1216+
1217+
1218+
it('should stop enter triggered submit from propagating to parent forms', function() {
1219+
var form = $compile(
1220+
'<form name="parent">' +
1221+
'<ng-form name="topLevelForm" ng-form-top-level="true">' +
1222+
'<input type="text" name="i"/>' +
1223+
'</ng-form>' +
1224+
'</form>')(scope);
1225+
scope.$digest();
1226+
1227+
var inputElm = form.find('input').eq(0);
1228+
var topLevelFormElm = form.find('ng-form').eq(0);
1229+
1230+
var parentFormKeypress = jasmine.createSpy('parentFormKeypress');
1231+
var topLevelFormKeyPress = jasmine.createSpy('topLevelFormKeyPress');
1232+
1233+
form.on('keypress', parentFormKeypress);
1234+
topLevelFormElm.on('keypress', topLevelFormKeyPress);
1235+
1236+
browserTrigger(inputElm[0], 'keypress', {bubbles: true, keyCode:13});
1237+
1238+
expect(parentFormKeypress).not.toHaveBeenCalled();
1239+
expect(topLevelFormKeyPress).toHaveBeenCalled();
1240+
1241+
dealoc(form);
1242+
});
1243+
1244+
1245+
it('should chain nested forms as default behaviour', function() {
1246+
doc = jqLite(
1247+
'<ng:form name="parent">' +
1248+
'<ng:form name="child" >' +
1249+
'<input ng:model="modelA" name="inputA">' +
1250+
'<input ng:model="modelB" name="inputB">' +
1251+
'</ng:form>' +
1252+
'</ng:form>');
1253+
$compile(doc)(scope);
1254+
1255+
var parent = scope.parent,
1256+
child = scope.child,
1257+
inputA = child.inputA,
1258+
inputB = child.inputB;
1259+
1260+
inputA.$setValidity('MyError', false);
1261+
inputB.$setValidity('MyError', false);
1262+
expect(parent.$error.MyError).toEqual([child]);
1263+
expect(child.$error.MyError).toEqual([inputA, inputB]);
1264+
1265+
inputA.$setValidity('MyError', true);
1266+
expect(parent.$error.MyError).toEqual([child]);
1267+
expect(child.$error.MyError).toEqual([inputB]);
1268+
1269+
inputB.$setValidity('MyError', true);
1270+
expect(parent.$error.MyError).toBeFalsy();
1271+
expect(child.$error.MyError).toBeFalsy();
1272+
1273+
child.$setDirty();
1274+
expect(parent.$dirty).toBeTruthy();
1275+
1276+
child.$setSubmitted();
1277+
expect(parent.$submitted).toBeTruthy();
1278+
});
1279+
1280+
it('should chain nested forms when "ng-form-top-level" is false', function() {
1281+
doc = jqLite(
1282+
'<ng:form name="parent">' +
1283+
'<ng:form name="child" ng-form-top-level="false">' +
1284+
'<input ng:model="modelA" name="inputA">' +
1285+
'<input ng:model="modelB" name="inputB">' +
1286+
'</ng:form>' +
1287+
'</ng:form>');
1288+
$compile(doc)(scope);
1289+
1290+
var parent = scope.parent,
1291+
child = scope.child,
1292+
inputA = child.inputA,
1293+
inputB = child.inputB;
1294+
1295+
inputA.$setValidity('MyError', false);
1296+
inputB.$setValidity('MyError', false);
1297+
expect(parent.$error.MyError).toEqual([child]);
1298+
expect(child.$error.MyError).toEqual([inputA, inputB]);
1299+
1300+
inputA.$setValidity('MyError', true);
1301+
expect(parent.$error.MyError).toEqual([child]);
1302+
expect(child.$error.MyError).toEqual([inputB]);
1303+
1304+
inputB.$setValidity('MyError', true);
1305+
expect(parent.$error.MyError).toBeFalsy();
1306+
expect(child.$error.MyError).toBeFalsy();
1307+
1308+
child.$setDirty();
1309+
expect(parent.$dirty).toBeTruthy();
1310+
1311+
child.$setSubmitted();
1312+
expect(parent.$submitted).toBeTruthy();
1313+
});
1314+
1315+
it('should maintain the default behavior for children of a root form', function() {
1316+
doc = jqLite(
1317+
'<ng:form name="parent">' +
1318+
'<ng:form name="child" ng-form-top-level="true">' +
1319+
'<ng:form name="grandchild">' +
1320+
'<input ng:model="modelA" name="inputA">' +
1321+
'<input ng:model="modelB" name="inputB">' +
1322+
'</ng:form>' +
1323+
'</ng:form>' +
1324+
'</ng:form>');
1325+
$compile(doc)(scope);
1326+
1327+
var parent = scope.parent,
1328+
child = scope.child,
1329+
grandchild = scope.grandchild,
1330+
inputA = grandchild.inputA,
1331+
inputB = grandchild.inputB;
1332+
1333+
inputA.$setValidity('MyError', false);
1334+
inputB.$setValidity('MyError', false);
1335+
expect(parent.$error.MyError).toBeFalsy();
1336+
expect(child.$error.MyError).toEqual([grandchild]);
1337+
expect(grandchild.$error.MyError).toEqual([inputA, inputB]);
1338+
1339+
inputA.$setValidity('MyError', true);
1340+
expect(parent.$error.MyError).toBeFalsy();
1341+
expect(child.$error.MyError).toEqual([grandchild]);
1342+
expect(grandchild.$error.MyError).toEqual([inputB]);
1343+
1344+
inputB.$setValidity('MyError', true);
1345+
expect(parent.$error.MyError).toBeFalsy();
1346+
expect(child.$error.MyError).toBeFalsy();
1347+
expect(grandchild.$error.MyError).toBeFalsy();
1348+
1349+
child.$setDirty();
1350+
expect(parent.$dirty).toBeFalsy();
1351+
1352+
child.$setSubmitted();
1353+
expect(parent.$submitted).toBeFalsy();
1354+
});
1355+
});
11791356
});
11801357

11811358
describe('form animations', function() {

0 commit comments

Comments
 (0)