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

Commit

Permalink
feat(form): add support for ngFormTopLevel attribute
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gonzalo Ruiz de Villa committed Sep 10, 2015
1 parent d817bc1 commit 35d08c8
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 1 deletion.
27 changes: 26 additions & 1 deletion src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
controls = [];

var topLevel = $scope.$eval(attrs.ngFormTopLevel) || false;

var parentForm = form.$$parentForm =
(!topLevel && element.parent().controller('form'))
|| nullFormCtrl;

// init state
form.$error = {};
form.$$success = {};
Expand All @@ -75,7 +81,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$valid = true;
form.$invalid = false;
form.$submitted = false;
form.$$parentForm = nullFormCtrl;

form.$$topLevel = topLevel;

parentForm.$addControl(form);

/**
* @ngdoc method
Expand Down Expand Up @@ -318,6 +327,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.
*
*/

Expand Down Expand Up @@ -420,6 +432,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;
}
}]);
</script>
<style>
Expand Down Expand Up @@ -501,13 +517,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);
});
}
Expand Down
177 changes: 177 additions & 0 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,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(
'<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>');
$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(
'<form name="parent">' +
'<ng-form name="topLevelForm" ng-form-top-level="true">' +
'<input type="text" name="i"/>' +
'</ng-form>' +
'</form>')(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(
'<ng:form name="parent">' +
'<ng:form name="child" >' +
'<input ng:model="modelA" name="inputA">' +
'<input ng:model="modelB" name="inputB">' +
'</ng:form>' +
'</ng:form>');
$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(
'<ng:form name="parent">' +
'<ng:form name="child" ng-form-top-level="false">' +
'<input ng:model="modelA" name="inputA">' +
'<input ng:model="modelB" name="inputB">' +
'</ng:form>' +
'</ng:form>');
$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(
'<ng:form name="parent">' +
'<ng:form name="child" ng-form-top-level="true">' +
'<ng:form name="grandchild">' +
'<input ng:model="modelA" name="inputA">' +
'<input ng:model="modelB" name="inputB">' +
'</ng:form>' +
'</ng:form>' +
'</ng:form>');
$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() {
Expand Down

0 comments on commit 35d08c8

Please sign in to comment.