Skip to content

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: angular#5858
  • Loading branch information
Gonzalo Ruiz de Villa committed Apr 16, 2015
1 parent e65efa6 commit ff97935
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 10 deletions.
61 changes: 51 additions & 10 deletions src/ng/directive/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
controls = [];

var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
var topLevel = $scope.$eval(attrs.ngFormTopLevel) || false;

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

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

form.$$topLevel = topLevel;

parentForm.$addControl(form);

/**
Expand Down Expand Up @@ -299,6 +305,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 @@ -400,6 +409,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 All @@ -412,15 +425,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
background: red;
}
</style>
<form name="myForm" ng-controller="FormController" class="my-form">
userType: <input name="input" ng-model="userType" required>
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
<tt>userType = {{userType}}</tt><br>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
</form>
<div ng-controller="FormController" >
<form name="myForm" class="my-form">
userType: <input name="input" ng-model="userType" required>
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
<tt>userType = {{userType}}</tt><br>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
</form>
<form name="parentForm" ng-submit="submit()">
Submitted: {{submitted}}
<ng-form name="topLevelForm" ng-form-top-level="true">
<input id="topLevelFormInput" name="topLevelFormInput" ng-model="topLevelFormInput">
</ng-form>
</form>
</div>
</file>
<file name="protractor.js" type="protractor">
it('should initialize to model', function() {
Expand All @@ -442,6 +463,17 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
expect(userType.getText()).toEqual('userType =');
expect(valid.getText()).toContain('false');
});
it('should not propagate keypress enter event from top level forms', function(){
var topLevelFormInput = element(by.model('topLevelFormInput'));
var submitted = element(by.binding('submitted'));
expect(submitted.getText()).toEqual('Submitted: false');
topLevelFormInput.sendKeys(protractor.Key.ENTER);
expect(submitted.getText()).toEqual('Submitted: false');
});
</file>
</example>
*
Expand Down Expand Up @@ -479,13 +511,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
178 changes: 178 additions & 0 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,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 Expand Up @@ -1018,4 +1195,5 @@ describe('form animations', function() {
assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error');
assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error');
}));

});

0 comments on commit ff97935

Please sign in to comment.