diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 78c4afbe4c..8eb0b566d3 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -201,6 +201,10 @@ export class StylingBuilder { const entry: BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null}; if (isMapBased) { + if (this._classMapInput) { + throw new Error( + '[class] and [className] bindings cannot be used on the same element simultaneously'); + } this._classMapInput = entry; } else { (this._singleClassInputs = this._singleClassInputs || []).push(entry); diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 5e1c9c9936..114351d6ca 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -19,7 +19,7 @@ import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; import {setUpAttributes} from '../util/attrs_utils'; -import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../util/styling_utils'; +import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared'; @@ -132,7 +132,8 @@ export function ɵɵelementEnd(): void { } if (hasClassInput(tNode)) { - setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']); + const inputName: string = selectClassBasedInputName(tNode.inputs !); + setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName]); } if (hasStyleInput(tNode)) { diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index da605a6df1..85157d0be3 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -853,7 +853,7 @@ function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void { } if (inputsStore !== null) { - if (inputsStore.hasOwnProperty('class')) { + if (inputsStore.hasOwnProperty('class') || inputsStore.hasOwnProperty('className')) { tNode.flags |= TNodeFlags.hasClassInput; } if (inputsStore.hasOwnProperty('style')) { diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 159db4d871..ef2b50c894 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -19,7 +19,7 @@ import {activateStylingMapFeature} from '../styling/map_based_bindings'; import {attachStylingDebugObject} from '../styling/styling_debug'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; -import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, setValue, stylingMapToString} from '../util/styling_utils'; +import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; @@ -402,7 +402,7 @@ function updateDirectiveInputValue( // directive input(s) in the event that it is falsy during the // first update pass. if (newValue || isContextLocked(context, false)) { - const inputName = isClassBased ? 'class' : 'style'; + const inputName: string = isClassBased ? selectClassBasedInputName(tNode.inputs !) : 'style'; const inputs = tNode.inputs ![inputName] !; const initialValue = getInitialStylingValue(context); const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased); diff --git a/packages/core/src/render3/util/styling_utils.ts b/packages/core/src/render3/util/styling_utils.ts index 1a4e26a2c7..f96f53c155 100644 --- a/packages/core/src/render3/util/styling_utils.ts +++ b/packages/core/src/render3/util/styling_utils.ts @@ -5,7 +5,7 @@ * 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 {TNode, TNodeFlags} from '../interfaces/node'; +import {PropertyAliases, TNode, TNodeFlags} from '../interfaces/node'; import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling'; import {NO_CHANGE} from '../tokens'; @@ -433,3 +433,9 @@ export function normalizeIntoStylingMap( return stylingMapArr; } + +// TODO (matsko|AndrewKushnir): refactor this once we figure out how to generate separate +// `input('class') + classMap()` instructions. +export function selectClassBasedInputName(inputs: PropertyAliases): string { + return inputs.hasOwnProperty('class') ? 'class' : 'className'; +} \ No newline at end of file diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 1a6c4a2d65..e0c02739c6 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -551,6 +551,74 @@ describe('styling', () => { expect(capturedMyClassBindingCount).toEqual(1); }); + it('should write to a `className` input binding', () => { + @Component({ + selector: 'comp', + template: `{{className}}`, + }) + class Comp { + @Input() className: string = ''; + } + @Component({ + template: ``, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [Comp, App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('my-className'); + }); + + onlyInIvy('only ivy combines static and dynamic class-related attr values') + .it('should write to a `className` input binding, when static `class` is present', () => { + @Component({ + selector: 'comp', + template: `{{className}}`, + }) + class Comp { + @Input() className: string = ''; + } + + @Component({ + template: ``, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [Comp, App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + 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: ``, + }) + 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', () => { diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 1edbb3f222..ec7b4c2d66 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -584,6 +584,9 @@ { "name": "saveResolvedLocalsInData" }, + { + "name": "selectClassBasedInputName" + }, { "name": "selectIndexInternal" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index a03e73d82e..2a8f0e48d6 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -1184,6 +1184,9 @@ { "name": "searchTokensOnInjector" }, + { + "name": "selectClassBasedInputName" + }, { "name": "selectIndexInternal" },