mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
Objects received from outside AngularJS may have had their `hasOwnProperty` method overridden with something else. In cases where we can do this without incurring a performance penalty we call directly on Object.prototype.hasOwnProperty to ensure that we use the correct method. Also, we have some internal hash objects, where the keys for the map are provided from outside AngularJS. In such cases we either prevent `hasOwnProperty` from being used as a key or provide some other way of preventing our objects from having their `hasOwnProperty` overridden. BREAKING CHANGE: Inputs with name equal to "hasOwnProperty" are not allowed inside form or ngForm directives. Before, inputs whose name was "hasOwnProperty" were quietly ignored and not added to the scope. Now a badname exception is thrown. Using "hasOwnProperty" for an input name would be very unusual and bad practice. Either do not include such an input in a `form` or `ngForm` directive or change the name of the input. Closes #3331
587 lines
18 KiB
JavaScript
587 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
describe('form', function() {
|
|
var doc, control, scope, $compile, changeInputValue;
|
|
|
|
beforeEach(module(function($compileProvider) {
|
|
$compileProvider.directive('storeModelCtrl', function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attr, ctrl) {
|
|
control = ctrl;
|
|
}
|
|
};
|
|
});
|
|
}));
|
|
|
|
beforeEach(inject(function($injector, $sniffer) {
|
|
$compile = $injector.get('$compile');
|
|
scope = $injector.get('$rootScope');
|
|
|
|
changeInputValue = function(elm, value) {
|
|
elm.val(value);
|
|
browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change');
|
|
};
|
|
}));
|
|
|
|
afterEach(function() {
|
|
dealoc(doc);
|
|
});
|
|
|
|
|
|
it('should instantiate form and attach it to DOM', function() {
|
|
doc = $compile('<form>')(scope);
|
|
expect(doc.data('$formController')).toBeTruthy();
|
|
expect(doc.data('$formController') instanceof FormController).toBe(true);
|
|
});
|
|
|
|
|
|
it('should remove the widget when element removed', function() {
|
|
doc = $compile(
|
|
'<form name="myForm">' +
|
|
'<input type="text" name="alias" ng-model="value" store-model-ctrl/>' +
|
|
'</form>')(scope);
|
|
|
|
var form = scope.myForm;
|
|
control.$setValidity('required', false);
|
|
expect(form.alias).toBe(control);
|
|
expect(form.$error.required).toEqual([control]);
|
|
|
|
doc.find('input').remove();
|
|
expect(form.$error.required).toBe(false);
|
|
expect(form.alias).toBeUndefined();
|
|
});
|
|
|
|
|
|
it('should use ngForm value as form name', function() {
|
|
doc = $compile(
|
|
'<div ng-form="myForm">' +
|
|
'<input type="text" name="alias" ng-model="value"/>' +
|
|
'</div>')(scope);
|
|
|
|
expect(scope.myForm).toBeDefined();
|
|
expect(scope.myForm.alias).toBeDefined();
|
|
});
|
|
|
|
it('should use ngForm value as form name when nested inside form', function () {
|
|
doc = $compile(
|
|
'<form name="myForm">' +
|
|
'<div ng-form="nestedForm"><input type="text" name="alias" ng-model="value"/></div>' +
|
|
'</form>')(scope);
|
|
|
|
expect(scope.myForm).toBeDefined();
|
|
expect(scope.myForm.nestedForm).toBeDefined();
|
|
expect(scope.myForm.nestedForm.alias).toBeDefined();
|
|
});
|
|
|
|
|
|
it('should publish form to scope when name attr is defined', function() {
|
|
doc = $compile('<form name="myForm"></form>')(scope);
|
|
expect(scope.myForm).toBeTruthy();
|
|
expect(doc.data('$formController')).toBeTruthy();
|
|
expect(doc.data('$formController')).toEqual(scope.myForm);
|
|
});
|
|
|
|
|
|
it('should support expression in form name', function() {
|
|
doc = $compile('<form name="obj.myForm"></form>')(scope);
|
|
|
|
expect(scope.obj).toBeDefined();
|
|
expect(scope.obj.myForm).toBeTruthy();
|
|
});
|
|
|
|
|
|
it('should support two forms on a single scope', function() {
|
|
doc = $compile(
|
|
'<div>' +
|
|
'<form name="formA">' +
|
|
'<input name="firstName" ng-model="firstName" required>' +
|
|
'</form>' +
|
|
'<form name="formB">' +
|
|
'<input name="lastName" ng-model="lastName" required>' +
|
|
'</form>' +
|
|
'</div>'
|
|
)(scope);
|
|
|
|
scope.$apply();
|
|
|
|
expect(scope.formA.$error.required.length).toBe(1);
|
|
expect(scope.formA.$error.required).toEqual([scope.formA.firstName]);
|
|
expect(scope.formB.$error.required.length).toBe(1);
|
|
expect(scope.formB.$error.required).toEqual([scope.formB.lastName]);
|
|
|
|
var inputA = doc.find('input').eq(0),
|
|
inputB = doc.find('input').eq(1);
|
|
|
|
changeInputValue(inputA, 'val1');
|
|
changeInputValue(inputB, 'val2');
|
|
|
|
expect(scope.firstName).toBe('val1');
|
|
expect(scope.lastName).toBe('val2');
|
|
|
|
expect(scope.formA.$error.required).toBe(false);
|
|
expect(scope.formB.$error.required).toBe(false);
|
|
});
|
|
|
|
|
|
it('should publish widgets', function() {
|
|
doc = jqLite('<form name="form"><input type="text" name="w1" ng-model="some" /></form>');
|
|
$compile(doc)(scope);
|
|
|
|
var widget = scope.form.w1;
|
|
expect(widget).toBeDefined();
|
|
expect(widget.$pristine).toBe(true);
|
|
expect(widget.$dirty).toBe(false);
|
|
expect(widget.$valid).toBe(true);
|
|
expect(widget.$invalid).toBe(false);
|
|
});
|
|
|
|
|
|
it('should throw an exception if an input has name="hasOwnProperty"', function() {
|
|
doc = jqLite(
|
|
'<form name="form">'+
|
|
'<input name="hasOwnProperty" ng-model="some" />'+
|
|
'<input name="other" ng-model="someOther" />'+
|
|
'</form>');
|
|
expect(function() {
|
|
$compile(doc)(scope);
|
|
}).toThrowMinErr('ng', 'badname');
|
|
});
|
|
|
|
|
|
describe('preventing default submission', function() {
|
|
|
|
it('should prevent form submission', function() {
|
|
var nextTurn = false,
|
|
submitted = false,
|
|
reloadPrevented;
|
|
|
|
doc = jqLite('<form ng-submit="submitMe()">' +
|
|
'<input type="submit" value="submit">' +
|
|
'</form>');
|
|
|
|
var assertPreventDefaultListener = function(e) {
|
|
reloadPrevented = e.defaultPrevented || (e.returnValue === false);
|
|
};
|
|
|
|
// native dom event listeners in IE8 fire in LIFO order so we have to register them
|
|
// there in different order than in other browsers
|
|
if (msie==8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener);
|
|
|
|
$compile(doc)(scope);
|
|
|
|
scope.submitMe = function() {
|
|
submitted = true;
|
|
}
|
|
|
|
if (msie!=8) addEventListenerFn(doc[0], 'submit', assertPreventDefaultListener);
|
|
|
|
browserTrigger(doc.find('input'));
|
|
|
|
// let the browser process all events (and potentially reload the page)
|
|
setTimeout(function() { nextTurn = true;});
|
|
|
|
waitsFor(function() { return nextTurn; });
|
|
|
|
runs(function() {
|
|
expect(reloadPrevented).toBe(true);
|
|
expect(submitted).toBe(true);
|
|
|
|
// prevent mem leak in test
|
|
removeEventListenerFn(doc[0], 'submit', assertPreventDefaultListener);
|
|
});
|
|
});
|
|
|
|
|
|
it('should prevent the default when the form is destroyed by a submission via a click event',
|
|
inject(function($timeout) {
|
|
doc = jqLite('<div>' +
|
|
'<form ng-submit="submitMe()">' +
|
|
'<button ng-click="destroy()"></button>' +
|
|
'</form>' +
|
|
'</div>');
|
|
|
|
var form = doc.find('form'),
|
|
destroyed = false,
|
|
nextTurn = false,
|
|
submitted = false,
|
|
reloadPrevented;
|
|
|
|
scope.destroy = function() {
|
|
// yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep
|
|
// this test small. Imagine that the destroy action will cause a model change (e.g.
|
|
// $location change) that will cause some directive to destroy the dom (e.g. ngView+$route)
|
|
doc.html('');
|
|
destroyed = true;
|
|
}
|
|
|
|
scope.submitMe = function() {
|
|
submitted = true;
|
|
}
|
|
|
|
var assertPreventDefaultListener = function(e) {
|
|
reloadPrevented = e.defaultPrevented || (e.returnValue === false);
|
|
};
|
|
|
|
// native dom event listeners in IE8 fire in LIFO order so we have to register them
|
|
// there in different order than in other browsers
|
|
if (msie == 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener);
|
|
|
|
$compile(doc)(scope);
|
|
|
|
if (msie != 8) addEventListenerFn(form[0], 'submit', assertPreventDefaultListener);
|
|
|
|
browserTrigger(doc.find('button'), 'click');
|
|
|
|
// let the browser process all events (and potentially reload the page)
|
|
setTimeout(function() { nextTurn = true;}, 100);
|
|
|
|
waitsFor(function() { return nextTurn; });
|
|
|
|
|
|
// I can't get IE8 to automatically trigger submit in this test, in production it does it
|
|
// properly
|
|
if (msie == 8) browserTrigger(form, 'submit');
|
|
|
|
runs(function() {
|
|
expect(doc.html()).toBe('');
|
|
expect(destroyed).toBe(true);
|
|
expect(submitted).toBe(false); // this is known corner-case that is not currently handled
|
|
// the issue is that the submit listener is destroyed before
|
|
// the event propagates there. we can fix this if we see
|
|
// the issue in the wild, I'm not going to bother to do it
|
|
// now. (i)
|
|
|
|
// IE9 and IE10 are special and don't fire submit event when form was destroyed
|
|
if (msie < 9) {
|
|
expect(reloadPrevented).toBe(true);
|
|
$timeout.flush();
|
|
}
|
|
|
|
// prevent mem leak in test
|
|
removeEventListenerFn(form[0], 'submit', assertPreventDefaultListener);
|
|
});
|
|
}));
|
|
|
|
|
|
it('should NOT prevent form submission if action attribute present', function() {
|
|
var callback = jasmine.createSpy('submit').andCallFake(function(event) {
|
|
expect(event.isDefaultPrevented()).toBe(false);
|
|
event.preventDefault();
|
|
});
|
|
|
|
doc = $compile('<form action="some.py"></form>')(scope);
|
|
doc.on('submit', callback);
|
|
|
|
browserTrigger(doc, 'submit');
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
|
|
|
|
describe('nested forms', function() {
|
|
|
|
it('should chain nested forms', 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).toBe(false);
|
|
expect(child.$error.MyError).toBe(false);
|
|
|
|
child.$setDirty();
|
|
expect(parent.$dirty).toBeTruthy();
|
|
});
|
|
|
|
|
|
it('should deregister a child form when its DOM is removed', function() {
|
|
doc = jqLite(
|
|
'<form name="parent">' +
|
|
'<div class="ng-form" name="child">' +
|
|
'<input ng:model="modelA" name="inputA" required>' +
|
|
'</div>' +
|
|
'</form>');
|
|
$compile(doc)(scope);
|
|
scope.$apply();
|
|
|
|
var parent = scope.parent,
|
|
child = scope.child;
|
|
|
|
expect(parent).toBeDefined();
|
|
expect(child).toBeDefined();
|
|
expect(parent.$error.required).toEqual([child]);
|
|
doc.children().remove(); //remove child
|
|
|
|
expect(parent.child).toBeUndefined();
|
|
expect(scope.child).toBeUndefined();
|
|
expect(parent.$error.required).toBe(false);
|
|
});
|
|
|
|
|
|
it('should deregister a child form whose name is an expression when its DOM is removed', function() {
|
|
doc = jqLite(
|
|
'<form name="parent">' +
|
|
'<div class="ng-form" name="child.form">' +
|
|
'<input ng:model="modelA" name="inputA" required>' +
|
|
'</div>' +
|
|
'</form>');
|
|
$compile(doc)(scope);
|
|
scope.$apply();
|
|
|
|
var parent = scope.parent,
|
|
child = scope.child.form;
|
|
|
|
expect(parent).toBeDefined();
|
|
expect(child).toBeDefined();
|
|
expect(parent.$error.required).toEqual([child]);
|
|
doc.children().remove(); //remove child
|
|
|
|
expect(parent.child).toBeUndefined();
|
|
expect(scope.child.form).toBeUndefined();
|
|
expect(parent.$error.required).toBe(false);
|
|
});
|
|
|
|
|
|
it('should deregister a input when its removed from DOM', function() {
|
|
doc = jqLite(
|
|
'<form name="parent">' +
|
|
'<div class="ng-form" name="child">' +
|
|
'<input ng:model="modelA" name="inputA" required>' +
|
|
'</div>' +
|
|
'</form>');
|
|
$compile(doc)(scope);
|
|
scope.$apply();
|
|
|
|
var parent = scope.parent,
|
|
child = scope.child,
|
|
input = child.inputA;
|
|
|
|
expect(parent).toBeDefined();
|
|
expect(child).toBeDefined();
|
|
expect(parent.$error.required).toEqual([child]);
|
|
expect(child.$error.required).toEqual([input]);
|
|
expect(doc.hasClass('ng-invalid')).toBe(true);
|
|
expect(doc.hasClass('ng-invalid-required')).toBe(true);
|
|
expect(doc.find('div').hasClass('ng-invalid')).toBe(true);
|
|
expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true);
|
|
doc.find('input').remove(); //remove child
|
|
|
|
expect(parent.$error.required).toBe(false);
|
|
expect(child.$error.required).toBe(false);
|
|
expect(doc.hasClass('ng-valid')).toBe(true);
|
|
expect(doc.hasClass('ng-valid-required')).toBe(true);
|
|
expect(doc.find('div').hasClass('ng-valid')).toBe(true);
|
|
expect(doc.find('div').hasClass('ng-valid-required')).toBe(true);
|
|
});
|
|
|
|
|
|
it('should chain nested forms in repeater', function() {
|
|
doc = jqLite(
|
|
'<ng:form name=parent>' +
|
|
'<ng:form ng:repeat="f in forms" name=child>' +
|
|
'<input type=text ng:model=text name=text>' +
|
|
'</ng:form>' +
|
|
'</ng:form>');
|
|
$compile(doc)(scope);
|
|
|
|
scope.$apply(function() {
|
|
scope.forms = [1];
|
|
});
|
|
|
|
var parent = scope.parent;
|
|
var child = doc.find('input').scope().child;
|
|
var input = child.text;
|
|
|
|
expect(parent).toBeDefined();
|
|
expect(child).toBeDefined();
|
|
expect(input).toBeDefined();
|
|
|
|
input.$setValidity('myRule', false);
|
|
expect(input.$error.myRule).toEqual(true);
|
|
expect(child.$error.myRule).toEqual([input]);
|
|
expect(parent.$error.myRule).toEqual([child]);
|
|
|
|
input.$setValidity('myRule', true);
|
|
expect(parent.$error.myRule).toBe(false);
|
|
expect(child.$error.myRule).toBe(false);
|
|
});
|
|
})
|
|
|
|
|
|
describe('validation', function() {
|
|
|
|
beforeEach(function() {
|
|
doc = $compile(
|
|
'<form name="form">' +
|
|
'<input ng-model="name" name="name" store-model-ctrl/>' +
|
|
'</form>')(scope);
|
|
|
|
scope.$digest();
|
|
});
|
|
|
|
|
|
it('should have ng-valid/ng-invalid css class', function() {
|
|
expect(doc).toBeValid();
|
|
|
|
control.$setValidity('error', false);
|
|
expect(doc).toBeInvalid();
|
|
expect(doc.hasClass('ng-valid-error')).toBe(false);
|
|
expect(doc.hasClass('ng-invalid-error')).toBe(true);
|
|
|
|
control.$setValidity('another', false);
|
|
expect(doc.hasClass('ng-valid-error')).toBe(false);
|
|
expect(doc.hasClass('ng-invalid-error')).toBe(true);
|
|
expect(doc.hasClass('ng-valid-another')).toBe(false);
|
|
expect(doc.hasClass('ng-invalid-another')).toBe(true);
|
|
|
|
control.$setValidity('error', true);
|
|
expect(doc).toBeInvalid();
|
|
expect(doc.hasClass('ng-valid-error')).toBe(true);
|
|
expect(doc.hasClass('ng-invalid-error')).toBe(false);
|
|
expect(doc.hasClass('ng-valid-another')).toBe(false);
|
|
expect(doc.hasClass('ng-invalid-another')).toBe(true);
|
|
|
|
control.$setValidity('another', true);
|
|
expect(doc).toBeValid();
|
|
expect(doc.hasClass('ng-valid-error')).toBe(true);
|
|
expect(doc.hasClass('ng-invalid-error')).toBe(false);
|
|
expect(doc.hasClass('ng-valid-another')).toBe(true);
|
|
expect(doc.hasClass('ng-invalid-another')).toBe(false);
|
|
});
|
|
|
|
|
|
it('should have ng-pristine/ng-dirty css class', function() {
|
|
expect(doc).toBePristine();
|
|
|
|
control.$setViewValue('');
|
|
scope.$apply();
|
|
expect(doc).toBeDirty();
|
|
});
|
|
});
|
|
|
|
|
|
describe('$setPristine', function() {
|
|
|
|
it('should reset pristine state of form and controls', function() {
|
|
|
|
doc = $compile(
|
|
'<form name="testForm">' +
|
|
'<input ng-model="named1" name="foo">' +
|
|
'<input ng-model="named2" name="bar">' +
|
|
'</form>')(scope);
|
|
|
|
scope.$digest();
|
|
|
|
var form = doc,
|
|
formCtrl = scope.testForm,
|
|
input1 = form.find('input').eq(0),
|
|
input1Ctrl = input1.controller('ngModel'),
|
|
input2 = form.find('input').eq(1),
|
|
input2Ctrl = input2.controller('ngModel');
|
|
|
|
input1Ctrl.$setViewValue('xx');
|
|
input2Ctrl.$setViewValue('yy');
|
|
scope.$apply();
|
|
expect(form).toBeDirty();
|
|
expect(input1).toBeDirty();
|
|
expect(input2).toBeDirty();
|
|
|
|
formCtrl.$setPristine();
|
|
expect(form).toBePristine();
|
|
expect(formCtrl.$pristine).toBe(true);
|
|
expect(formCtrl.$dirty).toBe(false);
|
|
expect(input1).toBePristine();
|
|
expect(input1Ctrl.$pristine).toBe(true);
|
|
expect(input1Ctrl.$dirty).toBe(false);
|
|
expect(input2).toBePristine();
|
|
expect(input2Ctrl.$pristine).toBe(true);
|
|
expect(input2Ctrl.$dirty).toBe(false);
|
|
});
|
|
|
|
|
|
it('should reset pristine state of anonymous form controls', function() {
|
|
|
|
doc = $compile(
|
|
'<form name="testForm">' +
|
|
'<input ng-model="anonymous">' +
|
|
'</form>')(scope);
|
|
|
|
scope.$digest();
|
|
|
|
var form = doc,
|
|
formCtrl = scope.testForm,
|
|
input = form.find('input').eq(0),
|
|
inputCtrl = input.controller('ngModel');
|
|
|
|
inputCtrl.$setViewValue('xx');
|
|
scope.$apply();
|
|
expect(form).toBeDirty();
|
|
expect(input).toBeDirty();
|
|
|
|
formCtrl.$setPristine();
|
|
expect(form).toBePristine();
|
|
expect(formCtrl.$pristine).toBe(true);
|
|
expect(formCtrl.$dirty).toBe(false);
|
|
expect(input).toBePristine();
|
|
expect(inputCtrl.$pristine).toBe(true);
|
|
expect(inputCtrl.$dirty).toBe(false);
|
|
});
|
|
|
|
|
|
it('should reset pristine state of nested forms', function() {
|
|
|
|
doc = $compile(
|
|
'<form name="testForm">' +
|
|
'<div ng-form>' +
|
|
'<input ng-model="named" name="foo">' +
|
|
'</div>' +
|
|
'</form>')(scope);
|
|
|
|
scope.$digest();
|
|
|
|
var form = doc,
|
|
formCtrl = scope.testForm,
|
|
nestedForm = form.find('div'),
|
|
nestedFormCtrl = nestedForm.controller('form'),
|
|
nestedInput = form.find('input').eq(0),
|
|
nestedInputCtrl = nestedInput.controller('ngModel');
|
|
|
|
nestedInputCtrl.$setViewValue('xx');
|
|
scope.$apply();
|
|
expect(form).toBeDirty();
|
|
expect(nestedForm).toBeDirty();
|
|
expect(nestedInput).toBeDirty();
|
|
|
|
formCtrl.$setPristine();
|
|
expect(form).toBePristine();
|
|
expect(formCtrl.$pristine).toBe(true);
|
|
expect(formCtrl.$dirty).toBe(false);
|
|
expect(nestedForm).toBePristine();
|
|
expect(nestedFormCtrl.$pristine).toBe(true);
|
|
expect(nestedFormCtrl.$dirty).toBe(false);
|
|
expect(nestedInput).toBePristine();
|
|
expect(nestedInputCtrl.$pristine).toBe(true);
|
|
expect(nestedInputCtrl.$dirty).toBe(false);
|
|
});
|
|
});
|
|
});
|