Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aio/content/guide/form-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Like in AngularJS, Angular automatically mirrors many control properties onto th
* `.ng-dirty`
* `.ng-untouched`
* `.ng-touched`
* `.ng-submitted`

The hero form uses the `.ng-valid` and `.ng-invalid` classes to
set the color of each form control's border.
Expand Down
16 changes: 16 additions & 0 deletions aio/content/guide/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,22 @@ You can leverage those class names to change the appearance of the control.

</tr>

<tr>

<td>
The parent form (if any) has been submitted.
</td>

<td>
<code>ng-submitted</code>
</td>

<td>
None
</td>

</tr>

</table>

Temporarily add a [template reference variable](guide/template-syntax#ref-vars) named `spy`
Expand Down
24 changes: 19 additions & 5 deletions packages/forms/src/directives/ng_control_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Self} from '@angular/core';
import {Directive, Optional, Self} from '@angular/core';

import {AbstractControlDirective} from './abstract_control_directive';
import {ControlContainer} from './control_container';
import {NgControl} from './ng_control';
import {NgForm} from './ng_form';
import {FormGroupDirective} from './reactive_directives/form_group_directive';

export class AbstractControlStatus {
private _cd: AbstractControlDirective;
private _controlContainer: ControlContainer|undefined;

constructor(cd: AbstractControlDirective) { this._cd = cd; }
constructor(cd: AbstractControlDirective, controlContainer?: ControlContainer) {
this._cd = cd;
this._controlContainer = controlContainer;
}

get ngClassUntouched(): boolean { return this._cd.control ? this._cd.control.untouched : false; }
get ngClassTouched(): boolean { return this._cd.control ? this._cd.control.touched : false; }
Expand All @@ -24,6 +30,11 @@ export class AbstractControlStatus {
get ngClassValid(): boolean { return this._cd.control ? this._cd.control.valid : false; }
get ngClassInvalid(): boolean { return this._cd.control ? this._cd.control.invalid : false; }
get ngClassPending(): boolean { return this._cd.control ? this._cd.control.pending : false; }
get ngClassSubmitted(): boolean {
return this._controlContainer && this._controlContainer.formDirective ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially looking at this I was confused because you're checking for this._controlContainer to exist when the constructor here say's required. But in NgControlStatus below you've marked it as @Optional. However, as far as TypeScript goes it's still required.

I would like to make the types align correctly. You will need to use @Inject along with making the ControlContainer optional in order for the JIT compiler to behave. Something like this:

export class AbstractControlStatus {
  ...
  constructor(cd: AbstractControlDirective, controlContainer?: ControlContainer) { ... }
}

and

export class NgControlStatus extends AbstractControlStatus {
  constructor(@Self() cd: NgControl, @Inject(ControlContainer) @Optional() controlContainer?: ControlContainer) { ... }
}

(this._controlContainer.formDirective as NgForm | FormGroupDirective).submitted :
false;
}
}

export const ngControlStatusHost = {
Expand All @@ -34,6 +45,7 @@ export const ngControlStatusHost = {
'[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'ngClassInvalid',
'[class.ng-pending]': 'ngClassPending',
'[class.ng-submitted]': 'ngClassSubmitted',
};

/**
Expand Down Expand Up @@ -61,14 +73,16 @@ export const ngControlStatusHost = {
*/
@Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost})
export class NgControlStatus extends AbstractControlStatus {
constructor(@Self() cd: NgControl) { super(cd); }
constructor(@Self() cd: NgControl, @Optional() controlContainer?: ControlContainer) {
super(cd, controlContainer);
}
}

/**
* @description
* Directive automatically applied to Angular form groups that sets CSS classes
* based on control status (valid/invalid/dirty/etc).
*
*
* @see `NgControlStatus`
*
* @ngModule ReactiveFormsModule
Expand All @@ -81,5 +95,5 @@ export class NgControlStatus extends AbstractControlStatus {
host: ngControlStatusHost
})
export class NgControlStatusGroup extends AbstractControlStatus {
constructor(@Self() cd: ControlContainer) { super(cd); }
constructor(@Self() cd: ControlContainer) { super(cd, cd); }
}
15 changes: 15 additions & 0 deletions packages/forms/test/reactive_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,14 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
fixture.detectChanges();

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(input)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
});

it('should work with formGroup', () => {
Expand All @@ -830,6 +838,13 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
fixture.detectChanges();

expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);

dispatchEvent(formEl, 'submit');
fixture.detectChanges();

expect(sortedClassList(formEl)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
});

});
Expand Down
18 changes: 18 additions & 0 deletions packages/forms/test/template_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
dispatchEvent(input, 'input');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);

const form = fixture.debugElement.query(By.css('form')).nativeElement;
dispatchEvent(form, 'submit');
fixture.detectChanges();

expect(sortedClassList(input)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
});
}));

Expand Down Expand Up @@ -225,6 +233,16 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat

expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);

dispatchEvent(form, 'submit');
fixture.detectChanges();

expect(sortedClassList(modelGroup)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
expect(sortedClassList(form)).toEqual([
'ng-dirty', 'ng-submitted', 'ng-touched', 'ng-valid'
]);
});
}));

Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/forms/forms.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ export declare abstract class NgControl extends AbstractControlDirective {
}

export declare class NgControlStatus extends AbstractControlStatus {
constructor(cd: NgControl);
constructor(cd: NgControl, controlContainer?: ControlContainer);
}

export declare class NgControlStatusGroup extends AbstractControlStatus {
Expand Down