fix(ivy): ensure map-based interpolation works with other map-based sources (#33236)

Prior to this fix if a map-based class or style binding wrote
its values onto an elemenent, the internal styling context would
not register the binding if the initial value as a `NO_CHANGE`
value. This situation occurs if a directive takes control of the
`class` or `style` input values and then returns a `NO_CHANGE` value
if the initial value is empty.

This patch ensures that all bindings are always registered with the
`TStylingContext` data-structure even if their initial value is
an instance of `NO_CHANGE`.

PR Close #33236
This commit is contained in:
Matias Niemelä 2019-10-16 15:13:37 -07:00
parent d5b59009d4
commit 7b64680670
2 changed files with 29 additions and 2 deletions

View File

@ -60,7 +60,12 @@ export function updateClassViaContext(
const isMapBased = !prop; const isMapBased = !prop;
const state = getStylingState(element, directiveIndex); const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++; const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
if (value !== NO_CHANGE) { const hostBindingsMode = isHostStylingActive(state.sourceIndex);
// even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngClass])
// then we still need to register the binding within the context so that the context
// is aware of the binding before it gets locked.
if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) {
const updated = updateBindingData( const updated = updateBindingData(
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate, context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
false); false);
@ -95,7 +100,12 @@ export function updateStyleViaContext(
const isMapBased = !prop; const isMapBased = !prop;
const state = getStylingState(element, directiveIndex); const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++; const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
if (value !== NO_CHANGE) { const hostBindingsMode = isHostStylingActive(state.sourceIndex);
// even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngStyle])
// then we still need to register the binding within the context so that the context
// is aware of the binding before it gets locked.
if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) {
const sanitizationRequired = isMapBased ? const sanitizationRequired = isMapBased ?
true : true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);

View File

@ -2292,6 +2292,23 @@ describe('styling', () => {
fixture.detectChanges(); fixture.detectChanges();
}).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/); }).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
}); });
it('should properly merge class interpolation with class-based directives', () => {
@Component(
{template: `<div class="zero {{one}}" [class.two]="true" [ngClass]="'three'"></div>`})
class MyComp {
one = 'one';
}
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('zero');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('one');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('two');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('three');
});
}); });
function assertStyleCounters(countForSet: number, countForRemove: number) { function assertStyleCounters(countForSet: number, countForRemove: number) {