fix(core): treat `[class]` and `[className]` as unrelated bindings (#35668)

Before this change `[class]` and `[className]` were both converted into `ɵɵclassMap`. The implication of this is that at runtime we could not differentiate between the two and as a result we treated `@Input('class')` and `@Input('className)` as equivalent.

This change makes `[class]` and `[className]` distinct. The implication of this is that `[class]` becomes `ɵɵclassMap` instruction but  `[className]` becomes `ɵɵproperty' instruction. This means that `[className]` will no longer participate in styling and will overwrite the DOM `class` value.

Fix #35577

PR Close #35668
This commit is contained in:
Miško Hevery 2020-02-25 10:31:01 -08:00 committed by atscott
parent a4e956a7cf
commit a153b61098
4 changed files with 44 additions and 34 deletions

View File

@ -202,8 +202,7 @@ export class StylingBuilder {
let binding: BoundStylingEntry|null = null;
const prefix = name.substring(0, 6);
const isStyle = name === 'style' || prefix === 'style.' || prefix === 'style!';
const isClass = !isStyle &&
(name === 'class' || name === 'className' || prefix === 'class.' || prefix === 'class!');
const isClass = !isStyle && (name === 'class' || prefix === 'class.' || prefix === 'class!');
if (isStyle || isClass) {
const isMapBased = name.charAt(5) !== '.'; // style.prop or class.prop makes this a no
const property = name.substr(isMapBased ? 5 : 6); // the dot explains why there's a +1

View File

@ -53,6 +53,5 @@ export function setDirectiveInputsWhichShadowsStyling(
const inputs = tNode.inputs !;
const property = isClassBased ? 'class' : 'style';
// We support both 'class' and `className` hence the fallback.
const stylingInputs = inputs[property] || (isClassBased && inputs['className']);
setInputsForProperty(tView, lView, stylingInputs, property, value);
setInputsForProperty(tView, lView, inputs[property], property, value);
}

View File

@ -907,7 +907,7 @@ function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
}
if (inputsStore !== null) {
if (inputsStore.hasOwnProperty('class') || inputsStore.hasOwnProperty('className')) {
if (inputsStore.hasOwnProperty('class')) {
tNode.flags |= TNodeFlags.hasClassInput;
}
if (inputsStore.hasOwnProperty('style')) {

View File

@ -1129,17 +1129,17 @@ describe('styling', () => {
});
onlyInIvy('only ivy combines static and dynamic class-related attr values')
.it('should write to a `className` input binding, when static `class` is present', () => {
.it('should write combined class attribute and class binding to the class input', () => {
@Component({
selector: 'comp',
template: `{{className}}`,
})
class Comp {
@Input() className: string = '';
@Input('class') className: string = '';
}
@Component({
template: `<comp class="static" [className]="'my-className'"></comp>`,
template: `<comp class="static" [class]="'my-className'"></comp>`,
})
class App {
}
@ -1150,32 +1150,6 @@ describe('styling', () => {
expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('static my-className');
});
onlyInIvy('in Ivy [class] and [className] bindings on the same element are not allowed')
.it('should throw an error in case [class] and [className] bindings are used on the same element',
() => {
@Component({
selector: 'comp',
template: `{{class}} - {{className}}`,
})
class Comp {
@Input() class: string = '';
@Input() className: string = '';
}
@Component({
template: `<comp [class]="'my-class'" [className]="'className'"></comp>`,
})
class App {
}
TestBed.configureTestingModule({declarations: [Comp, App]});
expect(() => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
})
.toThrowError(
'[class] and [className] bindings cannot be used on the same element simultaneously');
});
onlyInIvy('only ivy persists static class/style attrs with their binding counterparts')
.it('should write to a `class` input binding if there is a static class value and there is a binding value',
() => {
@ -3475,6 +3449,44 @@ describe('styling', () => {
expectClass(cmp1).toEqual({foo: true, bar: true});
expectClass(cmp2).toEqual({foo: true, bar: true});
});
it('should not bind [class] to @Input("className")', () => {
@Component({
selector: 'my-cmp',
template: `className = {{className}}`,
})
class MyCmp {
@Input()
className: string = 'unbound';
}
@Component({template: `<my-cmp [class]="'bound'"></my-cmp>`})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyApp, MyCmp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('className = unbound');
});
it('should not bind class to @Input("className")', () => {
@Component({
selector: 'my-cmp',
template: `className = {{className}}`,
})
class MyCmp {
@Input()
className: string = 'unbound';
}
@Component({template: `<my-cmp class="bound"></my-cmp>`})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyApp, MyCmp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('className = unbound');
});
});
});