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 ngFormOptions attribute and form isolation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gonzalo Ruiz de Villa committed Dec 1, 2014
1 parent 325eecf commit cca3f39
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 1 deletion.
10 changes: 9 additions & 1 deletion 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;
form.$options = $scope.$eval(attrs.ngFormOptions) || {};

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

// init state
form.$error = {};
Expand Down Expand Up @@ -296,6 +300,10 @@ 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 {Object} ngFormOptions options to apply to the current form.
* - `root`: boolean value which indicates that the form should be considered as a root
* 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
149 changes: 149 additions & 0 deletions test/ng/directive/formSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,154 @@ describe('form', function() {
expect(scope.form.$submitted).toBe(false);
});
});

describe('ngFormOptions attributes', function() {
it('should allow define a form as root', function() {
doc = jqLite(
'<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>');
$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 chain nested forms as default behaviour', function() {
doc = jqLite(
'<ng:form name="parent">' +
'<ng:form name="child" ng-form-options="{}">' +
'<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 "root" is false', function() {
doc = jqLite(
'<ng:form name="parent">' +
'<ng:form name="child" ng-form-options="{root: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-options="{root: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 @@ -947,4 +1095,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 cca3f39

Please sign in to comment.