fix(ivy): ExpressionChangedAfterItHasBeenCheckedError for SafeValue (#33749)

Fix #33448

PR Close #33749
This commit is contained in:
Miško Hevery 2019-11-11 15:43:09 -08:00 committed by Alex Rickabaugh
parent cbb1d9f2bb
commit f2828a08bd
4 changed files with 36 additions and 6 deletions

View File

@ -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);
} }
} }

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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) {