fix(ivy): ExpressionChangedAfterItHasBeenCheckedError for SafeValue (#33749)
Fix #33448 PR Close #33749
This commit is contained in:
parent
cbb1d9f2bb
commit
f2828a08bd
|
@ -13,14 +13,14 @@ import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../int
|
||||||
import {RElement} from '../interfaces/renderer';
|
import {RElement} from '../interfaces/renderer';
|
||||||
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling';
|
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling';
|
||||||
import {isDirectiveHost} from '../interfaces/type_checks';
|
import {isDirectiveHost} from '../interfaces/type_checks';
|
||||||
import {LView, RENDERER, TVIEW, TView} from '../interfaces/view';
|
import {LView, RENDERER, TVIEW} from '../interfaces/view';
|
||||||
import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state';
|
import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state';
|
||||||
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings';
|
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings';
|
||||||
import {activateStylingMapFeature} from '../styling/map_based_bindings';
|
import {activateStylingMapFeature} from '../styling/map_based_bindings';
|
||||||
import {attachStylingDebugObject} from '../styling/styling_debug';
|
import {attachStylingDebugObject} from '../styling/styling_debug';
|
||||||
import {NO_CHANGE} from '../tokens';
|
import {NO_CHANGE} from '../tokens';
|
||||||
import {renderStringify} from '../util/misc_utils';
|
import {renderStringify} from '../util/misc_utils';
|
||||||
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, isHostStylingActive, isStylingContext, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils';
|
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, hasValueChangedUnwrapSafeValue, isHostStylingActive, isStylingContext, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils';
|
||||||
import {getNativeByTNode, getTNode} from '../util/view_utils';
|
import {getNativeByTNode, getTNode} from '../util/view_utils';
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ function stylingProp(
|
||||||
// as well.
|
// as well.
|
||||||
if (ngDevMode && getCheckNoChangesMode()) {
|
if (ngDevMode && getCheckNoChangesMode()) {
|
||||||
const oldValue = getValue(lView, bindingIndex);
|
const oldValue = getValue(lView, bindingIndex);
|
||||||
if (hasValueChanged(oldValue, value)) {
|
if (hasValueChangedUnwrapSafeValue(oldValue, value)) {
|
||||||
throwErrorIfNoChangesMode(false, oldValue, value);
|
throwErrorIfNoChangesMode(false, oldValue, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {unwrapSafeValue} from '../../sanitization/bypass';
|
||||||
import {PropertyAliases, TNodeFlags} from '../interfaces/node';
|
import {PropertyAliases, TNodeFlags} from '../interfaces/node';
|
||||||
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags, TStylingNode} from '../interfaces/styling';
|
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags, TStylingNode} from '../interfaces/styling';
|
||||||
import {NO_CHANGE} from '../tokens';
|
import {NO_CHANGE} from '../tokens';
|
||||||
|
@ -170,6 +171,14 @@ export function getPropValuesStartPosition(
|
||||||
return startPosition;
|
return startPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasValueChangedUnwrapSafeValue(
|
||||||
|
a: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined | {},
|
||||||
|
b: NO_CHANGE | StylingMapArray | number | String | string | null | boolean | undefined |
|
||||||
|
{}): boolean {
|
||||||
|
return hasValueChanged(unwrapSafeValue(a), unwrapSafeValue(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function hasValueChanged(
|
export function hasValueChanged(
|
||||||
a: NO_CHANGE | StylingMapArray | number | string | null | boolean | undefined | {},
|
a: NO_CHANGE | StylingMapArray | number | string | null | boolean | undefined | {},
|
||||||
b: NO_CHANGE | StylingMapArray | number | string | null | boolean | undefined | {}): boolean {
|
b: NO_CHANGE | StylingMapArray | number | string | null | boolean | undefined | {}): boolean {
|
||||||
|
|
|
@ -87,9 +87,11 @@ class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
|
||||||
getTypeName() { return BypassType.ResourceUrl; }
|
getTypeName() { return BypassType.ResourceUrl; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapSafeValue(value: string | SafeValue): string {
|
export function unwrapSafeValue(value: SafeValue): string;
|
||||||
return value instanceof SafeValueImpl ? value.changingThisBreaksApplicationSecurity :
|
export function unwrapSafeValue<T>(value: T): T;
|
||||||
value as string;
|
export function unwrapSafeValue<T>(value: T | SafeValue): T {
|
||||||
|
return value instanceof SafeValueImpl ? value.changingThisBreaksApplicationSecurity as any as T :
|
||||||
|
value as any as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2365,6 +2365,25 @@ describe('styling', () => {
|
||||||
|
|
||||||
function splitSortJoin(s: string) { return s.split(/\s+/).sort().join(' ').trim(); }
|
function splitSortJoin(s: string) { return s.split(/\s+/).sort().join(' ').trim(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ExpressionChangedAfterItHasBeenCheckedError', () => {
|
||||||
|
it('should not throw when bound to SafeValue', () => {
|
||||||
|
@Component({template: `<div [style.background-image]="iconSafe"></div>`})
|
||||||
|
class MyComp {
|
||||||
|
icon = 'https://i.imgur.com/4AiXzf8.jpg';
|
||||||
|
get iconSafe() { return this.sanitizer.bypassSecurityTrustStyle(`url("${this.icon}")`); }
|
||||||
|
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture =
|
||||||
|
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
|
||||||
|
fixture.detectChanges(true /* Verify that check no changes does not cause an exception */);
|
||||||
|
const div: HTMLElement = fixture.nativeElement.querySelector('div');
|
||||||
|
expect(div.style.getPropertyValue('background-image'))
|
||||||
|
.toEqual('url("https://i.imgur.com/4AiXzf8.jpg")');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function assertStyleCounters(countForSet: number, countForRemove: number) {
|
function assertStyleCounters(countForSet: number, countForRemove: number) {
|
||||||
|
|
Loading…
Reference in New Issue