perf(forms): use internal `ngDevMode` flag to tree-shake error messages in prod builds (#37821)

This commit adds a guard before throwing any forms errors. This will tree-shake
error messages which cannot be minified. It should also help to reduce the
bundle size of the `forms` package in production by ~20%.

Closes #37697

PR Close #37821
This commit is contained in:
Sonu Kapoor 2020-06-29 08:17:13 -04:00 committed by Misko Hevery
parent 6e643d9874
commit 201a546af8
14 changed files with 71 additions and 68 deletions

View File

@ -221,15 +221,6 @@
{
"name": "FormControlName"
},
{
"name": "FormErrorExamples_formControlName"
},
{
"name": "FormErrorExamples_formGroupName"
},
{
"name": "FormErrorExamples_ngModelGroup"
},
{
"name": "FormGroup"
},
@ -497,9 +488,6 @@
{
"name": "RangeValueAccessor"
},
{
"name": "ReactiveErrors"
},
{
"name": "ReactiveFormsComponent"
},
@ -608,9 +596,6 @@
{
"name": "TRANSITION_ID"
},
{
"name": "TemplateDrivenErrors"
},
{
"name": "TemplateFormsComponent"
},
@ -701,9 +686,6 @@
{
"name": "_keyMap"
},
{
"name": "_noControlError"
},
{
"name": "_randomChar"
},
@ -716,9 +698,6 @@
{
"name": "_testabilityGetter"
},
{
"name": "_throwError"
},
{
"name": "addComponentLogic"
},
@ -1628,9 +1607,6 @@
{
"name": "u"
},
{
"name": "unimplemented"
},
{
"name": "unwrapRNode"
},

View File

@ -13,7 +13,9 @@ import {ControlValueAccessor} from './control_value_accessor';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
function unimplemented(): any {
throw new Error('unimplemented');
if (typeof ngDevMode === 'undefined' || ngDevMode) {
throw new Error('unimplemented');
}
}
/**

View File

@ -317,18 +317,20 @@ export class NgModel extends NgControl implements OnChanges, OnDestroy {
}
private _checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) &&
this._parent instanceof AbstractFormGroupDirective) {
TemplateDrivenErrors.formGroupNameException();
} else if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelParentException();
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!(this._parent instanceof NgModelGroup) &&
this._parent instanceof AbstractFormGroupDirective) {
TemplateDrivenErrors.formGroupNameException();
} else if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelParentException();
}
}
}
private _checkName(): void {
if (this.options && this.options.name) this.name = this.options.name;
if (!this._isStandalone() && !this.name) {
if (!this._isStandalone() && !this.name && (typeof ngDevMode === 'undefined' || ngDevMode)) {
TemplateDrivenErrors.missingNameException();
}
}

View File

@ -68,7 +68,8 @@ export class NgModelGroup extends AbstractFormGroupDirective implements OnInit,
/** @internal */
_checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm) &&
(typeof ngDevMode === 'undefined' || ngDevMode)) {
TemplateDrivenErrors.modelGroupParentException();
}
}

View File

@ -17,6 +17,13 @@ export const RADIO_VALUE_ACCESSOR: any = {
multi: true
};
function throwNameError() {
throw new Error(`
If you define both a name and a formControlName attribute on your radio button, their values
must match. Ex: <input type="radio" formControlName="food" name="food">
`);
}
/**
* @description
* Class used by Angular to track radio buttons. For internal use only.
@ -213,16 +220,10 @@ export class RadioControlValueAccessor implements ControlValueAccessor, OnDestro
}
private _checkName(): void {
if (this.name && this.formControlName && this.name !== this.formControlName) {
this._throwNameError();
if (this.name && this.formControlName && this.name !== this.formControlName &&
(typeof ngDevMode === 'undefined' || ngDevMode)) {
throwNameError();
}
if (!this.name && this.formControlName) this.name = this.formControlName;
}
private _throwNameError(): void {
throw new Error(`
If you define both a name and a formControlName attribute on your radio button, their values
must match. Ex: <input type="radio" formControlName="food" name="food">
`);
}
}

View File

@ -68,11 +68,13 @@ export class FormControlDirective extends NgControl implements OnChanges {
/**
* @description
* Triggers a warning that this input should not be used with reactive forms.
* Triggers a warning in dev mode that this input should not be used with reactive forms.
*/
@Input('disabled')
set isDisabled(isDisabled: boolean) {
ReactiveErrors.disabledAttrWarning();
if (typeof ngDevMode === 'undefined' || ngDevMode) {
ReactiveErrors.disabledAttrWarning();
}
}
// TODO(kara): remove next 4 properties once deprecation period is over

View File

