Revert "feat(forms): add ng-submitted class to forms that have been submitted." (#42474)

This reverts commit f024d7556081f8913f21761bb8e6aab8d08be110.

PR Close #42474
This commit is contained in:
Jessica Janiuk 2021-06-03 17:19:26 -07:00
parent ae858c0504
commit 00b1444d12
6 changed files with 13 additions and 259 deletions

View File

@ -195,7 +195,6 @@ The following classes are currently supported.
* `.ng-dirty` * `.ng-dirty`
* `.ng-untouched` * `.ng-untouched`
* `.ng-touched` * `.ng-touched`
* `.ng-submitted` (enclosing form element only)
In the following example, the hero form uses the `.ng-valid` and `.ng-invalid` classes to In the following example, the hero form uses the `.ng-valid` and `.ng-invalid` classes to
set the color of each form control's border. set the color of each form control's border.

View File

@ -314,8 +314,6 @@ Angular sets special CSS classes on the control element to reflect the state, as
</table> </table>
Additionally, Angular applies the `ng-submitted` class to `<form>` elements upon submission. This class does *not* apply to inner controls.
You use these CSS classes to define the styles for your control based on its status. You use these CSS classes to define the styles for your control based on its status.
### Observe control states ### Observe control states

View File

@ -460,7 +460,7 @@ export declare class RadioControlValueAccessor extends ɵangular_packages_forms_
name: string; name: string;
onChange: () => void; onChange: () => void;
value: any; value: any;
constructor(renderer: Renderer2, elementRef: ElementRef, _registry: ɵangular_packages_forms_forms_r, _injector: Injector); constructor(renderer: Renderer2, elementRef: ElementRef, _registry: ɵangular_packages_forms_forms_q, _injector: Injector);
fireUncheck(value: any): void; fireUncheck(value: any): void;
ngOnDestroy(): void; ngOnDestroy(): void;
ngOnInit(): void; ngOnInit(): void;

View File

@ -12,8 +12,7 @@ import {AbstractControlDirective} from './abstract_control_directive';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
type AnyControlStatus = type AnyControlStatus = 'untouched'|'touched'|'pristine'|'dirty'|'valid'|'invalid'|'pending';
'untouched'|'touched'|'pristine'|'dirty'|'valid'|'invalid'|'pending'|'submitted';
export class AbstractControlStatus { export class AbstractControlStatus {
private _cd: AbstractControlDirective|null; private _cd: AbstractControlDirective|null;
@ -23,17 +22,6 @@ export class AbstractControlStatus {
} }
is(status: AnyControlStatus): boolean { is(status: AnyControlStatus): boolean {
// Currently with ViewEngine (in AOT mode) it's not possible to use private methods in host
// bindings.
// TODO: once ViewEngine is removed, this function should be refactored:
// - make the `is` method `protected`, so it's not accessible publicly
// - move the `submitted` status logic to the `NgControlStatusGroup` class
// and make it `private` or `protected` too.
if (status === 'submitted') {
// We check for the `submitted` field from `NgForm` and `FormGroupDirective` classes, but
// we avoid instanceof checks to prevent non-tree-shakable references to those types.
return !!(this._cd as unknown as {submitted: boolean} | null)?.submitted;
}
return !!this._cd?.control?.[status]; return !!this._cd?.control?.[status];
} }
} }
@ -48,11 +36,6 @@ export const ngControlStatusHost = {
'[class.ng-pending]': 'is("pending")', '[class.ng-pending]': 'is("pending")',
}; };
export const ngGroupStatusHost = {
...ngControlStatusHost,
'[class.ng-submitted]': 'is("submitted")',
};
/** /**
* @description * @description
* Directive automatically applied to Angular form controls that sets CSS classes * Directive automatically applied to Angular form controls that sets CSS classes
@ -86,8 +69,7 @@ export class NgControlStatus extends AbstractControlStatus {
/** /**
* @description * @description
* Directive automatically applied to Angular form groups that sets CSS classes * Directive automatically applied to Angular form groups that sets CSS classes
* based on control status (valid/invalid/dirty/etc). On groups, this includes the additional * based on control status (valid/invalid/dirty/etc).
* class ng-submitted.
* *
* @see `NgControlStatus` * @see `NgControlStatus`
* *
@ -98,7 +80,7 @@ export class NgControlStatus extends AbstractControlStatus {
@Directive({ @Directive({
selector: selector:
'[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]', '[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]',
host: ngGroupStatusHost host: ngControlStatusHost
}) })
export class NgControlStatusGroup extends AbstractControlStatus { export class NgControlStatusGroup extends AbstractControlStatus {
constructor(@Optional() @Self() cd: ControlContainer) { constructor(@Optional() @Self() cd: ControlContainer) {

View File

@ -190,7 +190,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
}); });
it('should update nested form group model when UI changes', () => { it('should update nested form group model when UI changes', () => {
const fixture = initTest(NestedFormGroupNameComp); const fixture = initTest(NestedFormGroupComp);
fixture.componentInstance.form = new FormGroup( fixture.componentInstance.form = new FormGroup(
{'signin': new FormGroup({'login': new FormControl(), 'password': new FormControl()})}); {'signin': new FormGroup({'login': new FormControl(), 'password': new FormControl()})});
fixture.detectChanges(); fixture.detectChanges();
@ -242,7 +242,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
}); });
it('should pick up dir validators from nested form groups', () => { it('should pick up dir validators from nested form groups', () => {
const fixture = initTest(NestedFormGroupNameComp, LoginIsEmptyValidator); const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({ const form = new FormGroup({
'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
}); });
@ -260,7 +260,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
}); });
it('should strip named controls that are not found', () => { it('should strip named controls that are not found', () => {
const fixture = initTest(NestedFormGroupNameComp, LoginIsEmptyValidator); const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({ const form = new FormGroup({
'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')})
}); });
@ -335,7 +335,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
}); });
it('should attach dirs to all child controls when group control changes', () => { it('should attach dirs to all child controls when group control changes', () => {
const fixture = initTest(NestedFormGroupNameComp, LoginIsEmptyValidator); const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator);
const form = new FormGroup({ const form = new FormGroup({
signin: new FormGroup( signin: new FormGroup(
{login: new FormControl('oldLogin'), password: new FormControl('oldPassword')}) {login: new FormControl('oldLogin'), password: new FormControl('oldPassword')})
@ -1087,8 +1087,6 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
fixture.detectChanges(); fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
@ -1101,19 +1099,6 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
}); });
it('should work with formGroup', () => { it('should work with formGroup', () => {
@ -1137,126 +1122,6 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(formEl)).toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
});
it('should not assign `ng-submitted` class to elements with `formArrayName`', () => {
// Since element with the `formArrayName` can not represent top-level forms (can only be
// inside other elements), this test verifies that these elements never receive
// `ng-submitted` CSS class even when they are located inside submitted form.
const fixture = initTest(FormArrayComp);
const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]);
const form = new FormGroup({cities: cityArray});
fixture.componentInstance.form = form;
fixture.componentInstance.cityArray = cityArray;
fixture.detectChanges();
const [loginInput, passwordInput] =
fixture.debugElement.queryAll(By.css('input')).map(el => el.nativeElement);
const arrEl = fixture.debugElement.query(By.css('div')).nativeElement;
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
expect(passwordInput).toBeDefined();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
});
it('should apply submitted status with nested formArrayName', () => {
const fixture = initTest(NestedFormArrayNameComp);
const ic = new FormControl('foo');
const arr = new FormArray([ic]);
const form = new FormGroup({arr});
fixture.componentInstance.form = form;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
const arrEl = fixture.debugElement.query(By.css('div')).nativeElement;
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(input)).not.toContain('ng-submitted');
expect(sortedClassList(arrEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
});
it('should apply submitted status with nested formGroupName', () => {
const fixture = initTest(NestedFormGroupNameComp);
const loginControl =
new FormControl('', {validators: Validators.required, updateOn: 'change'});
const passwordControl = new FormControl('', Validators.required);
const formGroup = new FormGroup(
{signin: new FormGroup({login: loginControl, password: passwordControl})},
{updateOn: 'blur'});
fixture.componentInstance.form = formGroup;
fixture.detectChanges();
const [loginInput, passwordInput] =
fixture.debugElement.queryAll(By.css('input')).map(el => el.nativeElement);
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
const groupEl = fixture.debugElement.query(By.css('div')).nativeElement;
loginInput.value = 'Nancy';
// Input and blur events, as in a real interaction, cause the form to be touched and
// dirtied.
dispatchEvent(loginInput, 'input');
dispatchEvent(loginInput, 'blur');
fixture.detectChanges();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(groupEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(groupEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(loginInput)).not.toContain('ng-submitted');
expect(sortedClassList(groupEl)).not.toContain('ng-submitted');
expect(sortedClassList(formEl)).not.toContain('ng-submitted');
}); });
}); });
@ -1636,7 +1501,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
it('should allow child control updateOn blur to override group updateOn', () => { it('should allow child control updateOn blur to override group updateOn', () => {
const fixture = initTest(NestedFormGroupNameComp); const fixture = initTest(NestedFormGroupComp);
const loginControl = const loginControl =
new FormControl('', {validators: Validators.required, updateOn: 'change'}); new FormControl('', {validators: Validators.required, updateOn: 'change'});
const passwordControl = new FormControl('', Validators.required); const passwordControl = new FormControl('', Validators.required);
@ -1945,7 +1810,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
const validatorSpy = jasmine.createSpy('validator'); const validatorSpy = jasmine.createSpy('validator');
const groupValidatorSpy = jasmine.createSpy('groupValidatorSpy'); const groupValidatorSpy = jasmine.createSpy('groupValidatorSpy');
const fixture = initTest(NestedFormGroupNameComp); const fixture = initTest(NestedFormGroupComp);
const formGroup = new FormGroup({ const formGroup = new FormGroup({
signin: new FormGroup({login: new FormControl(), password: new FormControl()}), signin: new FormGroup({login: new FormControl(), password: new FormControl()}),
email: new FormControl('', {updateOn: 'submit'}) email: new FormControl('', {updateOn: 'submit'})
@ -2042,7 +1907,7 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
}); });
it('should allow child control updateOn submit to override group updateOn', () => { it('should allow child control updateOn submit to override group updateOn', () => {
const fixture = initTest(NestedFormGroupNameComp); const fixture = initTest(NestedFormGroupComp);
const loginControl = const loginControl =
new FormControl('', {validators: Validators.required, updateOn: 'change'}); new FormControl('', {validators: Validators.required, updateOn: 'change'});
const passwordControl = new FormControl('', Validators.required); const passwordControl = new FormControl('', Validators.required);
@ -4942,7 +4807,7 @@ class FormGroupComp {
} }
@Component({ @Component({
selector: 'nested-form-group-name-comp', selector: 'nested-form-group-comp',
template: ` template: `
<form [formGroup]="form"> <form [formGroup]="form">
<div formGroupName="signin" login-is-empty-validator> <div formGroupName="signin" login-is-empty-validator>
@ -4952,7 +4817,7 @@ class FormGroupComp {
<input *ngIf="form.contains('email')" formControlName="email"> <input *ngIf="form.contains('email')" formControlName="email">
</form>` </form>`
}) })
class NestedFormGroupNameComp { class NestedFormGroupComp {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
form!: FormGroup; form!: FormGroup;
} }
@ -4975,20 +4840,6 @@ class FormArrayComp {
cityArray!: FormArray; cityArray!: FormArray;
} }
@Component({
selector: 'nested-form-array-name-comp',
template: `
<form [formGroup]="form">
<div formArrayName="arr">
<input formControlName="0">
</div>
</form>
`
})
class NestedFormArrayNameComp {
form!: FormGroup;
}
@Component({ @Component({
selector: 'form-array-nested-group', selector: 'form-array-nested-group',
template: ` template: `

View File

@ -187,21 +187,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
dispatchEvent(input, 'input'); dispatchEvent(input, 'input');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(formEl)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
expect(sortedClassList(input)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(formEl)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
expect(sortedClassList(input)).not.toContain('ng-submitted');
}); });
})); }));
@ -259,52 +244,9 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(formEl)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
}); });
})); }));
it('should set status classes involving nested FormGroups', () => {
const fixture = initTest(NgModelNestedForm);
fixture.componentInstance.first = '';
fixture.componentInstance.other = '';
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form')).nativeElement;
const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement;
const input = fixture.debugElement.query(By.css('input')).nativeElement;
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
expect(sortedClassList(form)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
dispatchEvent(formEl, 'submit');
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
expect(sortedClassList(form)).toEqual([
'ng-pristine', 'ng-submitted', 'ng-untouched', 'ng-valid'
]);
expect(sortedClassList(input)).not.toContain('ng-submitted');
dispatchEvent(formEl, 'reset');
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
expect(sortedClassList(form)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']);
expect(sortedClassList(input)).not.toContain('ng-submitted');
});
});
it('should not create a template-driven form when ngNoForm is used', () => { it('should not create a template-driven form when ngNoForm is used', () => {
const fixture = initTest(NgNoFormComp); const fixture = initTest(NgNoFormComp);
fixture.detectChanges(); fixture.detectChanges();
@ -2425,24 +2367,6 @@ class NgModelNgIfForm {
email!: string; email!: string;
} }
@Component({
selector: 'ng-model-nested',
template: `
<form>
<div ngModelGroup="contact-info">
<input name="first" [(ngModel)]="first">
<div ngModelGroup="other-names">
<input name="other-names" [(ngModel)]="other">
</div>
</div>
</form>
`
})
class NgModelNestedForm {
first!: string;
other!: string;
}
@Component({ @Component({
selector: 'ng-no-form', selector: 'ng-no-form',
template: ` template: `