/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AfterViewInit, Directive, EventEmitter, forwardRef, Inject, Input, Optional, Provider, Self} from '@angular/core';
import {AbstractControl, FormHooks} from '../model/abstract_model';
import {FormControl} from '../model/form_control';
import {FormGroup} from '../model/form_group';
import {composeAsyncValidators, composeValidators, NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container';
import {Form} from './form_interface';
import {NgControl} from './ng_control';
import {NgModel} from './ng_model';
import {NgModelGroup} from './ng_model_group';
import {CALL_SET_DISABLED_STATE, SetDisabledStateOption, setUpControl, setUpFormContainer, syncPendingControls} from './shared';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
const formDirectiveProvider: Provider = {
provide: ControlContainer,
useExisting: forwardRef(() => NgForm)
};
const resolvedPromise = (() => Promise.resolve())();
/**
* @description
* Creates a top-level `FormGroup` instance and binds it to a form
* to track aggregate form value and validation status.
*
* As soon as you import the `FormsModule`, this directive becomes active by default on
* all `
* ```
*
* ### Native DOM validation UI
*
* In order to prevent the native DOM form validation UI from interfering with Angular's form
* validation, Angular automatically adds the `novalidate` attribute on any `
* ```
*
* @ngModule FormsModule
* @publicApi
*/
@Directive({
selector: 'form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
outputs: ['ngSubmit'],
exportAs: 'ngForm'
})
export class NgForm extends ControlContainer implements Form, AfterViewInit {
/**
* @description
* Returns whether the form submission has been triggered.
*/
public readonly submitted: boolean = false;
private _directives = new Set();
/**
* @description
* The `FormGroup` instance created for this form.
*/
form: FormGroup;
/**
* @description
* Event emitter for the "ngSubmit" event
*/
ngSubmit = new EventEmitter();
/**
* @description
* Tracks options for the `NgForm` instance.
*
* **updateOn**: Sets the default `updateOn` value for all child `NgModels` below it
* unless explicitly set by a child `NgModel` using `ngModelOptions`). Defaults to 'change'.
* Possible values: `'change'` | `'blur'` | `'submit'`.
*
*/
// TODO(issue/24571): remove '!'.
@Input('ngFormOptions') options!: {updateOn?: FormHooks};
constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator|ValidatorFn)[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
(AsyncValidator|AsyncValidatorFn)[],
@Optional() @Inject(CALL_SET_DISABLED_STATE) private callSetDisabledState?:
SetDisabledStateOption) {
super();
this.form =
new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators));
}
/** @nodoc */
ngAfterViewInit() {
this._setUpdateStrategy();
}
/**
* @description
* The directive instance.
*/
override get formDirective(): Form {
return this;
}
/**
* @description
* The internal `FormGroup` instance.
*/
override get control(): FormGroup {
return this.form;
}
/**
* @description
* Returns an array representing the path to this group. Because this directive
* always lives at the top level of a form, it is always an empty array.
*/
override get path(): string[] {
return [];
}
/**
* @description
* Returns a map of the controls in this group.
*/
get controls(): {[key: string]: AbstractControl} {
return this.form.controls;
}
/**
* @description
* Method that sets up the control directive in this group, re-calculates its value
* and validity, and adds the instance to the internal list of directives.
*
* @param dir The `NgModel` directive instance.
*/
addControl(dir: NgModel): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
(dir as {control: FormControl}).control =
container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir, this.callSetDisabledState);
dir.control.updateValueAndValidity({emitEvent: false});
this._directives.add(dir);
});
}
/**
* @description
* Retrieves the `FormControl` instance from the provided `NgModel` directive.
*
* @param dir The `NgModel` directive instance.
*/
getControl(dir: NgModel): FormControl {
return this.form.get(dir.path);
}
/**
* @description
* Removes the `NgModel` instance from the internal list of directives
*
* @param dir The `NgModel` directive instance.
*/
removeControl(dir: NgModel): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
if (container) {
container.removeControl(dir.name);
}
this._directives.delete(dir);
});
}
/**
* @description
* Adds a new `NgModelGroup` directive instance to the form.
*
* @param dir The `NgModelGroup` directive instance.
*/
addFormGroup(dir: NgModelGroup): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
const group = new FormGroup({});
setUpFormContainer(group, dir);
container.registerControl(dir.name, group);
group.updateValueAndValidity({emitEvent: false});
});
}
/**
* @description
* Removes the `NgModelGroup` directive instance from the form.
*
* @param dir The `NgModelGroup` directive instance.
*/
removeFormGroup(dir: NgModelGroup): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
if (container) {
container.removeControl(dir.name);
}
});
}
/**
* @description
* Retrieves the `FormGroup` for a provided `NgModelGroup` directive instance
*
* @param dir The `NgModelGroup` directive instance.
*/
getFormGroup(dir: NgModelGroup): FormGroup {
return this.form.get(dir.path);
}
/**
* Sets the new value for the provided `NgControl` directive.
*
* @param dir The `NgControl` directive instance.
* @param value The new value for the directive's control.
*/
updateModel(dir: NgControl, value: any): void {
resolvedPromise.then(() => {
const ctrl = this.form.get(dir.path!);
ctrl.setValue(value);
});
}
/**
* @description
* Sets the value for this `FormGroup`.
*
* @param value The new value
*/
setValue(value: {[key: string]: any}): void {
this.control.setValue(value);
}
/**
* @description
* Method called when the "submit" event is triggered on the form.
* Triggers the `ngSubmit` emitter to emit the "submit" event as its payload.
*
* @param $event The "submit" event object
*/
onSubmit($event: Event): boolean {
(this as {submitted: boolean}).submitted = true;
syncPendingControls(this.form, this._directives);
this.ngSubmit.emit($event);
// Forms with `method="dialog"` have some special behavior
// that won't reload the page and that shouldn't be prevented.
return ($event?.target as HTMLFormElement | null)?.method === 'dialog';
}
/**
* @description
* Method called when the "reset" event is triggered on the form.
*/
onReset(): void {
this.resetForm();
}
/**
* @description
* Resets the form to an initial value and resets its submitted status.
*
* @param value The new value for the form.
*/
resetForm(value: any = undefined): void {
this.form.reset(value);
(this as {submitted: boolean}).submitted = false;
}
private _setUpdateStrategy() {
if (this.options && this.options.updateOn != null) {
this.form._updateOn = this.options.updateOn;
}
}
private _findContainer(path: string[]): FormGroup {
path.pop();
return path.length ? this.form.get(path) : this.form;
}
}