@ -92,11 +92,13 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
/**
* @description
* Triggers a warning that this input should not be used with reactive forms.
* Triggers a warning in dev mode that this input should not be used with reactive forms.
*/
@Input('disabled')
set isDisabled(isDisabled: boolean) {
ReactiveErrors.disabledAttrWarning();
if (typeof ngDevMode === 'undefined' || ngDevMode) {
ReactiveErrors.disabledAttrWarning();
}
}
// TODO(kara): remove next 4 properties once deprecation period is over
@ -212,13 +214,16 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
}
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) &&
this._parent instanceof AbstractFormGroupDirective) {
ReactiveErrors.ngModelGroupException();
} else if (
!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective) &&
!(this._parent instanceof FormArrayName)) {
ReactiveErrors.controlParentException();
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!(this._parent instanceof FormGroupName) &&
this._parent instanceof AbstractFormGroupDirective) {
ReactiveErrors.ngModelGroupException();
} else if (
!(this._parent instanceof FormGroupName) &&
!(this._parent instanceof FormGroupDirective) &&
!(this._parent instanceof FormArrayName)) {
ReactiveErrors.controlParentException();
}
}
}

View File

@ -291,7 +291,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
}
private _checkFormPresent() {
if (!this.form) {
if (!this.form && (typeof ngDevMode === 'undefined' || ngDevMode)) {
ReactiveErrors.missingFormException();
}
}

View File

@ -96,7 +96,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
/** @internal */
_checkParentType(): void {
if (_hasInvalidParent(this._parent)) {
if (_hasInvalidParent(this._parent) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
ReactiveErrors.groupParentException();
}
}
@ -228,7 +228,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
}
private _checkParentType(): void {
if (_hasInvalidParent(this._parent)) {
if (_hasInvalidParent(this._parent) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
ReactiveErrors.arrayParentException();
}
}

View File

@ -33,6 +33,7 @@ export class ReactiveErrors {
${Examples.ngModelGroup}`);
}
static missingFormException(): void {
throw new Error(`formGroup expects a FormGroup instance. Please pass one in.

View File

@ -115,7 +115,7 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
*/
@Input()
set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') {
if (typeof fn !== 'function' && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
}
this._compareWith = fn;

View File

@ -112,7 +112,7 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
*/
@Input()
set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') {
if (typeof fn !== 'function' && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
}
this._compareWith = fn;

View File

@ -33,8 +33,10 @@ export function controlPath(name: string|null, parent: ControlContainer): string
}
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
}
control.validator = Validators.compose([control.validator!, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator!, dir.asyncValidator]);
@ -64,8 +66,14 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
}
export function cleanUpControl(control: FormControl, dir: NgControl) {
dir.valueAccessor!.registerOnChange(() => _noControlError(dir));
dir.valueAccessor!.registerOnTouched(() => _noControlError(dir));
const noop = () => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
_noControlError(dir);
}
};
dir.valueAccessor!.registerOnChange(noop);
dir.valueAccessor!.registerOnTouched(noop);
dir._rawValidators.forEach((validator: any) => {
if (validator.registerOnValidatorChange) {
@ -120,7 +128,8 @@ function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
export function setUpFormContainer(
control: FormGroup|FormArray, dir: AbstractFormGroupDirective|FormArrayName) {
if (control == null) _throwError(dir, 'Cannot find control with');
if (control == null && (typeof ngDevMode === 'undefined' || ngDevMode))
_throwError(dir, 'Cannot find control with');
control.validator = Validators.compose([control.validator, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
}
@ -190,7 +199,7 @@ export function selectValueAccessor(
dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor|null {
if (!valueAccessors) return null;
if (!Array.isArray(valueAccessors))
if (!Array.isArray(valueAccessors) && (typeof ngDevMode === 'undefined' || ngDevMode))
_throwError(dir, 'Value accessor was not provided as an array for form control with');
let defaultAccessor: ControlValueAccessor|undefined = undefined;
@ -202,12 +211,12 @@ export function selectValueAccessor(
defaultAccessor = v;
} else if (isBuiltInAccessor(v)) {
if (builtinAccessor)
if (builtinAccessor && (typeof ngDevMode === 'undefined' || ngDevMode))
_throwError(dir, 'More than one built-in value accessor matches form control with');
builtinAccessor = v;
} else {
if (customAccessor)
if (customAccessor && (typeof ngDevMode === 'undefined' || ngDevMode))
_throwError(dir, 'More than one custom value accessor matches form control with');
customAccessor = v;
}
@ -217,7 +226,9 @@ export function selectValueAccessor(
if (builtinAccessor) return builtinAccessor;
if (defaultAccessor) return defaultAccessor;
_throwError(dir, 'No valid value accessor for form control with');
if (typeof ngDevMode === 'undefined' || ngDevMode) {
_throwError(dir, 'No valid value accessor for form control with');
}
return null;
}
@ -234,7 +245,9 @@ export function _ngModelWarning(
if (((warningConfig === null || warningConfig === 'once') && !type._ngModelWarningSentOnce) ||
(warningConfig === 'always' && !instance._ngModelWarningSent)) {
ReactiveErrors.ngModelWarning(name);
if (typeof ngDevMode === 'undefined' || ngDevMode) {
ReactiveErrors.ngModelWarning(name);
}
type._ngModelWarningSentOnce = true;
instance._ngModelWarningSent = true;
}

View File

@ -469,7 +469,7 @@ function isPresent(o: any): boolean {
export function toObservable(r: any): Observable<any> {
const obs = isPromise(r) ? from(r) : r;
if (!(isObservable(obs))) {
if (!(isObservable(obs)) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw new Error(`Expected validator to return Promise or Observable.`);
}
return obs;