mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): input coercion (#42803)
BREAKING CHANGE: Forms [email] input coercion Forms [email] input value will be considered as true if it is defined with any value rather than false and 'false'. PR Close #42803
This commit is contained in:
committed by
Dylan Hunn
parent
db6cf7e7c1
commit
d5719c2e0f
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import {Inject, Injectable, InjectionToken, Optional} from './di';
|
||||
import {isObservable, isPromise} from './util/lang';
|
||||
import {noop} from './util/noop';
|
||||
|
||||
@@ -15,8 +15,7 @@ export {SimpleChange, SimpleChanges} from '../interface/simple_change';
|
||||
export {devModeEqual} from './change_detection_util';
|
||||
export {ChangeDetectorRef} from './change_detector_ref';
|
||||
export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants';
|
||||
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
export {DefaultIterableDiffer} from './differs/default_iterable_differ';
|
||||
export {DefaultIterableDiffer, DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
export {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
|
||||
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
|
||||
@@ -26,6 +26,7 @@ export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn}
|
||||
export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass';
|
||||
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
|
||||
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
|
||||
export {coerceToBoolean as ɵcoerceToBoolean} from './util/coercion';
|
||||
export {makeDecorator as ɵmakeDecorator} from './util/decorators';
|
||||
export {global as ɵglobal} from './util/global';
|
||||
export {isObservable as ɵisObservable, isPromise as ɵisPromise, isSubscribable as ɵisSubscribable} from './util/lang';
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
*/
|
||||
|
||||
import {Type} from '../interface/type';
|
||||
|
||||
import {PlatformReflectionCapabilities} from './platform_reflection_capabilities';
|
||||
import {GetterFn, MethodFn, SetterFn} from './types';
|
||||
|
||||
export {PlatformReflectionCapabilities};
|
||||
export {GetterFn, MethodFn, SetterFn};
|
||||
export {GetterFn, MethodFn, PlatformReflectionCapabilities, SetterFn};
|
||||
|
||||
/**
|
||||
* Provides access to reflection data about symbols. Used internally by Angular
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import '../util/ng_dev_mode';
|
||||
|
||||
import {assertDefined, assertDomNode} from '../util/assert';
|
||||
|
||||
import {EMPTY_ARRAY} from '../util/empty';
|
||||
|
||||
import {LContext} from './interfaces/context';
|
||||
import {TNode, TNodeFlags} from './interfaces/node';
|
||||
import {RElement, RNode} from './interfaces/renderer_dom';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/** Coerces a value (typically a string) to a boolean. */
|
||||
export function coerceToBoolean(value: unknown): boolean {
|
||||
return typeof value === 'boolean' ? value : (value != null && value !== 'false');
|
||||
}
|
||||
@@ -668,6 +668,9 @@
|
||||
{
|
||||
"name": "coerceToAsyncValidator"
|
||||
},
|
||||
{
|
||||
"name": "coerceToBoolean"
|
||||
},
|
||||
{
|
||||
"name": "coerceToValidator"
|
||||
},
|
||||
|
||||
@@ -878,9 +878,6 @@
|
||||
{
|
||||
"name": "attachPatchData"
|
||||
},
|
||||
{
|
||||
"name": "attrBoolValue"
|
||||
},
|
||||
{
|
||||
"name": "autoRegisterModuleById"
|
||||
},
|
||||
@@ -911,6 +908,9 @@
|
||||
{
|
||||
"name": "cleanUpView"
|
||||
},
|
||||
{
|
||||
"name": "coerceToBoolean"
|
||||
},
|
||||
{
|
||||
"name": "collectNativeNodes"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @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 {coerceToBoolean} from '@angular/core/src/util/coercion';
|
||||
|
||||
{
|
||||
describe('coerceToBoolean', () => {
|
||||
it('should coerce undefined to false', () => {
|
||||
expect(coerceToBoolean(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce null to false', () => {
|
||||
expect(coerceToBoolean(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the empty string to true', () => {
|
||||
expect(coerceToBoolean('')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce zero to true', () => {
|
||||
expect(coerceToBoolean(0)).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce the string "false" to false', () => {
|
||||
expect(coerceToBoolean('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the boolean false to false', () => {
|
||||
expect(coerceToBoolean(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the boolean true to true', () => {
|
||||
expect(coerceToBoolean(true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce the string "true" to true', () => {
|
||||
expect(coerceToBoolean('true')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an arbitrary string to true', () => {
|
||||
expect(coerceToBoolean('pink')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an object to true', () => {
|
||||
expect(coerceToBoolean({})).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an array to true', () => {
|
||||
expect(coerceToBoolean([])).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges} from '@angular/core';
|
||||
import {ChangeDetectorRef, Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, ɵcoerceToBoolean as coerceToBoolean} from '@angular/core';
|
||||
|
||||
import {FormControl, FormHooks} from '../model';
|
||||
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
|
||||
@@ -320,8 +320,8 @@ export class NgModel extends NgControl implements OnChanges, OnDestroy {
|
||||
|
||||
private _updateDisabled(changes: SimpleChanges) {
|
||||
const disabledValue = changes['isDisabled'].currentValue;
|
||||
|
||||
const isDisabled = disabledValue === '' || (disabledValue && disabledValue !== 'false');
|
||||
// checking for 0 to avoid breaking change
|
||||
const isDisabled = disabledValue !== 0 && coerceToBoolean(disabledValue);
|
||||
|
||||
resolvedPromise.then(() => {
|
||||
if (isDisabled && !this.control.disabled) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, forwardRef, Input, OnChanges, SimpleChanges, StaticProvider} from '@angular/core';
|
||||
import {Directive, forwardRef, Input, OnChanges, SimpleChanges, StaticProvider, ɵcoerceToBoolean as coerceToBoolean} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import {AbstractControl} from '../model';
|
||||
@@ -22,16 +22,6 @@ function toInteger(value: string|number): number {
|
||||
return typeof value === 'number' ? value : parseInt(value, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that converts null, false or 'false' string to boolean.
|
||||
*
|
||||
* @param value input value.
|
||||
* @returns value of parameter converted to boolean.
|
||||
*/
|
||||
function toBoolean(input: unknown): boolean {
|
||||
return input != null && input !== false && `${input}` !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that ensures that provided value is a float (and converts it to float if needed).
|
||||
*
|
||||
@@ -380,7 +370,7 @@ export class RequiredValidator extends AbstractValidatorDirective {
|
||||
override inputName = 'required';
|
||||
|
||||
/** @internal */
|
||||
override normalizeInput = (input: unknown): boolean => toBoolean(input);
|
||||
override normalizeInput = coerceToBoolean;
|
||||
|
||||
/** @internal */
|
||||
override createValidator = (input: boolean): ValidatorFn => requiredValidator;
|
||||
@@ -472,11 +462,7 @@ export class EmailValidator extends AbstractValidatorDirective {
|
||||
override inputName = 'email';
|
||||
|
||||
/** @internal */
|
||||
override normalizeInput = (input: unknown): boolean =>
|
||||
// Avoid TSLint requirement to omit semicolon, see
|
||||
// https://github.com/palantir/tslint/issues/1476
|
||||
// tslint:disable-next-line:semicolon
|
||||
(input === '' || input === true || input === 'true');
|
||||
override normalizeInput = coerceToBoolean;
|
||||
|
||||
/** @internal */
|
||||
override createValidator = (input: number): ValidatorFn => emailValidator;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges} from '@angular/core';
|
||||
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ɵcoerceToBoolean as coerceToBoolean} from '@angular/core';
|
||||
import {Subject, Subscription} from 'rxjs';
|
||||
|
||||
import {Event, NavigationEnd} from '../events';
|
||||
@@ -242,8 +242,8 @@ export class RouterLink implements OnChanges {
|
||||
}
|
||||
|
||||
const extras = {
|
||||
skipLocationChange: attrBoolValue(this.skipLocationChange),
|
||||
replaceUrl: attrBoolValue(this.replaceUrl),
|
||||
skipLocationChange: coerceToBoolean(this.skipLocationChange),
|
||||
replaceUrl: coerceToBoolean(this.replaceUrl),
|
||||
state: this.state,
|
||||
};
|
||||
this.router.navigateByUrl(this.urlTree, extras);
|
||||
@@ -261,7 +261,7 @@ export class RouterLink implements OnChanges {
|
||||
queryParams: this.queryParams,
|
||||
fragment: this.fragment,
|
||||
queryParamsHandling: this.queryParamsHandling,
|
||||
preserveFragment: attrBoolValue(this.preserveFragment),
|
||||
preserveFragment: coerceToBoolean(this.preserveFragment),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -406,8 +406,8 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const extras = {
|
||||
skipLocationChange: attrBoolValue(this.skipLocationChange),
|
||||
replaceUrl: attrBoolValue(this.replaceUrl),
|
||||
skipLocationChange: coerceToBoolean(this.skipLocationChange),
|
||||
replaceUrl: coerceToBoolean(this.replaceUrl),
|
||||
state: this.state
|
||||
};
|
||||
this.router.navigateByUrl(this.urlTree, extras);
|
||||
@@ -431,11 +431,7 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
queryParams: this.queryParams,
|
||||
fragment: this.fragment,
|
||||
queryParamsHandling: this.queryParamsHandling,
|
||||
preserveFragment: attrBoolValue(this.preserveFragment),
|
||||
preserveFragment: coerceToBoolean(this.preserveFragment),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function attrBoolValue(s: any): boolean {
|
||||
return s === '' || !!s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user